6장 - Manage status with useReducer

기운찬곰·2020년 12월 2일
0

React

목록 보기
5/5
post-thumbnail

What is Reducer?

이전에는 useState를 사용해서 상태를 관리하고 업데이트 해줬는데, 사실 한가지 방법이 더있다. 바로 useReducer를 사용하는 것이다. useReducer는 처음 사용해보면 사용법이 복잡하다고 느낄 수 있는데 크게 알아야 할 부분은 3가지로 action, dispatch, reducer이다.

리듀서 사용 전 알아야 할 개념

1. action(액션 객체)

useState가 setValue를 통해 상태를 직접 지정해 변경해줬다면 useReducer는 액션 객체를 기반으로 상태를 업데이트한다. 여기서 액션 객체란 업데이트할 때 참조하는 객체를 말한다. 액션 객체를 분리해서 작성할 수도 있고 dispatch 안에 직접 작성할 수도 있다.

2. dispatch

dispatch 안에 액션객체를 넣으면 상태변화를 발생시킨다. 다음과 같이 type을 이용해 어떤 업데이트를 진행할 건지 명시하고 업데이트할 때 diff 처럼 참조가 필요한 다른 값이 있다면 이 객체 안에 넣을 수도 있다.

// dispatch안에 액션객체를 넣는다!!
dispatch({ type: 'INCREMENT', diff: 4 })

3. reducer

하지만 어떤 액션객체인지를 판별하고 실제 업데이트해주는 부분은 아직 작성하지 않았다. 여기서 reducer라는 개념이 있는데, reducer란 상태를 업데이트하는 함수를 말한다.

// dispatch가 실행되면 reducer를 통해 상태를 업데이트해준다!!
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    defulat:
      return state;
  }
}

reducer는 현재 상태와 액션객체를 파라미터로 받아와 새로운 상태를 반환해주는 형태를 갖추고 있어야 한다. 보면 dispatch로 액션을 발생시키고 reducer에서 어떤 액션타입인지 switch문을 통해 판별한 다음 액션에 맞게 INCREMENT면 1증가, DECREMENT면 1감소를 해주는 작업을 하고 있다.

4. useReducer

reducer를 작성한 다음 최종적으로 useReducer를 사용해서 첫번째로 reducer 함수를, 두번째로 초기 상태값을 넣어주면 number라는 현재 상태값과 dispatch를 반환해주게 된다.

const [number, dispatch] = useReducer(reducer, 0);

useState vs. useReducer Hook

그렇다면 useState를 쓸까 useReducer를 쓸까? 🤔

이건 정해진 답이 없다. 다만 권장사항은 컴포넌트가 관리하는 값이 많지 않거나 복잡하지 않다면 useState를 쓰는 편이 좋고, 반대로 관리하는 값이 많거나 복잡한 경우에는 useReducer를 사용하는게 좋다고 한다.

useReducer를 사용하면 상태 업데이트로직을 컴포넌트 밖으로 분리가 가능하고 심지어 다른 파일에서 작성도 가능하기 때문이다.


Pratice 1. Counter State 관리

Counter는 구조가 간단하고 실습하기 좋은 예제이므로 먼저 Counter 예제를 활용해서 useReducer에 대해 실습해보도록 하겠다.

import React, { useReducer } from "react";

// 첫번째. 리듀서 작성!!
function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      throw new Error("Unhandled action");
  }
}

function Counter() {
  // 두번째. useReducer를 사용해 reducer와 초기값을 넘겨준다음 상태값과 dispatch를 넘겨받는다.
  const [number, dispatch] = useReducer(reducer, 0);
	
  // 세번째. dispatch를 이용해 액션객체를 생성해서 넘겨준다.
  const onIncrease = () => {
    dispatch({ type: "INCREMENT" });
  };

  const onDecrease = () => {
    dispatch({ type: "DECREMENT" });
  };

  return (
    <>
      <h1>{number}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </>
  );
}

export default Counter;

생각해보니 간단하지? 😲


Pratice 2. Input State 관리

마지막으로 저번시간에 배열 렌더링과 성능 최적화까지 마친 코드를 가지고 useReducer로 바꿔주는 작업을 해보겠다. 기존에 App.js에서 작성된 코드는 아깝겠지만 날려주고 새롭게 작성해보도록 하겠다.

1단계. initalState

useState를 사용할 때는 inputs 따로, users 따로 관리해줬는데 이번에는 통합해서 관리해보도록 하겠다.

// 초기상태를 갖는 객체를 만든다.
const initialState = {
  inputs: {
    username: "",
    email: "",
  },
  users: [
    {
      id: 1,
      username: "홍길동",
      email: "홍길동@gmail.com",
      active: true,
    },
    {
      id: 2,
      username: "이순신",
      email: "이순신@example.com",
      active: false,
    },
    {
      id: 3,
      username: "강감찬",
      email: "강감찬@example.com",
      active: false,
    },
  ],
};

2단계. 기본 구조

리듀서 함수 뼈대 작성과 useReducer를 사용해 리듀서 함수와 초기값을 넣어주고 현재 상태와 dispatch를 반환 받는다. 그리고 비구조할당을 통해 users와 username, email를 분리해준다.

// 리듀서 함수 뼈대 작성
function reducer(state, action) {
  return state;
}

function App() {
  // useReducer를 사용해 리듀서함수와 초기값을 넣고 현재상태와 dispatch를 반환받음
  const [state, dispatch] = useReducer(reducer, initialState);
  const nextId = useRef(4);
  const { users } = state;
  const { username, email } = state.inputs;
  
  (...생략...)
}

3단계. input 상태 변환

먼저 input 상태 변환을 해주는 onChange 함수부터 작성해보겠다.

// CHANGE_INPUT 액션타입에 해당하는 동작을 작성해서 새로운 상태를 반환해준다.
function reducer(state, action) {
  switch (action.type) {
    case "CHANGE_INPUT":
      return {
        ...state,
        inputs: {
          ...state.inputs,
          [action.name]: action.value,
        },
      };
    default:
      throw new Error("Unhandled action");
  }
}

// input 값이 바뀌게 될 때 실행
const onChange = useCallback((e) => {
  const { name, value } = e.target;
  // 디스패치 발생시킴(타입과 참조값을 넣어줌)
  dispatch({
    type: "CHANGE_INPUT",
    name,
    value,
  });
}, []);
  • onChange에서 dispatch를 이용해 액션객체를 만들어서 넘겨준다. 액션객체는 type과 추가적으로 참조할 변수를 넘겨줄 수 있다.
  • reducer에서는 해당 액션객체 type에 대한 로직을 설계해준다.
  • CHANGE_INPUT이라는 액션을 받으면 state안에 있는 inputs에 있는 username과 email 중 하나를 바꿔서 넘겨주도록 설계하면 된다.
  • 불변성은 유지해야 하기 때문에 spread 연산을 사용했다.

4단계. User 상태 변환

마찬가지로 유저 생성을 하는 onCreate, 유저 삭제를 하는 onRemove, 유저 수정을 하는 onToggle도 작성해주면 된다.

///////////////////////////////////////
// reducer 안에 작성
case "CREATE_USER":
  return {
    inputs: initialState.inputs,
    users: state.users.concat(action.user),
  };
case "TOGGLE_USER":
  return {
    ...state,
    users: state.users.map((user) =>
      user.id === action.id ? { ...user, active: !user.active } : user
    ),
  };
case "REMOVE_USER":
  return {
    ...state,
    users: state.users.filter((user) => user.id !== action.id),
  };

///////////////////////////////////////
// App함수 안에 작성
const onCreate = useCallback(() => {
  dispatch({
    type: "CREATE_USER",
    user: {
      id: nextId.current,
      username,
      email,
    },
  });
  nextId.current += 1;
}, [username, email]);

const onToggle = useCallback((id) => {
  dispatch({
    type: "TOGGLE_USER",
    id,
  });
}, []);

const onRemove = useCallback((id) => {
  dispatch({
    type: "REMOVE_USER",
    id,
  });
}, []);
profile
배움을 좋아합니다. 새로운 것을 좋아합니다.

0개의 댓글