티스토리 뷰

반응형

useReducer는 React의 훅 중 하나로, useState처럼 state를 생성하고 관리할 수 있게 해주는 도구이다. useReducer는 여러 개의 하위 값을 포함하는 복잡한 state를 다뤄야 할 때 사용한다. useState와 비교하며 useReducer를 왜 사용하는지부터 우선 알아보자.

 

useState VS. useReducer

useState는 간단한 상태 관리에 주로 사용된다. 하지만 상태가 복잡하고 여러 개의 하위 값이 포함된 경우, 상태를 업데이트하는 로직이 복잡해질 수 있다. 이때, useReducer를 사용하면 복잡한 상태 로직을 더 효율적으로 다룰 수 있다.

복잡한 상태란, 여러 하위 값을 포함하거나 여러 상태 간의 관계가 복잡한 경우를 의미한다. 예를 들어, 사용자의 프로필 정보가 여러 부분으로 나누어져 있고, 각 부분마다 별도의 상태가 필요한 경우가 있다. 아래 예시를 살펴보자.

복잡한 상태를 useState로 관리하는 경우

// useState
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');

// 업데이트 로직
const handleNameChange = (newName) => {
  setName(newName);
};

const handleAgeChange = (newAge) => {
  setAge(newAge);
};

const handleEmailChange = (newEmail) => {
  setEmail(newEmail);
};

 

handleNameChange, handleAgeChange, handleEmailChange 함수는 각각 하나의 상태를 업데이트 하는 역할을 한다. 이렇게 되면 각 필드에 대한 업데이트 로직이 분리되어 있어서 상태 관리 코드가 복잡해진다. 특히 상태의 수가 많아지면 코드의 양이 급격하게 증가할 수 있다.

복잡한 상태를 useReducer로 관리하는 경우

// useReducer
const profileReducer = (state, action) => {
  switch (action.type) {
    case 'SET_NAME':
      return { ...state, name: action.payload };
    case 'SET_AGE':
      return { ...state, age: action.payload };
    case 'SET_EMAIL':
      return { ...state, email: action.payload };
    default:
      return state;
  }
};

const [profile, dispatch] = useReducer(profileReducer, {
  name: '',
  age: 0,
  email: '',
});

 

반면에 useReducr를 사용하면 여러 가지 상태 업데이트 로직이 하나의 reducer 함수 안에 모여 있어 코드가 더 구조적이고 간결해진다. 이를 통해 관련된 업데이트 로직을 한 곳에서 효과적으로 관리할 수 있다.

 

 

useReducer 이해와 사용

useReducer의 주요 개념은 State, Reducer, Dispatch, Action으로 나눌 수 있다. 이해하기 쉽도록 카페 주문 시스템을 예시로 살펴보자.

  • state: 현재 주문 목록
  • Reducer: 카페 점원
  • Dispatch: 카페 손님의 요구 행위
  • Action: 요구 사항

 

손님의 요구 행위(요구 사항) ⇒ 카페 점원(기존 목록, 요구 사항) ⇒ 새로운 주문 목록

dispatch(action) ⇒ reducer(state, action) ⇒ return new state!

 

카페 손님은 주문 목록을 직접 업데이트 하지 않는다. 카페 점원이 요구 사항에 따라 주문 목록을 업데이트한다.

dispatcher는 state를 직접 업데이트하지 않는다. reducer가 action에 따라 state를 업데이트한다.

 

코드 구현

github: https://github.com/fromnowwon/useReducer-cafe-order-system

 

CafeOrderSystem.js

import React, { useReducer, useState } from "react";
import { orderReducer } from "./reducer";

const CafeOrderSystem = () => {
	// orderReducer라는 reducer를 사용하고, 초기 상태는 빈 배열로 설정하겠다는 의미
	// useReducer는 state와 dispatch를 담은 배열을 반환한다
	// 반환된 현재 state와 dispatch를 [orders, dispatch]에 담는 것
	const [orders, dispatch] = useReducer(orderReducer, []);
	// 업데이트된 주문을 저장하는 state
	const [newOrder, setNewOrder] = useState({ drink: "", size: "", price: "" });

	// 주문 추가하는 함수
	const addOrder = () => {
		// input 입력 값이 비어있지 않은 경우에만 추가
		if (
			newOrder.drink.trim() !== "" &&
			newOrder.size.trim() !== "" &&
			newOrder.price.trim() !== ""
		) {
			// 손님요구(요구사항): Dispatch(Action)
			dispatch({ type: "ADD_ORDER", ...newOrder });
			// 주문을 추가한 후에는 입력값 초기화
			setNewOrder({ drink: "", size: "", price: "" });
		}
	};

	// 주문 취소 함수
	const cancelOrder = (id) => {
		// 손님요구(요구사항): Dispatch(Action)
		dispatch({ type: "CANCEL_ORDER", id });
	};

	return (
		<div>
			<h1>Cafe Order System</h1>
			{/* 새로운 주문 정보 입력 */}
			<input
				type="text"
				placeholder="음료"
				value={newOrder.drink}
				onChange={(e) => setNewOrder({ ...newOrder, drink: e.target.value })}
			/>
			<input
				type="text"
				placeholder="사이즈"
				value={newOrder.size}
				onChange={(e) => setNewOrder({ ...newOrder, size: e.target.value })}
			/>
			<input
				type="number"
				placeholder="가격"
				step="10"
				value={newOrder.price}
				onChange={(e) => setNewOrder({ ...newOrder, price: e.target.value })}
			/>
			<button onClick={addOrder}>주문 추가</button>
			{/* 주문 목록 표시 */}
			<ul>
				{orders.map((order) => (
					<li key={order.id}>
						{order.drink} ({order.size}) - {order.price}원
						<button onClick={() => cancelOrder(order.id)}>주문 취소</button>
					</li>
				))}
			</ul>
		</div>
	);
};

export default CafeOrderSystem;

 

 

reducer.js

// Action Type 정의 (요구사항 종류)
const ACTION_TYPES = {
	ADD_ORDER: "ADD_ORDER",
	CANCEL_ORDER: "CANCEL_ORDER",
}

// 점원(현재주문목록, 요구사항)
export const orderReducer = (state, action) => {
	switch (action.type) {
		case ACTION_TYPES.ADD_ORDER:
			return [
				...state,
				{
					id: Date.now(),
					drink: action.drink,
					size: action.size,
					price: action.price,
				},
			];
		case ACTION_TYPES.CANCEL_ORDER:
			return state.filter((order) => order.id !== action.id);
		default:
			return state;
	}
};

 

CafeOrderSystem 컴포넌트에서는 여러 주문을 다룬다. 주문 추가와 주문 취소와 같이 다양한 액션(행위)에 대한 상태 업데이트 로직이 있다. useReducer는 이러한 복잡한 상태 로직을 더 구조적으로 관리할 수 있게 해 준다.

useReducer를 사용하면 상태 관리 로직이 한 군데에서 이뤄지기 때문에, 여러 컴포넌트에서 동일한 상태 및 상태 업데이트 로직을 공유할 수 있다.

 

만약 위 코드에서 useReducer 대신 useState를 사용하여 상태 관리를 구현하면 어떤 문제가 발생할까?

import React, { useState } from "react";

const CafeOrderSystem = () => {
  const [orders, setOrders] = useState([]);
  const [newOrder, setNewOrder] = useState({ drink: "", size: "", price: "" });

  const addOrder = () => {
    if (
      newOrder.drink.trim() !== "" &&
      newOrder.size.trim() !== "" &&
      newOrder.price.trim() !== ""
    ) {
      // 주문 추가 시 기존 상태를 변경하는 방식
      setOrders([...orders, newOrder]);
      setNewOrder({ drink: "", size: "", price: "" });
    }
  };

  const cancelOrder = (id) => {
    // 주문 취소 시 기존 상태를 변경하는 방식
    setOrders(orders.filter(order => order.id !== id));
  };

  return (
    <div>
      <h1>Cafe Order System</h1>
      {/* 입력 폼 */}
      {/* ... (이하 생략) ... */}
      <button onClick={addOrder}>주문 추가</button>
      {/* 주문 목록 표시 */}
      <ul>
        {orders.map((order) => (
          <li key={order.id}>
            {order.drink} ({order.size}) - {order.price}원
            <button onClick={() => cancelOrder(order.id)}>주문 취소</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default CafeOrderSystem;

 

구조가 간단한 경우 별다른 차이를 못 느낄 수 있다. 하지만 액션과 업데이트 로직이 분산되어 있기 때문에 복잡한 구조일 경우 코드가 길어지고 가독성이 감소할 수 있다. useReducer를 사용하면 이러한 로직을 하나의 리듀서 함수에서 관리할 수 있다.

 

결론: 상태 관리가 복잡한 경우 useState보다는 useReducer가 코드의 일관성과 유지보수성을 높을 수 있다.

반응형
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31