icon
8장 : 상태 관리 및 폼 처리

상태 관리 도구

이전 절에서 React의 기본적인 상태 관리 방법인 useStateprops를 이용한 데이터 전달, 그리고 서버 컴포넌트와 클라이언트 컴포넌트 간의 폼 처리에 대해 알아보았습니다. 하지만 애플리케이션의 규모가 커지고 복잡해질수록, 컴포넌트 트리가 깊어지거나 여러 컴포넌트가 동일한 데이터를 공유해야 하는 상황에서 useStateprops만으로는 한계에 부딪히게 됩니다. 흔히 발생하는 프롭스 드릴링(Prop Drilling) 문제나, 컴포넌트 간의 상태 공유 복잡성 등이 대표적입니다.

이러한 문제들을 해결하기 위해 React 생태계에서는 다양한 상태 관리 도구들이 발전해왔습니다. Next.js App Router 환경에서는 이 도구들을 클라이언트 컴포넌트 내에서 활용하여 전역 상태를 관리할 수 있습니다. 이 절에서는 React의 내장 Context API와 널리 사용되는 외부 상태 관리 라이브러리들에 대해 살펴보겠습니다.


React Context API

React Context APIprops를 명시적으로 전달하지 않고도 컴포넌트 트리를 가로질러 데이터를 공유할 수 있게 해주는 React의 내장 기능입니다. 로그인한 사용자 정보, 테마(다크 모드/라이트 모드), 언어 설정 등과 같이 전역적으로 접근해야 하는 데이터에 특히 유용합니다.

Context API의 작동 원리

  1. createContext: 전역 상태를 담을 Context 객체를 생성합니다.
  2. Provider: Context 객체가 제공하는 Provider 컴포넌트를 사용하여 Context 값을 설정합니다. 이 Provider로 감싸진 모든 자식 컴포넌트들은 해당 Context 값에 접근할 수 있습니다.
  3. useContext: 자식 컴포넌트에서 useContext 훅을 사용하여 Provider가 제공한 Context 값을 읽습니다.

장점

  • 프롭스 드릴링 해결: 여러 레벨을 거쳐 props를 전달할 필요 없이, 필요한 컴포넌트에서 직접 전역 상태에 접근할 수 있습니다.
  • React 내장: 별도의 라이브러리 설치 없이 React 자체 기능으로 사용 가능합니다.

단점

  • 모든 컨텍스트 소비자 리렌더링: Context 값이 변경되면 해당 Context를 사용하는 모든 컴포넌트가 리렌더링될 수 있어, 불필요한 리렌더링이 발생할 가능성이 있습니다. (최적화가 필요할 수 있음)
  • 복잡한 상태 로직 처리의 어려움: Redux처럼 복잡한 상태 전환 로직이나 미들웨어 등을 관리하는 데는 적합하지 않습니다.

실습: 테마(다크/라이트 모드) 변경 예제

간단한 테마 토글 기능을 Context API를 사용하여 구현해 보겠습니다.

  1. src/app/context-api/ThemeContext.tsx 파일 생성 (클라이언트 컴포넌트): Context를 정의하고 Provider 컴포넌트를 만듭니다.

    src/app/context-api/ThemeContext.tsx
    // src/app/context-api/ThemeContext.tsx
    "use client";
    
    import React, { createContext, useContext, useState, ReactNode } from 'react';
    
    type Theme = 'light' | 'dark';
    
    interface ThemeContextType {
      theme: Theme;
      toggleTheme: () => void;
    }
    
    // 1. Context 객체 생성 (기본값 설정)
    const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
    
    interface ThemeProviderProps {
      children: ReactNode;
    }
    
    // 2. Provider 컴포넌트 정의
    export function ThemeProvider({ children }: ThemeProviderProps) {
      const [theme, setTheme] = useState<Theme>('light');
    
      const toggleTheme = () => {
        setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
      };
    
      return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
          {children}
        </ThemeContext.Provider>
      );
    }
    
    // 3. useContext 훅을 간편하게 사용할 커스텀 훅
    export function useTheme() {
      const context = useContext(ThemeContext);
      if (context === undefined) {
        throw new Error('useTheme must be used within a ThemeProvider');
      }
      return context;
    }
  2. src/app/context-api/ThemeToggler.tsx 파일 생성 (클라이언트 컴포넌트): Context 값을 사용하는 컴포넌트입니다.

    src/app/context-api/ThemeToggler.tsx
    // src/app/context-api/ThemeToggler.tsx
    "use client";
    
    import React from 'react';
    import { useTheme } from './ThemeContext';
    
    export default function ThemeToggler() {
      const { theme, toggleTheme } = useTheme(); // useTheme 훅으로 Context 값 접근
    
      return (
        <button
          onClick={toggleTheme}
          style={{
            padding: '10px 20px',
            fontSize: '1em',
            backgroundColor: theme === 'dark' ? '#555' : '#eee',
            color: theme === 'dark' ? 'white' : '#333',
            border: `1px solid ${theme === 'dark' ? '#777' : '#ccc'}`,
            borderRadius: '5px',
            cursor: 'pointer',
            transition: 'all 0.3s ease',
          }}
        >
          현재 테마: {theme === 'light' ? '밝음 ☀️' : '어두움 🌙'}
        </button>
      );
    }
  3. src/app/context-api/page.tsx 파일 생성 (서버 컴포넌트): 페이지 컴포넌트에서 ThemeProvider로 클라이언트 컴포넌트들을 감싸줍니다.

    src/app/context-api/page.tsx
    // src/app/context-api/page.tsx
    
    import { ThemeProvider } from './ThemeContext'; // 클라이언트 Provider 임포트
    import ThemeToggler from './ThemeToggler';       // 클라이언트 토글러 임포트
    
    export default function ContextApiPage() {
      return (
        <ThemeProvider> {/* 클라이언트 컴포넌트들을 ThemeProvider로 감쌈 */}
          <div style={{
            padding: '20px',
            maxWidth: '600px',
            margin: '20px auto',
            border: '1px solid #ccc',
            borderRadius: '8px',
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            gap: '20px',
            backgroundColor: 'var(--bg-color, white)', // CSS 변수 사용
            color: 'var(--text-color, black)' // CSS 변수 사용
          }}>
            <style jsx>{`
              .dark-theme {
                --bg-color: #333;
                --text-color: #f0f0f0;
              }
              .light-theme {
                --bg-color: white;
                --text-color: black;
              }
            `}</style>
            <h1>Context API를 이용한 테마 변경</h1>
            <p>이 페이지는 클라이언트 컴포넌트에서 Context API를 사용하여 테마를 관리합니다.</p>
            <ThemeToggler />
            <p>이 텍스트는 테마에 따라 색상이 변경됩니다.</p>
          </div>
        </ThemeProvider>
      );
    }

    참고: 위 page.tsx에서 <style jsx>는 클라이언트에서 동작하는 컴포넌트의 스타일을 정의하는 예시입니다. 실제로는 전역 CSS 변수를 설정하거나 Tailwind CSS 등을 활용하여 테마를 적용할 수 있습니다.

실습 확인: http://localhost:3000/context-api로 접속하여 "현재 테마: 밝음" 버튼을 클릭해 보세요. 버튼과 텍스트의 색상이 토글되는 것을 확인할 수 있습니다.


외부 상태 관리 라이브러리

Context API는 간단한 전역 상태 관리에는 충분하지만, 복잡한 비즈니스 로직, 대규모 상태, 비동기 작업 관리 등에서는 한계가 있습니다. 이때는 더 강력한 기능을 제공하는 외부 라이브러리들을 고려할 수 있습니다. Next.js App Router에서는 이 라이브러리들을 클라이언트 컴포넌트 내에서 사용해야 합니다.

가장 널리 사용되는 몇 가지 라이브러리는 다음과 같습니다.

TanStack Query (React Query)

TanStack Query (이전 React Query)는 주로 서버 상태(Server State) 관리에 특화된 라이브러리입니다. fetch 등을 통해 서버에서 가져오는 비동기 데이터를 캐싱, 동기화, 업데이트하는 과정을 매우 효율적으로 관리해 줍니다. 데이터 로딩 상태(로딩 중, 에러, 성공), 재검증(refetching), 뮤테이션(mutation) 등을 자동으로 처리하여 개발자가 데이터 페칭 로직에 덜 신경 쓰도록 돕습니다.

주요 특징

  • 자동 캐싱 및 백그라운드 재검증: 오래된 데이터를 표시하면서 백그라운드에서 최신 데이터를 가져와 UI를 업데이트합니다.
  • 간편한 로딩/에러/성공 상태 관리: isLoading, isError, data와 같은 훅 반환 값으로 쉽게 UI를 구성할 수 있습니다.
  • 뮤테이션: 서버 데이터 변경(POST, PUT, DELETE)을 위한 기능을 제공하고, 변경 후 캐시를 자동으로 무효화하거나 업데이트할 수 있습니다.
  • 강력한 개발자 도구: React Query Devtools는 캐시 상태를 시각적으로 확인하고 디버깅하는 데 큰 도움이 됩니다.

언제 사용하면 좋을까요?

  • REST API, GraphQL 등 서버에서 데이터를 가져와야 할 때.
  • 복잡한 캐싱 전략이 필요할 때.
  • 로딩, 에러, 성공 상태를 일관되게 관리해야 할 때.
  • 데이터 변경 후 UI 자동 업데이트가 중요할 때.

Zustand / Jotai / Recoil

이 라이브러리들은 아톰(Atom) 기반 또는 플럭스(Flux) 패턴 기반의 경량 상태 관리 솔루션입니다. Context API보다 더 유연하고 성능 최적화에 유리하며, Redux처럼 복잡한 보일러플레이트 코드 없이 간결하게 전역 상태를 관리할 수 있습니다.

  • Zustand: 매우 작고 빠르며 간결합니다. 훅 기반의 API를 제공하여 배우기 쉽고 사용하기 편리합니다. Redux DevTools를 지원합니다.
  • Jotai: Zistand와 유사하게 작고 빠르며 아톰(Atom) 모델을 사용합니다. Recoil과 철학이 비슷하지만 더 작은 번들 크기를 가집니다.
  • Recoil: Facebook에서 개발한 라이브러리로, 아톰(Atom) 기반의 접근 방식을 사용하여 상태를 독립적인 단위로 관리합니다. React 18의 동시성 모드를 활용할 수 있습니다.

주요 특징

  • 최소한의 리렌더링: 상태의 특정 부분만 변경될 때 해당 부분을 사용하는 컴포넌트만 리렌더링되도록 설계되어 효율적입니다.
  • 간결한 API: useState와 유사한 훅 기반의 API를 제공하여 학습 곡선이 낮습니다.
  • 상태 공유 용이: 컴포넌트 트리의 깊이와 상관없이 어떤 컴포넌트에서든 전역 상태에 접근하고 업데이트할 수 있습니다.

언제 사용하면 좋을까요?

  • 클라이언트 측에서만 관리되는 전역 상태가 많을 때 (예: UI 테마, 장바구니, 필터 설정).
  • Context API의 리렌더링 성능이 문제가 될 때.
  • Redux의 복잡성이 부담스러울 때.

Redux Toolkit

Redux Toolkit은 Redux를 더 쉽게 사용하도록 만들어진 공식 권장 도구 세트입니다. 여전히 Flux 아키텍처와 예측 가능한 상태 관리라는 Redux의 핵심 개념을 따르지만, 보일러플레이트 코드를 대폭 줄이고 개발자 경험을 향상시켰습니다.

주요 특징

  • 정형화된 상태 관리: 예측 가능하고 디버깅하기 쉬운 상태 변경 로직을 강제합니다.
  • 미들웨어 지원: 비동기 작업(Redux Thunk, Redux Saga 등)이나 로깅, 분석 등 추가 기능을 미들웨어로 통합할 수 있습니다.
  • DevTools: Redux DevTools는 상태 변화를 시간 여행하며 디버깅할 수 있는 강력한 기능을 제공합니다.

언제 사용하면 좋을까요?

  • 상태 관리 로직이 매우 복잡하고 광범위한 대규모 애플리케이션.
  • 명확한 상태 변경 로직과 중앙 집중식 상태 관리가 필요한 경우.
  • Redux 생태계의 다양한 미들웨어 및 확장 도구를 활용하고 싶을 때.

App Router에서의 상태 관리 전략 선택

Next.js App Router 환경에서는 서버 컴포넌트와 클라이언트 컴포넌트의 역할 분담을 고려하여 상태 관리 도구를 선택하는 것이 중요합니다.

  • 서버 컴포넌트
    • 상태를 직접 가지지 않습니다 (useState, useEffect 사용 불가).
    • 데이터 페칭은 async/await를 사용하여 서버에서 직접 수행합니다.
    • 전역적인 데이터는 Layout, Page 컴포넌트의 propsContext를 사용하여 하위 서버/클라이언트 컴포넌트에 전달될 수 있습니다. (하지만 서버 컴포넌트 자체에서 Context를 직접 소비할 수는 없습니다.)
  • 클라이언트 컴포넌트
    • 사용자 상호작용에 따른 동적 상태 변화를 담당합니다.
    • useState, useContext를 사용하거나, TanStack Query, Zustand, Jotai, Redux Toolkit과 같은 외부 라이브러리를 사용하여 클라이언트 측 상태를 관리합니다.

결론적으로, 서버 컴포넌트는 초기 데이터 로딩과 UI의 정적 부분을 담당하고, 클라이언트 컴포넌트는 사용자 상호작용 및 동적인 클라이언트 측 상태를 담당하며 필요한 경우 전역 상태 관리 도구의 도움을 받는 것이 가장 효율적인 전략입니다.

어떤 상태 관리 도구를 선택할지는 프로젝트의 규모, 팀의 선호도, 요구사항의 복잡성 등에 따라 달라질 수 있습니다. 중요한 것은 각 도구의 장단점을 이해하고, 애플리케이션의 특성에 맞는 최적의 솔루션을 선택하는 것입니다.