909 Devlog

[React] Reducer 본문

React/개념

[React] Reducer

구공구 2023. 9. 6. 00:41
728x90

👋 Reducer를 배우기 전에, state를 알고 계셔야 합니다


 

[React/개념] - [React] state, useState

 

[React] state, useState

👋 state, useState를 알아보기 전에, 간단한 예제를 한번 봅시다. const Example = () => { return ( 0 버튼 ); }; export default Example; 컴포넌트에 span과 button이 존재합니다. 버튼을 클릭하면 span에 있는 숫자를 1

gugonggu.tistory.com

제가 state에 대해 정리한 글이 있으니 한번 보고 오시는 걸 추천드립니다!

📌 Reducer란?


이전 글에서, 컴포넌트의 state를 useState를 사용해 관리했었습니다.

만약 우리가 To Do(할 일)✔️ 앱을 만든다고 생각 해 봅시다.

const [todos, setTodos] = useState([]);

useState를 사용해 ToDo 앱을 만들면, 할 일을 생성할 때, 수정할 때, 완료 표시를 할 때, 삭제할 때 등 여러 곳에서 설정자 함수인 setTodos를 불러와서 todos를 수정해야 할 것입니다.

 

새 기능을 추가하거나, 작성했던 코드를 수정하기 위해 코드 편집기의 여러 부분을 왔다 갔다 하면서 코드를 작성해야 하며,  state가 업데이트되는 경우를 한눈에 파악하기 어려워 개발 효율도 안 좋아질뿐더러, 매우 어지러울 것 같아요 :(

 

이런 state를 업데이트 하는 로직들을 한 곳에 모아 관리할 수 없을까요? 🤔

 

네, 그럴 때 Reducer를 사용합니다.

 

Reducer는 state를 관리하는 한 방법입니다. useState를 사용했던 것처럼, useReducer도 비슷하게 사용할 수 있습니다.

👨‍💻 Reducer 사용 방법


여기 버튼을 누르면 숫자가 1 증가하거나 1 감소되는 리액트 코드가 있습니다.

import React, { useState } from "react";

function Counter() {
    const [number, setNumber] = useState(0);

    const onIncrease = () => {
        setNumber((prevNumber) => prevNumber + 1);
    };

    const onDecrease = () => {
        setNumber((prevNumber) => prevNumber - 1);
    };

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

export default Counter;

코드 출처 : 벨로버트와 함께하는 모던 리액트

 

위의 예시코드는 코드가 짧아서 state가 어디서 어떻게 변하는지 한눈에 보이지만, 아까 말했듯이 코드가 길어지면 복잡해질 것입니다. 😅

 

useState를 통해 관리했던 state를 useReducer를 통해 관리해 보기 전에, Reducer의 기본 형태를 알아봅시다.

아래는 Reducer 함수의 기본 형태입니다.

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

Reducer는 현재 상태(state)와 액션 객체(action)를 매개변수로 받아와서, 새로운 상태(nextState)를 반환하는 함수입니다.

Reducer에서 반환하는 상태는 곧 컴포넌트가 가지게 될 새로운 상태가 됩니다.

 

state는 우리가 사용하던 그 state이고,

action은 업데이트를 위한 정보를 가지고 있는 객체입니다. 

// 숫자에 1을 증가시키기 위한 action 객체
{type: "INCREMENT"}

// 숫자에 1을 감소시키기 위한 action 객체
{type: "DECREMENT"}

// 예시 :
// 새 할 일을 등록하기 위한 action 객체
{
    type: "ADD_TODO",
    todo: {
    	id: 1,
        text: "reducer 배우기",
        done: false,
    }
}

액션 객체는 type을 지정하고, 대문자와 _(언더바)로 구성하는 관습이 있지만 꼭 따라하지 않아도 됩니다.

type은 사용자가 어떤 행동을 했는지 판별하고, 그 행동에 맞는 함수를 찾고 작동시키기 위해 필요합니다.

 

이제 useReducer를 사용해 봅시다! 😁

const [state, dispatch] = useReducer(reducer, initialState);

state에는 우리가 useState에 사용하던 것처럼, 데이터의 이름을 지정합니다.

dispatch는 우리가 조금 전에 봤던 액션 객체, 그러니까 사용자의 액션을 reducer에 전달해 주는 함수입니다.

첫 번째 파라미터인 reducer는 Reducer 함수의 기본 형태에서 봤던 함수의 이름을 작성해 주시면 되고, 

두 번째 파라미터인 initialState는 useState()의 괄호 안에 쓰던 초기값을 지정해 주시면 되겠습니다.

 

그럼 저는 이렇게 작성해야 겠군요

// Reducer 함수 작성
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

// ----------- 구분 -----------

// useState를 useReducer로 교체
const [number, setNumber] = useState(0);

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

// ----------- 구분 -----------


// state를 업데이트 하는 함수들 교체
const onIncrease = () => {
    setNumber((prevNumber) => prevNumber + 1);
};

const onDecrease = () => {
    setNumber((prevNumber) => prevNumber - 1);
};

                  ⬇️

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

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

이제 좀 감이 오시나요?

 

유저가 만약 숫자를 증가시키는 버튼을 눌렀다면

onIncrease 함수가 작동하고, 그 함수 내부의 dispatch가 작동해서 사용자의 액션을 Reducer 함수에 전달하게 됩니다.

Reducer 함수 내부에서 switch case 문을 통해 state(number)를 알맞게 변화시켜 준 뒤 컴포넌트가 다시 렌더링 됩니다.

 

최종 코드입니다.

import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

function Counter() {
  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;

코드 출처 : 벨로버트와 함께하는 모던 리액트

 

state 관리를 더 쉽고 편하게 하기 위해서 reducer를 사용했었죠?

그래서 reducer 함수를 아예 다른 파일로 분리하는 것도 가능합니다.

그럼 reducer 파일에는 export default 만 추가한 reducer 함수만 존재하는 거죠!

export default function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

 

이제 파일마다 관심사를 분리했으니 컴포넌트 로직을 더 쉽게 읽고 이해하실 수 있을 겁니다 :)

이제 이벤트 핸들러(on"..." 함수)에는 action만 전달하고

Reducer 파일로 와서 해당 action에 대한 return을 잘 작성하기만 하면 되는 거죠 😁

 

📌 Reducer도 좋은 점만 있는 것은 아닙니다.


좋은 점 😀

🎯 디버깅

useState에 버그가 있는 경우, state가 어디서 잘못 설정되었는지 알기 어려울 수 있습니다.

useReducer를 사용하면 reducer 함수에 console.log()를 찍어보며 어떤 action에서 버그가 발생했는지 비교적 쉽게 확인할 수 있습니다.

 

🎯 테스팅

reducer는 컴포넌트에 의존하지 않는 순수한 함수로써, 별도로 분리해서 테스트할 수 있습니다.

 

나쁜 점 😅

🎯 작성해야 할 코드가 늘어납니다.

일반적으로 useState를 사용하면 작성해야 할 코드가 줄어들지만

useReducer를 사용하면 reducer 함수와 action을 전달하는 부분을 모두 작성해야 해서 코드가 길어집니다.

 

🎯 간단한 로직일 때는 useState가 가독성이 더 좋습니다.

useState로 간단하게 state를 업데이트하는 경우가 reducer를 사용하는 것에 비해 코드 가독성이 좋습니다.

하지만 state 로직이 복잡해지면 reducer를 사용해 분리해서 깔끔하게 볼 수 있습니다.

 

📌 그럼 어쩔 때 useReducer를 사용할까요?


이 부분에 정해진 답은 없습니다.

위에서 좋은 점과 나쁜 점을 봤듯이 편할 때도 있고, 불편할 때도 있습니다.

 

컴포넌트에서 관리하는 state가 간단한 값 (숫자, 문자열, boolean) 하나라면 useState가 편해 보입니다.

하지만 2중 배열이라던가 배열 안에 객체가 있는 state들을 관리해야 한다면 useReducer가 좋은 선택이 될 수도 있겠지요.

 

둘 다 사용해 보시고, 좀 더 자신에게 맞고, 현재 코드에 맞는 편한 방법을 찾아봅시다! 💪

728x90

'React > 개념' 카테고리의 다른 글

[Redux] subscribe  (0) 2023.09.07
[React] state, useState  (1) 2023.09.03