[React] 요리조리 알아보는 useReducer

Minhyuk Song·2024년 3월 27일
0

React

목록 보기
2/3
post-thumbnail

[문제 상황]

컴포넌트는 간략하게 제 기능만 수행하는 게 제일 좋다고 생각한다. 근데 한 컴포넌트 내에서 너무 많은 상태를 관리하다보면 이 컴포넌트를 작성자가 아닌 사람이 봤을 때 좋은 컴포넌트라고 생각할까? (즉, 가독성이 똥망)
어쩔 수 없이 만들어진 자이언트 컴포넌트를 코드를 분리하는 방법, 가독성을 높이는 방법을 함께 찾아보자!

[해결 방법]

상태를 관리하기 위해서는 useState, useReducer가 있고, 전역으로 관리하고 싶다면 상태관리 라이브러리나 useContext를 사용하면 된다. 전역적으로 상태를 관리하면 코드의 의존성이 많아져서 불필요한 리렌더링을 야기할 수 있기 때문에 전역 상태 관리는 모든 곳에서 쓰이는 게 아니라면 웬만해서 줄이는 게 베스트라고 생각한다. 그래서 리액트에서 제공하는 훅 중에 하나인 useReducer를 사용해보자!

상태 관리 코드를 컴포넌트 외부에서 관리하기 위해 쓰는 훅, useReducer

useReducer 알아보기

첫 번째 관문인 useReducer의 구성요소를 아는 게 중요하다

  • state
  • dispatch
  • reducer
  • initialState

state는 상태를 담는 곳이며, dispatch는 상태 변화를 알려주는 발송기 (디스패치), reducer는 실제로 상태를 변환해주는 변환기, initialState는 상태의 초깃값을 의미한다.

재밌는 예제로 들어가보기

한 블로그에서 재미난 예시를 들면서 설명했는데 인용하자면,

  • state : 상태 이름 (컴포넌트에서 사용할 상태) > 빵(재료) 담는 접시
  • dispatch : 상태(state)를 변경 시 필요한 정보를 전달하는 '함수' > 주문서
  • reducer : dispatch를 확인해서 state를 변경해 주는 '함수' > 주방(공장)
  • initialState : state에 전달할 초기 값 > 빵(및 재료 등) 개수 설정
import React, { useReducer } from 'react';

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

function Baking() {
  // 3을 value 저장
  // 위에 선언했던 값을 변경하는 reducer 함수를 넣어주기!
  // reducer속 로직들을 실행시킬 명령어가 담겨있는 dispatch 선언
  const [value, dispatch] = useReducer(reducer, 3);

  const onPlus = () => {
    dispatch({ type: 'PLUS' });
  };

  const onMinus = () => {
    dispatch({ type: 'MINUS' });
  };

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

export default Baking;

각 구성요소의 역할을 레스토랑처럼 생각하고 나니깐 손님들이 주문하는 곳이 컴포넌트 안과 같고, 음식을 만드는 곳이 컴포넌트 밖이라고 생각하니 useReducer를 왜 쓰는지(상태를 다루는 코드를 분리) 알 것 같다.
주문서인 dispatch가 reducer에게 음식을 만들어달라는 게 보인다. 그리고 dispatch 안에 들어가는 객체를 액션 객체라고 한다! 그래서 reducer 인자에서 action가 들어가는 걸 자연스럽게 이해할 수 있다. 액션에 있는 타입에 따라 각각 다른 조리방법을 사용한다고 생각하니 재밌다...

[쉬운 예제로 적용해보기]

충분히 위 내용을 이해하셨더라면 [개선해보기]로 넘어가셔도 됩니다.
간단한 예제인 카운터를 만들어보자!

카운터

import { useReducer } from "react";

const counterReducer = (state, action) => {
  switch (action.type) {
    case "PLUS" : return state + 1
    case "MINUS" : return state - 1
  }
};

export const Counter = () => {
  const [state, dispatch] = useReducer(counterReducer, 1);

  const onPlus = () => {
    dispatch({ type: "PLUS" });
  };

  const onMinus = () => {
    dispatch({ type: "MINUS" });
  };

  return (
    <>
      <div>{state}</div>
      <button onClick={onPlus}>+</button>
      <button onClick={onMinus}>-</button>
    </>
  );
};

[개선해보기]

적당한 예시를 찾다가 발견한 코드! 참고 링크에 있는 코드를 한번 개선해보자!

// 📃 App.jsx
function App() {
  // ...생략
  const [cartItems, setCartItems] = useState([]);
  const [totalPrice, setTotalPrice] = useState(0);
  
  // 📌 장바구니 안에서 + 버튼을 누르면 수량을 1 증가시키는 함수
  const onAdd = id => {
    const updatedArr = cartItems.map((cur) => {
      if (cur.id === id) {
        cur.amount++;
      }
      return cur;
    });
	setCartItems(updatedArr);
    
    const newTotalPrice = updatedArr.reduce((acc, cur) => {
      return acc + (cur.amount * cur.price);
	}, 0);

  // 📌 장바구니 안에서 - 버튼을 누르면 수량을 1 감소시키는 함수
  const onRemove = id => {
    const updatedArr = cartItems.map((cur) => {
      if (cur.amount > 0 && cur.id === id) {
        cur.amount--;
      } else if (cur.amount === 0 && cur.id === id) { 
        cur.amount;
      }
      return cur;
    });

    const newTotalPrice = updatedArr.reduce((acc, cur) => {
      return acc + (cur.amount * cur.price);
    }, 0);
    setTotalPrice(newTotalPrice);

    const removedArr = updatedArr.filter((cur) => { 
      return cur.amount > 0;
    })
    setCartItems(removedArr);
  }
  
  // 📌 `+ 담기` 버튼을 누르면 장바구니에 선택한 아이템을 담아주는 함수
  const onSave = selectedItemData => {
    const newItemData = cartState.items.concat(selectedItemData);

    const mergedItemData = newItemData.reduce((acc, cur) => {
      const found = acc.find((item) => item.id === cur.id);
      if (found) {
        found.amount += Number(cur.amount);
      } else {
        acc.push({...cur});
      }
      return acc;
    }, []);

    setCartItems(mergedItemData);

    newItemData.forEach(item =>{
      setTotalPrice(totalPrice + item.amount * item.price);
    })
  };
}
export default App;

1단계

여러 개의 state를 한 곳으로 몰아주자! 그리고 useReducer를 구성 요소에 맞게 쓰자.

const initialState = {
  cartItems: [],
  totalPrice: 0
}

const cartReducer = () => {

}

export function Order() {
  // const [cartItems, setCartItems] = useState([]);
  // const [totalPrice, setTotalPrice] = useState(0);
  const [state, dispatch] = useReducer(cartReducer, initialState)
  ...

2단계

reducer에 쓸 수 있는 로직들을 옮겨보자

const cartReducer = (state, action) => {
  switch (action.type) {
    case "ADD": {
      const updatedArr = state.items.map((cur) => {
        if (cur.id === action.id) {
          return {
            ...cur,
            amount: cur.amount + 1,
          };
          // cur.amount++;
        }
        return cur;
      });
      const newTotalPrice = updatedArr.reduce(
        (acc, cur) => acc + cur.amount * cur.price,
        0
      );
      return {
        items: updatedArr,
        totalPrice: newTotalPrice,
      };
    }
    case "REMOVE": {
      ...
    }
    case "SAVE": {
      ...
    }
  }
};

export function Order() { 
 	... 
}

3단계

reducer에 전달될 수 있게 dispatch 코드를 작성하자

export function Order() {
  const [state, dispatch] = useReducer(cartReducer, initialState);
  ...
  
  const handleAddItem = id => {
    dispatch({
      type: 'added',
      id: id,
    })
  }
  ...
}
참고 링크

[주문앱 7탄] reducer로 리팩토링하기
[React] - useReducer()란 간단하고 쉽게 이해하기(예제코드, useReducer 사용예제)

profile
스크린을 넘어 유쾌한 경험을 드리는 프론트엔드 개발자가 되도록 노력하고 있습니다.

0개의 댓글