useReducer로 맛깔나게 상태 관리하기

gydotb·2025년 1월 17일
4

독감자스터디

목록 보기
3/5

정말로 맛깔나는 제목을 정해준 @gominzip 에게 무한한 감사를 바칩니다…💗

해당 주제 선정 이유

복잡한 조건에 대한 state 관리에 대한 대안을 찾다가 아래 글을 발견하고 실제 프로젝트에도 useReducer를 적용해보게 되었다. 기존에 사용해 왔던 다른 hook들에 비해 익숙하지 않은 개념이어서, 조금 더 잘 알고 싶어 따로 정리를 진행해보려 한다.

React 탐구생활 - useReducer 활용

들어가기 전에…

React 공식문서에서는 내장 hook을 5개 분류로 구별한다.

내장 hook의 분류

  • State hooks : useState, useReducer
  • Context hooks : useContext
  • Ref hooks : useRef, useImperativeHandle
  • Effect hooks : useEffect
  • Performance hooks : useMemo, useCallback

즉, 지금부터 이야기할 useReducerstate hooks에 포함되며 useState와 같은 분류에 속한다는 걸 먼저 이해하면 훨씬 이해가 쉬울 것이다!

🎯 useReducer

🕹️ State 관리를 해주겠다고? 너 누군데?

언제 사용할까?

state 업데이트가 여러 이벤트 핸들러로 분산되는 경우, 업데이트 로직reducer로 관리할 수 있다.

useState와 useReducer

useState useReducer
state의 특징

- 컴포넌트 내부에 state 업데이트 로직 존재

- 관리 대상 state가 단순 + 간단한 로직

- 업데이트 시, 이전 상태를 덮어씀

- next state를 직접 지정해주는 방식으로 업데이트(ex. setState(state+1))

- 컴포넌트 외부에 state 업데이트 로직 존재

- 관리 대상 state가 여러 개 + 복잡한 로직

- 업데이트 시, 이전 상태 변경 없이 새로운 상태 return

- action 객체 기반의 업데이트(ex. dispatch({type:plus}))

코드 미리 작성해야 하는 코드량 적음.

reducer, action 전달부 두 가지 모두를 작성해야 함.

🚨 하지만 이벤트 핸들러가 겹칠 경우 코드량 감소에 도움

가독성 간단한 state 업데이트의 경우 가독성이 좋음. 복잡한 구조를 가진 state를 업데이트하는 경우, 로직 동작 방법과 이벤트 핸들러 구현 부분을 명확히 파악가능

구분 표 참고자료

한 마디로, 이벤트 핸들러 + useState의 조합 여러 개일 때 이용하면 효과가 좋은 느낌으로 이해했다.

useReducer 구조

const [state, dispatch] = useReducer(reducer, initialArg, init?)

매개변수

  • reducer : state가 어떻게 업데이트 되는지 지정하는 리듀서 함수로, state와 action을 인수로 받아 다음 state를 반환
  • initialArg : 초기 state 계산 값.
  • init(선택) : 초기 state를 반환하는 초기화 함수.
    • 없을 경우: 초기 state === init(initialArg)
    • 있을 경우: 초기 state === init(initialArg)

반환값

  • state : 현재 state.
  • dispatch : state를 업데이트, 리렌더링하는 함수

🖋️ reducer 작성해보기

reducer

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

stateaction 객체를 전달 받고 switch문의 case에 따라 다음 state를 계산하고 반환한다.

reducer 작성 팁

  1. reducer는 반드시 순수해야 한다.
    • reducersetState와 비슷하게 렌더링 중 실행되므로, 반드시 순수해야 함.
      따라서 요청, timeout 스케줄링, 사이드 이펙트 수행 불가.
      - 순수 함수 : 입력 값이 같다면 결과 값도 항상 동일하게 보장되어야 함
    • 객체와 배열을 변경하지 않고 업데이트 해야 함.
  2. 각 action은 데이터 내 여러 변경이 있어도 하나의 사용자 상호작용을 설명해야 함.

기존의 state 로직을 reducer로 통합하기(공식 예제 코드)

기존 state 로직

/*
* state management with useState
*/

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);
  function handleAddTask(text) {
    //setTask 호출, add handler
  }
  function handleChangeTask(task) {
    //setTask 호출, change handler
  }
  function handleDeleteTask(taskId) {
   //setTask 호출, delete handler
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}
// 생략

state에서 useReducer로 바꾸는 규칙

  1. state 설정을 action을 dispatch 함수로 전달하도록 수정
  2. reducer 함수 작성
  3. 컴포넌트에서 reducer 사용
/*
* state management by useReducer
*/
export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );
  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }
  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }
  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}
/*
* reducer function
*/
function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

그래서… useReducer를 왜 사용해야 하는데?

state 관리의 차이점 (w. useState)

reducer를 사용한 state 관리는 state를 직접 설정하는 것과 약간 다릅니다. state를 설정하여 React에게 무엇을 할 지를 지시하는 대신, 이벤트 핸들러에서 action을 전달하여 사용자가 방금 한 일을 지정합니다.
(state 업데이트 로직은 다른 곳에 있습니다!)

즉, 이벤트 핸들러를 통해 tasks를 설정하는 대신 task를 추가/변경/삭제하는 action을 전달하는 것입니다. 이러한 방식이 사용자의 의도를 더 명확하게 설명합니다.

관심사 분리(SoC)를 통한 로직 단순화에 가까운 것으로 이해. 그리고 useState만 사용하거나 useReducer만 사용해야 하는 건 또 아니라고 하니까 적절하게 잘 섞어 이용하자.

😵‍💫 dispatch 이것 뭐예요…?

const [state, dispatch] = useReducer(reducer, { age: 42 });

function handleClick() {
  dispatch({ type: 'incremented_age' });
  // ...

→ 현재 statedispatch를 통해 전달된 action을 제공받아 호출된 reducer의 반환값을 통해 다음 state값을 설정

주의 사항

  • dispatch 함수는 오직 다음 렌더링에 사용할 state 변수만 업데이트 합니다. 만약 dispatch 함수를 호출한 직후에 state 변수를 읽는다면 호출 이전의 최신화되지 않은 값을 참조할 것입니다.
  • Object.is 비교를 통해 새롭게 제공된 값과 현재 state를 비교한 값이 같을 경우, React는 컴포넌트와 해당 컴포넌트의 자식 요소들의 리렌더링을 건너뜁니다. 이것은 최적화에 관련된 동작으로써 결과를 무시하기 전에 컴포넌트가 호출되지만, 호출된 결과가 코드에 영향을 미치지는 않습니다.
  • React는 state의 업데이트를 batch합니다. 이벤트 핸들러의 모든 코드가 수행되고 set 함수가 모두 호출된 후에 화면을 업데이트 합니다. 이는 하나의 이벤트에 리렌더링이 여러번 일어나는 것을 방지합니다. DOM 접근 등 이른 화면 업데이트를 강제해야할 특수한 상황이 있을 경우 flushSync를 사용할 수 있습니다.

👾 그래서 실제로는요…

다중 중첩 조건에 대한 state 관리를 위해 실제로 useReducer를 적용한 과정을 설명한 제 글(…) 입니다. state 관리를 위해 useReducer 를 이용하자라는 결론에 도달하기까지의 시도와 어떻게 useReducer를 적용했는지에 대해 설명했으니 웃긴 글 보는 기분으로 가볍게 읽어주세요
useReducer를 이용해 복잡한 조건 필터링을 개선해보자🌀

reference

React 공식문서 - useReducer
React 공식문서 - state 로직을 reducer로 작성하기

profile
프론트엔드 일짱되기

0개의 댓글

관련 채용 정보