useReducer와 useContext: React의 강력한 상태 관리 조합

박영광·2024년 8월 6일
0

React

목록 보기
23/23

useReducer와 useContext의 결합

useReducer와 useContext를 함께 사용하면 강력한 전역 상태 관리 시스템을 구축할 수 있습니다. 이 조합은 특히 중간 규모의 애플리케이션에서 Redux와 같은 외부 라이브러리 없이도 효과적인 전역 상태 관리를 가능하게 합니다.

왜 useReducer와 useContext를 함께 사용하는가?

  1. 전역 상태 관리: useContext를 통해 상태와 dispatch 함수를 앱 전체에 제공할 수 있습니다.
  2. props 드릴링 방지: 컴포넌트 트리의 모든 레벨을 통과하지 않고도 필요한 곳에 상태를 전달할 수 있습니다.
  3. 로직의 중앙화: 상태 관리 로직을 한 곳에 모아 관리할 수 있습니다.
  4. 성능 최적화: useContext와 useReducer의 조합은 불필요한 리렌더링을 줄일 수 있습니다.
  5. 코드 분할과 유지보수: 상태 로직과 UI 로직을 명확하게 분리할 수 있습니다.

구현 예제

다음은 useReducer와 useContext를 결합하여 간단한 전역 상태 관리 시스템을 구현하는 예제입니다:

import React, { createContext, useContext, useReducer } from 'react';

// 1. Context 생성
const TodoContext = createContext();

// 2. 초기 상태 정의
const initialState = {
  todos: [],
  loading: false,
  error: null
};

// 3. 리듀서 함수 정의
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return { 
        ...state, 
        todos: [...state.todos, action.payload]
      };
    case 'TOGGLE_TODO':
      return { 
        ...state, 
        todos: state.todos.map(todo => 
          todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
        )
      };
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    case 'SET_ERROR':
      return { ...state, error: action.payload };
    default:
      return state;
  }
}

// 4. Context Provider 컴포넌트 생성
export function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  // 5. 최적화를 위한 메모이제이션된 값
  const value = React.useMemo(() => [state, dispatch], [state]);

  return <TodoContext.Provider value={value}>{children}</TodoContext.Provider>;
}

// 6. 커스텀 훅 생성
export function useTodo() {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodo must be used within a TodoProvider');
  }
  return context;
}

// 7. 사용 예시
function TodoList() {
  const [state, dispatch] = useTodo();
  const { todos, loading, error } = state;

  // 할 일 추가 함수
  const addTodo = (text) => {
    dispatch({ type: 'ADD_TODO', payload: { id: Date.now(), text, completed: false } });
  };

  // 할 일 토글 함수
  const toggleTodo = (id) => {
    dispatch({ type: 'TOGGLE_TODO', payload: id });
  };

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <ul>
        {todos.map(todo => (
          <li 
            key={todo.id} 
            onClick={() => toggleTodo(todo.id)}
            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
          >
            {todo.text}
          </li>
        ))}
      </ul>
      <button onClick={() => addTodo('New Todo')}>Add Todo</button>
    </div>
  );
}

// 8. 앱 컴포넌트
function App() {
  return (
    <TodoProvider>
      <h1>Todo List</h1>
      <TodoList />
    </TodoProvider>
  );
}

이 예제에 대한 자세한 설명:

  1. Context 생성: createContext를 사용하여 TodoContext를 생성합니다. 이 context는 상태와 dispatch 함수를 저장합니다.
  2. 초기 상태 정의: todos 배열, loading 상태, error 상태를 포함하는 초기 상태 객체를 정의합니다.
  3. 리듀서 함수 정의: 여러 액션 타입(ADD_TODO, TOGGLE_TODO, SET_LOADING, SET_ERROR)을 처리하는 리듀서 함수를 정의합니다.
  4. Context Provider 컴포넌트 생성: TodoProvider 컴포넌트는 useReducer를 사용하여 상태와 dispatch 함수를 관리하고, 이를 Context.Provider를 통해 자식 컴포넌트들에게 제공합니다.
  5. 최적화: useMemo를 사용하여 상태가 변경될 때만 context 값이 새로 생성되도록 최적화합니다. 이는 불필요한 리렌더링을 방지합니다.
  6. 커스텀 훅 생성: useTodo 커스텀 훅을 만들어 context 사용을 간편하게 만듭니다. 이 훅은 context가 제대로 사용되고 있는지 확인하는 에러 처리도 포함합니다.
  7. 사용 예시: TodoList 컴포넌트에서 useTodo 훅을 사용하여 상태와 dispatch 함수에 접근합니다. 이를 통해 할 일을 추가하고 토글하는 기능을 구현합니다.
  8. 앱 컴포넌트: 전체 앱을 TodoProvider로 감싸 모든 자식 컴포넌트에서 todo 상태와 관련 함수들에 접근할 수 있게 합니다.

장단점

장점
1. 간결성: Redux에 비해 보일러플레이트 코드가 적습니다.
2. 학습 곡선: React의 내장 API만을 사용하므로 학습이 용이합니다.
3. 유연성: 애플리케이션의 필요에 따라 쉽게 확장할 수 있습니다.
4. 성능: 필요한 컴포넌트만 리렌더링되도록 최적화할 수 있습니다.

단점
1. 복잡성 관리: 대규모 애플리케이션에서는 상태 관리가 복잡해질 수 있습니다.
2. 미들웨어 지원 부족: Redux의 강력한 미들웨어 생태계를 활용할 수 없습니다.
3. 도구 지원: Redux DevTools와 같은 강력한 개발 도구를 사용할 수 없습니다.

성능 최적화 팁
1. 상태 분할: 큰 상태 객체를 여러 개의 작은 context로 나누어 관리합니다.
2. 메모이제이션: React.memo, useMemo, useCallback을 활용하여 불필요한 리렌더링을 방지합니다.
3. 선택적 구독: 필요한 상태만 선택적으로 구독하도록 최적화합니다.

const { todos } = useTodo(state => state.todos);  // 선택적 구독

고급 사용 팁

  1. 액션 생성자 함수: 액션 객체 생성을 위한 함수를 만들어 사용하면 코드의 일관성과 재사용성을 높일 수 있습니다.
const addTodo = (text) => ({ type: 'ADD_TODO', payload: { id: Date.now(), text, completed: false } });
const toggleTodo = (id) => ({ type: 'TOGGLE_TODO', payload: id });
  1. 비동기 작업 처리: 비동기 작업(예: API 호출)을 처리하기 위해 useEffect와 함께 사용할 수 있습니다.
useEffect(() => {
  dispatch({ type: 'SET_LOADING', payload: true });
  fetchTodos()
    .then(todos => dispatch({ type: 'SET_TODOS', payload: todos }))
    .catch(error => dispatch({ type: 'SET_ERROR', payload: error.message }))
    .finally(() => dispatch({ type: 'SET_LOADING', payload: false }));
}, []);
  1. 상태 선택자 사용: 큰 상태 객체에서 특정 부분만 사용하는 컴포넌트의 경우, 선택자 함수를 사용하여 필요한 상태만 가져올 수 있습니다. 이는 불필요한 리렌더링을 방지하는 데 도움이 됩니다.
const selectTodos = state => state.todos;
const selectLoading = state => state.loading;

function TodoCount() {
  const [state] = useTodo();
  const todos = selectTodos(state);
  return <div>Todo Count: {todos.length}</div>;
}
  1. 미들웨어 패턴: Redux 미들웨어와 유사한 패턴을 구현하여 로깅, 에러 처리 등의 부가 기능을 추가할 수 있습니다.
function logger(dispatch) {
  return action => {
    console.log('dispatching', action);
    let result = dispatch(action);
    console.log('next state', result);
    return result;
  };
}

// Provider 내부에서
const [state, baseDispatch] = useReducer(todoReducer, initialState);
const dispatch = logger(baseDispatch);

결론

useReducer와 useContext의 조합은 React 애플리케이션에서 강력하고 유연한 상태 관리 솔루션을 제공합니다. 이 방법은 복잡한 상태 로직을 관리하면서도, 컴포넌트 간의 결합도를 낮추고 코드의 재사용성을 높일 수 있습니다.

특히 중소 규모의 애플리케이션에서 이 패턴은 Redux와 같은 외부 라이브러리의 좋은 대안이 될 수 있습니다. 그러나 매우 큰 규모의 애플리케이션이나 더 복잡한 상태 관리가 필요한 경우에는 여전히 Redux나 MobX와 같은 전문적인 상태 관리 라이브러리를 고려해볼 수 있습니다.

useReducer와 useContext를 효과적으로 활용하면, React의 내장 기능만으로도 강력하고 확장 가능한 상태 관리 시스템을 구축할 수 있습니다.


#ReactHooks #useReducer #useContext #StateManagement #ReactPatterns #ContextAPI #GlobalState #ReactPerformance

profile
매일 1mm씩 성장하겠습니다

0개의 댓글