프레임워크/React

[React] useReducer

은돌1113 2022. 3. 1. 11:51

컴포넌트의 state를 생성하고 관리하기 위해서 지금까지 useState Hook을 사용 해왔었는 데요.

React Hook에서는 상태 관리를 위한 Hook으로 useReducer도 제공하고 있습니다!

 

useReducer는 useState처럼 state를 생성하고 관리 할 수 있게 도와주는 도구입니다.

여러 개의 하위 값을 포함하는 복잡한 state를 다룰 경우 useReducer를 사용하면

훨씬 더 깔끔하게 코드를 작성 할 수 있습니다. 물론 유지보수에도 좋습니다!

 

useReducer는 Reducer, Dispatch, Action 이 세가지로 이루어져 있습니다.

1️⃣ Reducer

2️⃣ Dispatch

3️⃣ Action

 

한가지 상황을 예로 들어 설명 하겠습니다.

 

철수라는 아이가 계좌에서 만원을 출금하기 위해 은행에 방문 했습니다.

은행에서 철수 계좌의 있는 만원을 출금 할 경우 기록이 남습니다. 이때 거래 내역을 state라고 하겠습니다.

 

철수는 거래 내역이라는 state를 직접 수정하지 않고 은행이 철수의 요구대로 대신 state를 수정 해줍니다.

이때 은행은 Reducer라고 할 수 있습니다. Reducer는 state를 업데이트 해주는 역할을 합니다.

컴포넌트의 state를 변경하고 싶을 때는 Reducer에게 요구를 보내야 합니다.

마치 철수가 은행에 출금을 요청한 것처럼요

 

여기서 요구라는 행위는 Dispatch라고 할 수 있고,

"만원을 출금 해주세요"라는 요구사항은 Action이라고 할 수 있습니다.

여기서 요구사항에 따라 다른 Action을 요구(Dispatch) 할 수 있습니다.

(ex 예금 Action, 출금 Action, 계좌 생성 Action 등등)

Redux 라이브러리를 React Hook으로 사용 할 수 있는 느낌???


reducer의 가장 좋은 점은 전달 받은 action 대로만 state를 업데이트 해준다는 점입니다.

우리가 예상한 대로만 동작하기 때문에 실수를 어느정도 줄여 줄 수 있습니다.

 

실습 1️⃣

// ##### App.js #####

import React, { useState, useReducer } from "react";

// reducer : state를 업데이트 해주는 역할 (은행)
// dispatch : state 업데이트를 위한 요구
// action : 요구의 내용

const reducer = (state, action) => {
  // 첫번째 인자 : 현재 state, 두번째 인자 : action
  console.log("reducer가 일을 합니다.", state, action);

  switch (action.type) {
    case "deposit":
      return state + action.payload;
    case "withdraw":
      return state - action.payload;
    default:
      return state;
  }
};

function App() {
  const [number, setNumber] = useState(0);
  const [money, dispatch] = useReducer(reducer, 0);
  // 첫번째 요소 : 새로 만들어진 state
  // 두번째 요소 : useReducer가 만들어 준 dispatch 함수
  // 첫번째 인자 : 우리가 만들어 준 reducer
  // 두번째 인자 : 초기값

  return (
    <div>
      <h2>useReducer 은행에 오신 것을 환영합니다.</h2>
      <p>잔고: {money}원</p>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value))}
        step="1000"
      />
      <button
        onClick={() => {
          dispatch({ type: "deposit", payload: number });
        }}
      >
        예금
      </button>
      <button
        onClick={() => {
          dispatch({ type: "withdraw", payload: number });
        }}
      >
        출금
      </button>
    </div>
  );
}

export default App;

위 코드처럼 각각 type을 사용하는 방법이 있고

 

// ##### App.js #####

import React, { useState, useReducer } from "react";

const ACTION_TYPES = {
  deposit: "deposit",
  withdraw: "withdraw",
};

const reducer = (state, action) => {
  console.log("reducer가 일을 합니다.", state, action);

  switch (action.type) {
    case ACTION_TYPES.deposit:
      return state + action.payload;
    case ACTION_TYPES.withdraw:
      return state - action.payload;
    default:
      return state;
  }
};

function App() {
  const [number, setNumber] = useState(0);
  const [money, dispatch] = useReducer(reducer, 0);

  return (
    <div>
      <h2>useReducer 은행에 오신 것을 환영합니다.</h2>
      <p>잔고: {money}원</p>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value))}
        step="1000"
      />
      <button
        onClick={() => {
          dispatch({ type: ACTION_TYPES.deposit, payload: number });
        }}
      >
        예금
      </button>
      <button
        onClick={() => {
          dispatch({ type: ACTION_TYPES.withdraw, payload: number });
        }}
      >
        출금
      </button>
    </div>
  );
}

export default App;

위 코드처럼 ACTION_TYPES라는 객체를 생성하여 공통으로 관리 해주는 방법도 있습니다.

 

실습 2️⃣

// ##### App.js #####

import React, { useState, useReducer } from "react";
import Student from "../src/Student";

const initialState = {
  count: 1,
  students: [{ id: Date.now(), name: "saebom", isHere: false }],
};

const reducer = (state, action) => {
  switch (action.type) {
    case "add-student":
      const name = action.payload.name;
      const newStudent = {
        id: Date.now(),
        name,
        isHere: false,
      };

      return {
        count: state.count + 1,
        students: [...state.students, newStudent],
      };
    case "delete-student":
      const newStudents = state.students.filter((student) => {
        if (student.id !== action.payload.id) {
          return student;
        }
      });

      return {
        count: state.count - 1,
        students: [...newStudents],
      };
    case "mark-student":
      return {
        count: state.count,
        students: state.students.map((student) => {
          if (student.id === action.payload.id) {
            return { ...student, isHere: !student.isHere };
          }

          return student;
        }),
      };
    default:
      return state;
  }
};

function App() {
  const [name, setName] = useState("");
  const [studentInfo, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <h1>출석부</h1>
      <p>총 학생 수 : {studentInfo.count}</p>
      <input
        type="text"
        placeholder="이름을 입력 해주세요"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button
        onClick={() => {
          dispatch({ type: "add-student", payload: { name } });
        }}
      >
        추가
      </button>
      {studentInfo.students.map((student) => {
        return (
          <Student
            dispatch={dispatch}
            key={student.id}
            id={student.id}
            name={student.name}
            isHere={student.isHere}
          />
        );
      })}
    </div>
  );
}

export default App;
// ##### Student.js #####

import React from "react";

const Student = ({ id, name, dispatch, isHere }) => {
  return (
    <div>
      <span
        style={{
          textDecoration: isHere ? "line-through" : "none",
          color: isHere ? "gray" : "black",
        }}
        onClick={() => {
          dispatch({ type: "mark-student", payload: { id } });
        }}
      >
        {name}
      </span>
      <button
        onClick={() => {
          dispatch({ type: "delete-student", payload: { id } });
        }}
      >
        삭제
      </button>
    </div>
  );
};

export default Student;