React는 구성 요소를 기반으로 한 JavaScript 라이브러리로, 사용자 인터페이스(UI) 구축을 위해 널리 사용됩니다. React의 두 가지 주요 상태 관리 훅은 useState와 useReducer입니다. 이번 강좌에서는 useReducer에 대해 깊이 있게 살펴보겠습니다.
1. useReducer란?
useReducer는 리액트의 내장 훅 중 하나로, 복잡한 상태 로직을 관리하기 위해 사용됩니다. 이 훅은 상태 변화가 있을 때마다 특정한 함수를 호출하여 상태를 업데이트합니다. useState가 간단한 상태 관리를 위한 훅이라면, useReducer는 보다 복잡한 로직이나 다수의 상태 변화를 필요로 할 때 적합합니다.
2. useReducer 기본 사용법
useReducer를 사용하는 기본적인 방법은 다음과 같습니다.
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
위의 예제에서 볼 수 있듯이 useReducer는 두 개의 인자를 사용합니다:
reducer 함수와 초기 상태(initial state). reducer 함수는 현재 상태와 액션을 매개변수로 받아서 새로운 상태를 반환합니다. dispatch 함수는 액션 객체를 사용하여 상태를 업데이트합니다.
3. useReducer의 장점
- 복잡한 상태 관리: useReducer는 여러 상태를 관리하기 위한 강력한 도구입니다. 여러 액션과 상태를 명확히 규명할 수 있습니다.
- 리팩토링과 확장성: reducer 함수는 독립적으로 관리되기 때문에 상태 로직을 리팩토링하기가 용이합니다.
- 테스트 용이성: reducer 함수는 순수 함수이기 때문에, 이를 단위 테스트하기가 상대적으로 수월합니다.
4. useReducer와 useState 비교
useState와 useReducer는 상태를 관리하는 데 도움을 주지만, 사용 목적에 따라 각각의 적합한 상황이 다릅니다.
특징 | useState | useReducer |
---|---|---|
단순한 상태 | 유리 | 불리 |
복잡한 상태 | 불리 | 유리 |
상태 변경 로직 | 불리 | 유리 |
상태의 분리 | 불리 | 유리 |
5. 실전 예제: ToDo 리스트 앱 만들기
이제 useReducer를 활용하여 간단한 ToDo 리스트 앱을 만들어 보겠습니다. 이 앱은 할 일을 추가하고, 삭제할 수 있는 기능을 포함합니다.
import React, { useReducer } from 'react';
const initialState = { todos: [] };
function reducer(state, action) {
switch (action.type) {
case 'add':
return { ...state, todos: [...state.todos, action.payload] };
case 'remove':
return { ...state, todos: state.todos.filter((_, index) => index !== action.payload) };
default:
throw new Error();
}
}
function TodoApp() {
const [state, dispatch] = useReducer(reducer, initialState);
const [input, setInput] = React.useState('');
const addTodo = () => {
dispatch({ type: 'add', payload: input });
setInput('');
};
return (
<div>
<h2>ToDo List</h2>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={addTodo}>Add</button>
<ul>
{state.todos.map((todo, index) => (
<li key={index}>
{todo} <button onClick={() => dispatch({ type: 'remove', payload: index })}>Remove</button>
</li>
))}
</ul>
</div>
);
}
위의 TodoApp 컴포넌트는 사용자가 할 일을 추가하고 제거할 수 있는 기능을 제공합니다. useReducer를 사용하여 상태를 관리하며, 각 액션(type)별로 할 일을 추가하거나 삭제하는 로직을 정의하고 있습니다.
6. useReducer의 고급 사용법
useReducer는 단순한 상태 관리를 넘어서, 고급 기능을 지원합니다. 다음은 그 일부입니다:
6.1 초기 상태 로딩
초기 상태를 비동기적으로 로드할 수 있는 방법입니다. 이를 위해 useEffect 훅을 사용합니다.
import React, { useReducer, useEffect } from 'react';
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/api/todos');
const data = await response.json();
data.forEach(todo => dispatch({ type: 'add', payload: todo }));
};
fetchData();
}, []);
// 나머지 앱 코드...
}
6.2 중첩된 상태 관리
여러 개의 reducer를 조합하여 구조화된 상태 관리를 적용할 수 있습니다. context API와 함께 사용할 경우, 전역 상태 관리를 손쉽게 할 수 있습니다.
function mainReducer(state, action) {
return {
counter: counterReducer(state.counter, action),
todos: todoReducer(state.todos, action),
};
}
// 상태와 dispatch는 여러 개의 reducer로 제공됩니다.
const [state, dispatch] = useReducer(mainReducer, initialState);
7. 결론
useReducer는 리액트에서 복잡한 상태 관리 로직을 구축할 수 있는 강력한 도구입니다. 특히 여러 상태 변화가 필요한 경우, useReducer를 통해 코드의 가독성과 유지보수성을 높일 수 있습니다. 이번 강좌에서 우리가 배운 내용을 바탕으로 여러분만의 리액트 애플리케이션에서 useReducer를 활용해 보시기 바랍니다.
추가로 실습하고 싶은 예제나 질문이 있다면 댓글로 남겨주세요. 여러분의 리액트 개발 여정을 응원합니다!