[React] useReducer

letthere·2021년 1월 18일
0

react note

목록 보기
10/17


이전에는 useState를 사용해서 상태를 업데이트 해주었는데, useReducer hook을 사용해서 상태 업데이트도 가능하다.

useStateuseReducer

리액트에서 컴포넌트의 상태 관리를 위해 기본적으로 가장 많이 쓰이는 hook은 useState 함수이다. 그러나 좀 더 복잡한 상태 관리가 필요한 리액트 컴포넌트에서는 useReducer hook 함수를 사용할 수 있다.
useState: 다음 상태를 직접 지정해주는 방식으로 상태 업데이트
useReducer: action이라는 객체를 기반으로 상태 업데이트, action 객체는 업데이트를 할 때 참조하는 객체

useReducer를 사용하면 상태 업데이트 로직을 컴포넌트 밖으로 분리할 수 있다.

const [state, dispatch] = useReducer(reducer, initialArg, init);
const [<상태 객체>, <dispatch 함수>] = useReducer(<reducer 함수>, <초기 상태>, <초기 함수>)

reducer 함수는 현재 상태(state) 객체와 행동(action) 객체를 파라미터로 받아서 새로운 상태 객체를 반환하는 함수이다.
dispatch 함수는 컴포넌트 내에서 상태 변경을 일으키기 위해서 사용되는데 파라미터로 reducer 함수에 넘길 action 객체를 받는다.
action 객체는 관행적으로 어떤 부류의 행동인지를 나타내는 type 속성과 해당 행동과 관련된 데이터를 담고 있다.
다시 말해, 컴포넌트에서 dispatch 함수에 행동(action)을 던지면, reducer 함수가 이 행동(action)에 따라서 상태(state)를 변경해준다.

reducer

reducer 는 현재 상태(state)와, 행동(action) 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수이다. reducer 에서 반환하는 상태는 곧 컴포넌트가 지닐 새로운 상태가 됩니다.

function reducer(state, action) {
  // 새로운 상태를 만드는 로직
  // const nextState = ...
  return nextState;
}

useReducer() hook 함수는 첫번째 파라미터로 넘어오는 reducer 함수를 통해 컴포넌트의 상태가 action에 따라 어떻게 변해야하는지 정의한다. action 은 업데이트를 위한 정보를 가지고 있다.

  • 액션의 예시
// 카운터에 1을 더하는 액션
{
  type: 'INCREMENT'
}
// 카운터에 1을 빼는 액션
{
  type: 'DECREMENT'
}
// input 값을 바꾸는 액션
{
  type: 'CHANGE_INPUT',
  key: 'email',
  value: 'tester@react.com'
}
// 새 할 일을 등록하는 액션
{
  type: 'ADD_TODO',
  todo: {
    id: 1,
    text: 'useReducer 배우기',
    done: false,
  }
}

Counter 컴포넌트 useReducer로 구현

카운터 컴포넌트에서 사용할 reducer 함수는 switch 분기문을 이용하면 이해하기 쉽게 작성 가능하다.

import React, { useReducer } from "react";

// 1) reducer 함수 만들기
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() {
  // 2) useReducer hook 사용
  const [number, dispatch] = useReducer(reducer, 0);

  const onIncrease = () => {
    dispatch({
      type: "INCREMENT",
    });
  };

  const onDecrease = () => {
    dispatch({
      type: "DECREMENT",
    });
  };
  
  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

export default Counter;

APP 컴포넌트 useReducer로 구현

import React, { useReducer, useRef, useMemo, useCallback } from "react";
import CreateUser from "./CreateUser";
import UserList from "./UserList";

// useMemo: 특정값이 바뀌었을 때만 특정 함수를 실행해서 연산하도록 처리

function countActiveUsers(users) {
  console.log("활성 사용자 수 세는 중");
  return users.filter((user) => user.active).length;
  // active가 true인 사용자 필터링해 수를 연산해서 가져옴
}

// 1) 앱 컴포넌트에서 사용할 초기상태를 컴포넌트 바깥에 선언해주기
const initialState = {
  inputs: {
    username: "",
    email: "",
  },
  users: [
    {
      id: 1,
      username: "su",
      email: "susu@gmail.com",
      active: true,
    },
    {
      id: 2,
      username: "liz",
      email: "lili@gmail.com",
      active: false,
    },
    {
      id: 3,
      username: "ro",
      email: "ro@gmail.com",
      active: false,
    },
  ],
};

// 2) reducer 함수의 틀 만들기
function reducer(state, action) {
  switch (action.type) {
    case "CHANGE_INPUT":
      return {
        ...state, // why? 새로운 상태 만들 때 불변성을 지켜주기 위해 state(users) 한번 복사
        inputs: {
          ...state.inputs, // 마찬가지로 기존 state.inputs 한번 복사
          [action.name]: action.value,
        },
      };
    case "CREATE_USER":
      return {
        inputs: initialState.inputs, // input 공백값 반환
        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),
      };
    default:
      return state;
  }
}

function App() {
  // 3) 내부에서 useReducer 함수 선언
  const [state, dispatch] = useReducer(reducer, initialState);
  // 6-1) useRef 사용 아이디값 부여
  const nextId = useRef(4);

  // 4) 비구조화 할당으로 users, inputs 내용 추출
  const { users } = state;
  const { username, email } = state.inputs;

  // 5) onChange 함수 구현
  const onChange = useCallback((e) => {
    const { name, value } = e.target;
    dispatch({ type: "CHANGE_INPUT", name, value });
  }, []);

  // 6) onCreate 구현
  const onCreate = useCallback(() => {
    dispatch({
      type: "CREATE_USER",
      user: {
        id: nextId.current,
        username,
        email,
      },
    });
    nextId.current += 1;
  }, [username, email]); // 함수에서 사용하는 상태 혹은 props, deps 배열에 포함

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

  // 8) onRemove
  const onRemove = useCallback((id) => {
    dispatch({
      type: "REMOVE_USER",
      id, // id값만 비교해주니까!
    });
  }, []);

  // 9) 활성사용자 수
  const count = useMemo(() => countActiveUsers(users), [users]);

  return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} />
      <div>활성 사용자 수 : {count} </div>
    </>
  );
}

export default App;

0개의 댓글