프로그래밍/ReactJS

React 애플리케이션의 복잡한 상태 관리: useReducer 완벽 가이드

shimdh 2025. 10. 9. 09:51
728x90

React 애플리케이션을 개발하다 보면, 상태 관리의 중요성은 아무리 강조해도 지나치지 않습니다. 특히 애플리케이션의 복잡성이 커질수록 효율적인 상태 관리는 개발 경험과 유지보수성에 큰 영향을 미칩니다. 'useState' 훅이 간단한 상태 관리에 매우 유용하지만, 좀 더 복잡하고 예측 가능한 상태 로직을 다룰 때는 'useReducer' 훅이 훨씬 더 체계적인 해결책을 제공합니다. 이 글에서는 'useReducer'가 무엇인지, 왜 필요한지, 그리고 실제 예제를 통해 어떻게 활용할 수 있는지 자세히 알아보겠습니다.

useReducer란 무엇인가요?

'useReducer' 훅은 React에서 리듀서 함수를 통해 상태를 관리하는 데 사용되는 도구입니다. Redux와 같은 외부 라이브러리의 핵심 원칙을 따르지만, React 자체에서 제공하는 훅이기 때문에 별도의 라이브러리 설치 없이 사용할 수 있다는 큰 장점이 있습니다. 'useReducer'를 사용하면 상태 구조와 상태를 업데이트하는 로직을 한 곳에 정의할 수 있어서 복잡한 상태를 더 쉽게 이해하고 유지보수할 수 있습니다.

728x90

useReducer의 기본 구조

'useReducer' 훅은 다음과 같은 형태로 사용됩니다:

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

여기에 사용되는 각 요소들은 다음과 같은 의미를 가집니다:

  • state: 리듀서에 의해 관리되는 현재 상태 값입니다.
  • dispatch: 상태를 업데이트하기 위한 '액션'을 리듀서 함수로 보내는 데 사용되는 함수입니다. 이 'dispatch' 함수를 호출함으로써 상태 변경을 요청합니다.
  • reducerFunction: 현재 상태와 액션 객체를 인수로 받아 새로운 상태를 반환하는 '순수 함수'입니다. '순수 함수'라는 것은 동일한 입력에 대해 항상 동일한 출력을 반환하고, 외부 상태를 변경하지 않는다는 의미입니다.
  • initialState: 상태의 초기 값을 정의합니다.

useReducer 사용의 이점

'useReducer'를 사용해야 하는 이유는 여러 가지가 있습니다. 특히 애플리케이션의 규모가 커지거나 상태 로직이 복잡해질수록 그 이점은 더욱 명확해집니다.

  1. 중앙 집중식 로직: 모든 상태 업데이트 로직이 하나의 리듀서 함수 안에 모여 있습니다. 이는 애플리케이션의 어떤 부분에서든 액션이 발생했을 때 상태가 어떻게 변할지 예측하기 쉽게 만듭니다. 또한, 디버깅을 할 때 상태 변경의 흐름을 한눈에 파악할 수 있어 효율적입니다.
  2. 복잡한 상태 변경의 더 나은 처리: 여러 개의 관련 데이터 조각을 동시에 다루거나, 이전 상태를 기반으로 복잡한 계산을 통해 새로운 상태를 도출해야 할 때 'useReducer'는 코드를 훨씬 간결하고 명확하게 만들어 줍니다. 'useState'를 여러 번 사용해야 하는 상황에서 'useReducer'는 단일 상태 객체로 이를 통합하여 관리할 수 있게 해줍니다.
  3. 향상된 가독성 및 유지보수성: 'useReducer'는 상태 정의와 UI 렌더링 로직을 분리하는 데 도움을 줍니다. 컴포넌트 내부의 코드가 상태 업데이트 로직으로 복잡해지는 것을 막고, UI와 관련된 부분에만 집중할 수 있게 하여 컴포넌트를 더 깔끔하고 읽기 쉽게 만듭니다.

실용적인 예제: 쇼핑 카트 관리하기

'useReducer'의 개념을 더 명확하게 이해하기 위해 실제 시나리오를 통해 살펴보겠습니다. 여기서는 'useReducer'를 사용하여 간단한 쇼핑 카트의 항목을 추가하고 제거하는 기능을 구현해 보겠습니다.

1단계: 초기 상태 및 리듀서 함수 정의

먼저, 쇼핑 카트의 초기 상태를 설정하고, 카트 항목을 추가하거나 제거하는 로직을 담을 리듀서 함수를 정의합니다.

const initialState = {
    items: [],
    totalAmount: 0,
};

function cartReducer(state, action) {
    switch(action.type) {
        case 'ADD_ITEM':
            const updatedItems = [...state.items, action.item];
            const updatedTotalAmount = updatedItems.reduce((total, item) => total + item.price * item.quantity, 0);
            return { items: updatedItems, totalAmount: updatedTotalAmount };

        case 'REMOVE_ITEM':
            const filteredItems = state.items.filter(item => item.id !== action.id);
            const newTotalAmount = filteredItems.reduce((total, item) => total + item.price * item.quantity, 0);
            return { items: filteredItems, totalAmount: newTotalAmount };
        default:
            return state;
    }
}

이 코드에서 initialState는 카트에 담긴 상품들과 총액을 관리하는 객체입니다. cartReducer 함수는 action.type에 따라 ADD_ITEM 또는 REMOVE_ITEM 로직을 수행하여 새로운 상태를 반환합니다. 이 리듀서는 현재 상태와 액션을 기반으로 완전히 새로운 상태 객체를 생성하여 반환하는 '순수 함수'의 원칙을 잘 따르고 있습니다.

2단계: 컴포넌트에 useReducer 구현

이제 정의된 리듀서와 초기 상태를 실제 React 컴포넌트에 적용해 보겠습니다.

import React, { useReducer } from 'react';

function ShoppingCart() {
    const [cartState, dispatch] = useReducer(cartReducer, initialState);

    const addItemToCartHandler = (item) => {
        dispatch({ type: 'ADD_ITEM', item });
    };

    const removeItemFromCartHandler = (id) => {
        dispatch({ type: 'REMOVE_ITEM', id });
    };

    return (
        <div>
            <h2>장바구니</h2>
            <ul>
                {cartState.items.map(item => (
                    <li key={item.id}>
                        {item.name} - ${item.price} x {item.quantity}
                        <button onClick={() => removeItemFromCartHandler(item.id)}>제거</button>
                    </li>
                ))}
            </ul>
            <div>총액: ${cartState.totalAmount.toFixed(2)}</div>
        </div>
    );
}

export default ShoppingCart;

ShoppingCart 컴포넌트에서는 useReducer 훅을 사용하여 cartStatedispatch 함수를 가져옵니다. addItemToCartHandlerremoveItemFromCartHandler 함수는 각각 dispatch 함수를 호출하여 'ADD_ITEM' 또는 'REMOVE_ITEM' 액션을 리듀서로 보냅니다. 이 액션에 따라 리듀서는 상태를 업데이트하고, 업데이트된 cartState는 UI에 자동으로 반영됩니다.

이 예제를 통해 우리는 다음과 같은 점을 알 수 있습니다:

  • 초기 카트 구조는 항목 배열과 총액을 포함하도록 정의되었습니다.
  • 리듀서 함수는 항목 추가/제거 로직과 함께 총액을 자동으로 재계산하는 기능을 담당합니다.
  • ShoppingCart 컴포넌트는 dispatch() 함수를 호출하여 카트 내용을 수정하라는 요청을 보낼 뿐, 실제 상태 변경 로직은 리듀서에 위임하고 있습니다.

결론

'useReducer' 훅은 React 애플리케이션 내에서 복잡한 상태를 효과적으로 관리하는 데 매우 강력한 도구입니다. 모든 관련 로직을 한 곳에 중앙 집중화함으로써 디버깅을 더욱 쉽게 만들고, UI 렌더링과 비즈니스 로직 처리 간의 관심사를 분리하여 컴포넌트를 깔끔하게 유지하는 데 도움을 줍니다.

단순한 토글이나 카운터와 같은 간단한 상태 변화에는 'useState'가 충분하지만, 여러 데이터 조각이나 사용자 입력 간의 미묘한 상호 작용을 요구하는 더 정교한 애플리케이션을 구축할 때 'useReducer' 패턴을 숙달하는 것은 개발자로서 당신에게 큰 도움이 될 것입니다. 'useReducer'를 통해 더욱 견고하고 유지보수하기 쉬운 React 애플리케이션을 만들어나가세요!

728x90