[React] useState 대신 useReducer 사용하여 복잡한 상태관리하기

ryeoni·2023년 1월 26일

React

목록 보기
4/9

개요


state를 변경할 때 주로 useState를 사용하였는데 아래의 경우에 관리하기 힘들다고 느꼈다.

  1. state 가 늘어날수록
  2. 객체가 중첩될수록
  3. state 가 자주 변경될수록

내가 주로 관리하던 state는 다음과 비슷한 형태를 가지는데

const [state, setState] = useState({
  arrTodos: [
    { code: 0, hobby: '그림그리기' },
    { code: 1, hobby: '운동하기' },
  ],
  objUser: {
    num: 1000,
    name: '김영희',
  },
  strTitle: '', // 추가 input 제목
  strContent: '', // 추가 input 내용
});

이번에 맡은 일이 input도 많고 state만 60개가 넘어가서 기존의 방식으로는
안전하게 state를 관리하기 힘들었다 (useState로 코드 작성 시 너무 길어지기도 했다)

이럴 땐 useReducer가 효과적이라길래 useReducer로 결정했다!

  • useReducer 사용하는 경우
    • 상태값이 많을 때
    • 업데이트 로직이 복잡할 때
    • 개별적으로 관리될 때
    • 서로 연관되어 있을 때

useState


todo

예제는 예전에 만든 간단한 투두리스트이다.

const [state, setState] = useState({
  title: '', // 추가 input 제목
  content: '', // 추가 input 내용
});
const handleChange = ({ target }) => {
  const { name, value } = target;
  setState((prev) => ({ ...prev, [name]: value }));
};
<div>
  제목 : &nbsp;
  <input
    type="text"
    name="title"
    value={state.title}
    onChange={handleChange}
    required
    style={{
      outline: 'none',
      display: 'inline-block',
    }}
  />
</div>
<div>
  내용 : &nbsp;
  <input
    type="text"
    name="content"
    value={state.content}
    onChange={handleChange}
    required
  />
</div>

사용자가 내용을 입력할 때 마다 handleChange가 호출되어 input의 value가 변경되는 로직이다.
state가 2개 밖에 없어서 useState를 사용해도 전혀 문제없지만 연습용으로 해당 예제를 useReducer로 변경해보았다.

useReducer


기본형태

const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);

state

  • 최신 state 스냅샷

dispatch

  • state 스냅샷을 업데이트할 수 있게 해주는 함수
  • useState와 비슷한 역할이지만 작동방식이 다르다.
  • 액션을 디스패치하며 새로운 state 값을 설정한다.

reducerFn

  • 최신 state 스냅샷과 디스패치 된 액션을 가져오는 함수
  • 작동방식
    1. 새 액션이 디스패치될 때마다 리액트는 리듀서 함수를 호출한다.
    2. 리듀서 함수는 리액트가 관리하는 최신 state 스냅샷과 디스패치 된 액션을 가져온다.
    3. 새로운 업데이트 된 state를 반환한다.
  • 초기 state나 초기 함수를 설정할 수 있다.

initialState

  • 초기 state

initFn

  • 초기 state를 설정하기 위해 실행해야 하는 함수
  • 초기 state가 복잡한 경우 사용 (예: http request 결과)

예제 코드

  • initialState

    const initialInputState = {
      title: '',
      content: '',
    };
  • reducerFn

    const stateReducer = (state, action) => {
      if (action.type === 'USER_INPUT') {
        return {
          ...state,
          [action.field]: action.payload,
        };
      }
      return state;
    };
    
  • useReducer
    최상단에 import 해야된다.

    import { useState, useEffect, useReducer } from 'react';
    const [formState, dispatch] = useReducer(stateReducer, initialInputState);

이렇게 작성하면 초기 설정은 끝났다.
이제 액션을 디스패치하는 함수를 만들어서 input에 입력된 값을 변경해보겠다.


  • 액션 디스패치
    넘어오는 이벤트 객체의 속성을 구조 분해 할당식을 이용하여 분해하였다.
    const handleTextChange = ({ target }) => {
      const { name, value } = target;
      dispatch({
        type: 'USER_INPUT',
        field: name,
        payload: value,
      });
    };
  • handleTextChange 호출
    <div>
    제목 : &nbsp;
    <input
      type="text"
      name="title"
      value={formState.title}
      onChange={handleTextChange}
      required
      style={{
        outline: 'none',
        display: 'inline-block',
      }}
    />
    </div>
    <div>
    내용 : &nbsp;
    <input
      type="text"
      name="content"
      value={formState.content}
      onChange={handleTextChange}
      required
    />
    </div>
    <div>

  1. 사용자가 input에 값을 입력하면 handleTextChange 가 호출된다.
  2. handleTextChange 함수에서 액션 디스패치 및 새로운 값을 전달한다.
  3. 리듀서 함수에서 조건에 해당하는 액션이 실행되고 새로운 값이 반환된다.

input의 내용을 각각 변경하는 경우는 위 코드 처럼 사용하면 된다.


그러나...
API를 호출해서 여러 데이터를 가져온 다음
여래 개의 input 값을 동시에 변경해야 하는 경우는 코드를 어떻게 작성해야 효율적일까?


const handleAllChange = () => {
  const title = { title: '운동하기' };
  const content = { content: '러닝' };
  const mergeData = { ...title, ...content };
  dispatch({
    type: 'MULTIPLE_DATA',
    payload: mergeData,
  });
};

handleAllChange 함수를 만들고 예시 데이터로 title과 content를 선언한 다음 합친다.
❗ 주의할 점은 로컬 상수의 property가 initialState 의 property와 동일해야 된다.


if (action.type === 'USER_INPUT') {
  return {
    ...state,
    [action.field]: action.payload,
  };
}
if (action.type === 'MULTIPLE_DATA') {
  return {
    ...state,
    ...action.payload,
  };
}

"MULTIPLE_DATA" 액션이 디스패치되는데 "USER_INPUT" 과 차이점은
속성을 정의하지 않고 전개 문법을 이용하여 최신 state와 합쳐주면 된다.
(이전에 handleAllChange 함수에서 속성을 정의하였기 때문)


state를 초기화하고 싶은 경우는 가장 간단하다.

액션만 디스패치하고

dispatch({
  type: 'RESET',
})

initialInputState를 리턴하면 된다.

if (action.type === 'RESET') {
  return initialInputState;
}

input에 유효성 검증이 필요한 경우
액션을 디스패치하기 전 유효성 검증 로직을 추가하고 해당 결과를 payload로 전달하면 되는데
작성하다 보니 너무 길어져서 이 부분은 다음에 정리해야 겠다.


참고

[GitHub] To-Do List
useReducer Form Example
【한글자막】 React 완벽 가이드 with Redux, Next.js, TypeScript

profile
기록하는 습관 ✏️ 공유하고 싶은 정보들 🔎

4개의 댓글

comment-user-thumbnail
2023년 1월 27일

멋쪄용 👍 잘보고갑니다.

1개의 답글
comment-user-thumbnail
2023년 1월 27일

어려웠던 내용인데 잘 정리해주셔서 이해 되었어요! 감사합니다.

1개의 답글