icon
11장 : 프론트엔드

상태 관리 라이브러리 소개


지난 장에서는 React의 핵심 개념인 컴포넌트, JSX, state, props를 학습하며 선언형 UI를 구축하는 기초를 다졌습니다. 특히 useState 훅을 사용하여 컴포넌트 내부에서 데이터를 관리하고, props를 통해 부모에서 자식 컴포넌트로 데이터를 전달하는 방식을 이해했습니다.

하지만 애플리케이션의 규모가 커지고, 여러 컴포넌트가 동시에 동일한 데이터를 공유하거나 업데이트해야 하는 상황이 발생하면 문제가 복잡해지기 시작합니다. 예를 들어, 웹사이트의 상단 내비게이션 바에 표시되는 로그인 사용자 이름과, 프로필 페이지에 표시되는 사용자 이름이 동일한 데이터를 공유해야 할 때를 생각해봅시다.

이러한 상황에서 컴포넌트 간의 데이터 전달이 복잡해지는 문제를 "Prop Drilling" 이라고 합니다. 이는 데이터를 필요로 하지 않는 중간 컴포넌트들을 거쳐 여러 단계의 props를 전달해야 하는 상황을 의미하며, 코드를 읽고 유지보수하기 어렵게 만듭니다. 또한, 여러 컴포넌트에서 동일한 데이터를 독립적으로 관리하다 보면 데이터 불일치가 발생하기 쉽습니다.

이러한 문제들을 해결하고 애플리케이션 전체의 상태를 일관되고 예측 가능한 방식으로 관리하기 위해 상태 관리 라이브러리(State Management Library) 가 등장했습니다. 이번 장에서는 상태 관리 라이브러리의 필요성을 설명하고, 현대 프론트엔드 개발에서 널리 사용되는 주요 상태 관리 라이브러리들의 특징과 작동 방식을 간략하게 소개하겠습니다.


Prop Drilling 문제와 상태 관리의 필요성

React와 같은 컴포넌트 기반 프레임워크에서 데이터는 기본적으로 부모에서 자식으로 props를 통해 전달됩니다. 이 방식은 간단한 애플리케이션에서는 효과적이지만, 다음과 같은 경우에 한계에 부딪힙니다.

1. Prop Drilling (Prop 지옥)

  • 컴포넌트 트리의 깊이가 깊어질수록, 데이터를 필요로 하지 않는 중간 컴포넌트들까지도 props를 전달해야 하는 상황이 발생합니다.
  • 이는 코드를 지저분하게 만들고, 어떤 데이터가 어디로 전달되는지 추적하기 어렵게 만들며, 리팩토링 시에도 큰 부담이 됩니다.
// 예시: Prop Drilling
<App>
  <Header user={user}> // user 데이터를 Header에 전달
    <Navbar user={user}> // Navbar는 user를 필요로 하지만 Header는 필요 없음
      <Avatar user={user} /> // Avatar는 user를 필요로 함
    </Navbar>
  </Header>
  <Main />
</App>

2. 여러 컴포넌트 간의 상태 공유 및 동기화 어려움

  • 서로 다른 계층에 위치한 컴포넌트들이 동일한 상태를 공유하고 업데이트해야 할 때, propscallback 함수를 사용하는 방식으로는 구현이 복잡해지고 오류가 발생하기 쉽습니다.
  • 예를 들어, "장바구니 담기" 버튼이 있는 상품 상세 컴포넌트와 "총 상품 개수"를 표시하는 장바구니 아이콘 컴포넌트가 멀리 떨어져 있을 때, 두 컴포넌트가 동일한 장바구니 상태를 참조하고 업데이트해야 합니다.

이러한 문제들을 해결하기 위해, 애플리케이션 전역 또는 특정 도메인의 공유 상태(Shared State) 를 중앙에서 관리하는 메커니즘이 필요해졌습니다. 이것이 바로 상태 관리 라이브러리의 핵심 역할입니다.


상태 관리 라이브러리의 기본 개념

상태 관리 라이브러리들은 다양한 구현 방식을 가지고 있지만, 대부분 다음과 같은 공통적인 개념을 공유합니다.

  • 중앙 집중식 저장소 (Centralized Store): 애플리케이션의 모든 공유 상태를 한 곳에 모아둡니다.
  • 상태 변경 규칙: 상태는 정해진 규칙(Action, Mutation, Reducer 등)에 의해서만 변경될 수 있습니다. 이는 상태 변경의 예측 가능성을 높이고 디버깅을 용이하게 합니다.
  • 단방향 데이터 흐름: 사용자 인터페이스에서 발생한 이벤트는 특정 '액션'을 발생시키고, 이 액션은 상태 저장소의 상태를 변경합니다. 상태가 변경되면, 해당 상태를 구독(subscribe)하고 있는 모든 UI 컴포넌트들이 자동으로 업데이트됩니다.

주요 상태 관리 라이브러리 소개

프론트엔드 생태계에는 다양한 상태 관리 라이브러리들이 존재하며, 각각의 특징과 장단점을 가지고 있습니다. 여기서는 가장 널리 사용되거나 주목받는 라이브러리들을 소개합니다.

Redux

Redux는 React와 가장 오랫동안, 그리고 널리 함께 사용되어 온 상태 관리 라이브러리입니다. 예측 가능한 상태 컨테이너(Predictable State Container)를 지향하며, Flux 아키텍처 패턴을 기반으로 합니다.

  • 핵심 원칙
    1. 단일 진실 원천 (Single source of truth): 모든 애플리케이션 상태는 하나의 거대한 객체 트리로, 단일 저장소(Store)에 저장됩니다.
    2. 상태는 읽기 전용 (State is read-only): 상태를 직접 변경할 수 없으며, 반드시 액션(Action)을 디스패치(Dispatch)해야만 변경할 수 있습니다.
    3. 순수 함수로 변경 (Changes are made with pure functions): 상태를 변경하는 로직은 리듀서(Reducer)라는 순수 함수를 통해 이루어집니다. 리듀서는 이전 상태와 액션을 받아 새로운 상태를 반환합니다.
  • 장점
    • 예측 가능한 상태 변화: 엄격한 규칙으로 상태 변화를 예측하고 추적하기 용이합니다.
    • 강력한 디버깅 도구: Redux DevTools를 통해 상태 변화를 시간여행(Time-travel Debugging)하며 디버깅할 수 있습니다.
    • 확장성: 미들웨어(Middleware)를 통해 비동기 처리, 로깅 등 다양한 기능을 추가할 수 있습니다 (예: Redux Thunk, Redux Saga).
  • 단점
    • 상대적으로 복잡한 설정: 초기 설정과 보일러플레이트(boilerplate) 코드가 많아 학습 곡선이 높을 수 있습니다.
    • 작은 애플리케이션에는 과할 수 있음: 간단한 애플리케이션에는 오버헤드가 될 수 있습니다.

Redux 개념 흐름 View (React Component) -> Action Dispatch -> Middleware (Optional) -> Reducer -> Store (State Update) -> View (Re-render)

Recoil

Recoil은 Facebook(Meta)에서 React를 위해 개발한 상태 관리 라이브러리입니다. React의 "Hook" 개념과 유사하게 작동하여 React 개발자에게 친숙합니다.

  • 핵심 개념
    • Atom: Recoil 상태의 가장 작은 단위입니다. useState와 유사하게 컴포넌트에서 직접 구독하고 업데이트할 수 있습니다.
    • Selector: Atom 또는 다른 Selector의 값을 기반으로 계산된 파생된 상태(Derived State)를 정의합니다. 데이터를 변환하거나 필터링하는 데 사용됩니다.
  • 장점
    • React 친화적: React Hooks와 유사한 API를 제공하여 배우기 쉽습니다.
    • 간편한 설정: Redux에 비해 설정이 훨씬 간단하고 보일러플레이트가 적습니다.
    • 성능 최적화: React의 렌더링 최적화와 잘 통합되어 있습니다.
    • 병렬 모드 지원: React의 동시성 모드(Concurrent Mode)와 잘 작동하도록 설계되었습니다.
  • 단점
    • 아직 비교적 신규: Redux만큼 광범위하게 사용되지는 않아 커뮤니티 자료가 적을 수 있습니다 (점점 늘어나는 추세).
    • React 전용: 다른 프레임워크에서는 사용할 수 없습니다.

Zustand

Zustand는 간결하고 빠르며 확장성이 좋은 상태 관리 라이브러리입니다. React Hooks 기반의 가벼운 API를 제공합니다.

  • 특징
    • 간결한 API: Redux나 Context API보다 훨씬 적은 코드로 상태를 정의하고 사용할 수 있습니다.
    • 보일러플레이트 최소화: 상태 정의와 구독이 매우 직관적입니다.
    • 프록시 기반: 내부적으로 Proxy를 사용하여 상태 변경을 효율적으로 감지합니다.
    • React 외부에서도 사용 가능: React 외의 순수 JavaScript 환경에서도 사용할 수 있습니다.
  • 장점
    • 매우 쉽고 빠름: 학습 곡선이 낮고, 즉시 사용하기 좋습니다.
    • 작은 번들 크기: 애플리케이션의 전체 크기를 늘리지 않습니다.
    • 유연성: 다양한 패턴과 함께 사용할 수 있습니다.
  • 단점
    • 명확한 규칙 부재: Redux처럼 엄격한 규칙을 강제하지 않으므로, 팀 내에서 상태 관리 규칙을 명확히 정해야 합니다.

Jotai

Jotai는 Recoil과 유사하게 Atom 개념을 기반으로 하지만, 더 적은 API와 더 명시적인 패턴을 제공하는 경량의 상태 관리 라이브러리입니다. "상태는 원자적(atomic)이다"라는 철학을 따릅니다.

  • 특징
    • Atom 기반: 상태의 최소 단위를 Atom으로 정의합니다.
    • 간결하고 원자적: Recoil보다 더 간결한 API를 지향하며, 상태를 작은 단위로 쪼개는 데 집중합니다.
    • TypeScript 친화적: 타입 추론이 잘 되도록 설계되었습니다.
  • 장점
    • 매우 가벼움: 작은 번들 크기.
    • 유연성: 다양한 사용 사례에 적용 가능.
    • 뛰어난 성능: React의 렌더링 최적화와 잘 통합됨.

React Context API (+ useReducer)

React 자체적으로 제공하는 Context APIprops를 일일이 전달하지 않고도 컴포넌트 트리를 통해 데이터를 공유할 수 있는 방법을 제공합니다. useReducer 훅과 함께 사용하면 Redux와 유사한 패턴으로 복잡한 상태 관리가 가능합니다.

  • 장점
    • 별도 라이브러리 설치 불필요: React에 내장되어 있어 추가 의존성이 없습니다.
    • Props Drilling 해결: 중간 컴포넌트가 props를 넘겨받을 필요 없이 필요한 컴포넌트에서 직접 데이터를 구독할 수 있습니다.
  • 단점
    • 전역 상태의 변화: Context의 Provider 값이 변경되면 해당 Context를 구독하는 모든 하위 컴포넌트들이 재렌더링될 수 있어 성능 문제가 발생할 수 있습니다. (최적화 노력 필요)
    • 분리된 스토어 관리 어려움: 여러 Context를 사용할 경우 상태를 분리해서 관리하기 어려울 수 있습니다.
    • Redux DevTools와 같은 강력한 디버깅 도구 부재: 상태 변화 추적이 상대적으로 어렵습니다.

활용: 전역적으로 필요한 테마 설정, 인증 정보 등 자주 변경되지 않거나 컴포넌트 트리를 따라 깊게 전달되는 데이터에 적합합니다. 복잡한 비즈니스 로직이나 빈번한 상태 업데이트가 필요한 경우에는 Redux, Zustand, Recoil 같은 전용 라이브러리가 더 적합할 수 있습니다.


마무리하며

이번 장에서는 복잡한 프론트엔드 애플리케이션 개발에서 발생하는 Prop Drilling 문제와 컴포넌트 간의 상태 공유 어려움을 해결하기 위한 상태 관리 라이브러리의 필요성을 학습했습니다.

여러분은 애플리케이션의 공유 상태를 중앙 집중식으로 관리하고, 예측 가능한 방식으로 상태를 변경하며, UI 컴포넌트들을 효율적으로 업데이트하기 위한 상태 관리 라이브러리의 기본 개념을 이해했습니다. 또한, 가장 널리 사용되는 Redux부터 React 친화적인 Recoil, 간결한 Zustand, 그리고 경량의 Jotai, React 내장 기능인 Context API (+ useReducer) 까지 다양한 상태 관리 라이브러리들의 특징과 장단점을 간략하게 살펴보았습니다.

각 라이브러리는 특정 상황과 개발자의 선호도에 따라 장단점이 분명하므로, 프로젝트의 규모, 팀의 숙련도, 요구사항 등을 고려하여 적절한 라이브러리를 선택하는 것이 중요합니다.