useReducer는 React의 내장 Hook으로, 복잡한 상태 로직을 관리하는 데 사용됩니다. 이는 Redux의 핵심 개념인 리듀서(reducer)에서 영감을 받았습니다.
리듀서는 다음과 같은 형태를 가진 순수 함수입니다:
(state, action) => newState
여기서:
useReducer의 기본 사용법은 다음과 같습니다:
const [state, dispatch] = useReducer(reducer, initialState);
여기서:
import React, { useReducer } from 'react';
// 액션 타입 상수 정의
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
const SET_VALUE = 'SET_VALUE';
// 리듀서 함수
function counterReducer(state, action) {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 };
case DECREMENT:
return { count: state.count - 1 };
case RESET:
return { count: 0 };
case SET_VALUE:
return { count: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: INCREMENT })}>+</button>
<button onClick={() => dispatch({ type: DECREMENT })}>-</button>
<button onClick={() => dispatch({ type: RESET })}>Reset</button>
<button onClick={() => dispatch({ type: SET_VALUE, payload: 10 })}>Set to 10</button>
</div>
);
}
이 예제에서:
useState와 useReducer는 모두 상태 관리를 위한 도구지만, 각각 다른 상황에서 더 적합합니다.
import React, { useState, useReducer } from 'react';
// useState를 사용한 카운터
function CounterWithState() {
const [count, setCount] = useState(0);
const increment = () => setCount(prevCount => prevCount + 1);
const decrement = () => setCount(prevCount => prevCount - 1);
const reset = () => setCount(0);
return (
<div>
<h2>Counter with useState</h2>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
}
// useReducer를 사용한 카운터
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}
function CounterWithReducer() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<h2>Counter with useReducer</h2>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
}
이 예제에서 볼 수 있듯이, 간단한 카운터의 경우 useState가 더 간결하지만, 상태 업데이트 로직이 복잡해지면 useReducer가 더 체계적인 관리를 가능하게 합니다.
이제 좀 더 복잡한 예제를 통해 useReducer의 강력함을 살펴보겠습니다. 여기서는 TODO 리스트 애플리케이션을 만들어보겠습니다.
import React, { useReducer, useState } from 'react';
// 액션 타입 정의
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
const SET_FILTER = 'SET_FILTER';
// 초기 상태
const initialState = {
todos: [],
filter: 'ALL' // 'ALL', 'ACTIVE', 'COMPLETED'
};
// 리듀서 함수
function todoReducer(state, action) {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.payload, completed: false }]
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
)
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case SET_FILTER:
return {
...state,
filter: action.payload
};
default:
return state;
}
}
// 필터링된 할 일 목록을 반환하는 함수
function getFilteredTodos(todos, filter) {
switch (filter) {
case 'ACTIVE':
return todos.filter(todo => !todo.completed);
case 'COMPLETED':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, initialState);
const [text, setText] = useState('');
const filteredTodos = getFilteredTodos(state.todos, state.filter);
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
dispatch({ type: ADD_TODO, payload: text });
setText('');
};
return (
<div>
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">Add Todo</button>
</form>
<div>
<button onClick={() => dispatch({ type: SET_FILTER, payload: 'ALL' })}>All</button>
<button onClick={() => dispatch({ type: SET_FILTER, payload: 'ACTIVE' })}>Active</button>
<button onClick={() => dispatch({ type: SET_FILTER, payload: 'COMPLETED' })}>Completed</button>
</div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => dispatch({ type: TOGGLE_TODO, payload: todo.id })}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: DELETE_TODO, payload: todo.id })}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
이 예제에서 우리는:
이 예제는 useReducer를 사용하여 복잡한 상태 로직을 어떻게 체계적으로 관리할 수 있는지 보여줍니다.
장점:
1. 예측 가능한 상태 변화: 모든 상태 변화가 리듀서 함수 내에서 명확하게 정의됩니다.
2. 디버깅 용이성: 액션과 그에 따른 상태 변화를 쉽게 추적할 수 있습니다.
3. 로직의 분리: 상태 관리 로직을 컴포넌트에서 분리할 수 있어 코드 구조가 개선됩니다.
4. 테스트 용이성: 리듀서 함수는 순수 함수이므로 테스트하기 쉽습니다.
5. 복잡한 상태 관리: 여러 하위 값을 가진 복잡한 상태를 효과적으로 관리할 수 있습니다.
6. 성능 최적화: 복잡한 상태 업데이트를 최적화하는 데 도움이 됩니다.
단점:
1. 러닝 커브: useState에 비해 개념 이해와 사용법 습득에 시간이 더 걸릴 수 있습니다.
2. 보일러플레이트: 간단한 상태 관리의 경우 useState보다 더 많은 코드가 필요할 수 있습니다.
3. 과도한 추상화: 작은 규모의 애플리케이션에서는 불필요한 복잡성을 야기할 수 있습니다.
4. 액션 타입 관리: 많은 액션 타입을 관리해야 할 경우 코드가 복잡해질 수 있습니다.
const addTodo = (text) => ({ type: ADD_TODO, payload: text });
const toggleTodo = (id) => ({ type: TOGGLE_TODO, payload: id });
// 사용
dispatch(addTodo('새로운 할 일'));
dispatch(toggleTodo(1));
const initializeTodos = () => {
const localData = localStorage.getItem('todos');
return localData ? JSON.parse(localData) : [];
};
const [state, dispatch] = useReducer(todoReducer, null, initializeTodos);
const TodoContext = React.createContext();
const TodoProvider = ({ children }) => {
const [state, dispatch] = useReducer(todoReducer, initialState);
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
};
// 자식 컴포넌트에서 사용
const { state, dispatch } = useContext(TodoContext);
type State = {
todos: { id: number; text: string; completed: boolean }[];
filter: 'ALL' | 'ACTIVE' | 'COMPLETED';
};
type Action =
| { type: 'ADD_TODO'; payload: string }
| { type: 'TOGGLE_TODO'; payload: number }
| { type: 'SET_FILTER'; payload: State['filter'] };
const todoReducer = (state: State, action: Action): State => {
// 리듀서 로직
};
import produce from 'immer';
const todoReducer = produce((draft, action) => {
switch (action.type) {
case ADD_TODO:
draft.todos.push({ id: Date.now(), text: action.payload, completed: false });
break;
// 다른 케이스들...
}
});
const memoizedDispatch = useCallback(dispatch, []);
const memoizedState = useMemo(() => computeExpensiveState(state), [state]);
const loggerMiddleware = (reducer) => (state, action) => {
console.log('이전 상태:', state);
console.log('액션:', action);
const nextState = reducer(state, action);
console.log('다음 상태:', nextState);
return nextState;
};
const [state, dispatch] = useReducer(loggerMiddleware(todoReducer), initialState);
useReducer는 React 애플리케이션에서 복잡한 상태 로직을 관리하는 강력한 도구입니다. 상태 변화의 예측 가능성을 높이고, 로직을 분리하여 코드의 구조를 개선할 수 있습니다. 특히 여러 관련 상태를 함께 관리해야 하거나, 상태 변화 로직이 복잡한 경우에 useReducer의 사용을 고려해보세요.
useReducer는 단순히 상태 관리 도구를 넘어 애플리케이션의 아키텍처를 설계하는 데 도움을 줍니다. 잘 구조화된 리듀서와 액션을 통해 비즈니스 로직을 명확하게 표현할 수 있으며, 이는 코드의 가독성과 유지보수성을 크게 향상시킵니다.
그러나 모든 상황에 useReducer가 적합한 것은 아닙니다. 간단한 상태 관리나 작은 규모의 애플리케이션에서는 useState가 여전히 좋은 선택일 수 있습니다. 프로젝트의 복잡성과 규모, 팀의 숙련도를 고려하여 적절한 상태 관리 전략을 선택하는 것이 중요합니다.
최종적으로, useReducer를 효과적으로 활용하면 React 애플리케이션의 상태 관리를 더욱 체계적이고 확장 가능하게 만들 수 있습니다. 이는 애플리케이션의 성능을 최적화하고, 코드의 구조를 개선하며, 개발 경험을 향상시키는 데 큰 도움이 될 것입니다.
#React #useReducer #Hooks #상태관리 #리액트훅 #프론트엔드개발 #리액트상태관리 #리듀서패턴 #리액트최적화 #자바스크립트