Jin's IT Story.

Context API와 useReducer, 라이브러리 없이 상태관리하는 법

| 2026-10-01
목차
    어두운 네이비 배경에 부드럽게 빛나는 하늘색 3D 큐브 아이콘. 리액트의 전역 상태 관리 데이터 저장소를 은유적으로 표현함.

    리액트로 웹 애플리케이션을 개발하다 보면 반드시 마주하는 난관이 있습니다. 바로 컴포넌트 간의 데이터 전달, 즉 상태 관리입니다. 프로젝트 규모가 커질수록 최상위 부모 컴포넌트에서 저 멀리 아래에 있는 증손자 컴포넌트까지 데이터를 전달하기 위해 수많은 컴포넌트를 거쳐야 하는 현상이 발생합니다. 이를 'Prop Drilling(프롭 드릴링)'이라고 부르며, 개발자를 지치게 만드는 대표적인 원인 중 하나입니다.

    이 문제를 해결하기 위해 흔히 Redux, Zustand, Recoil 같은 외부 상태 관리 라이브러리를 먼저 떠올립니다. 하지만 가벼운 토이 프로젝트나 중간 규모의 서비스에서 이러한 라이브러리를 도입하는 것은 때로 과도한 설정(Boilerplate)과 번들 크기 증가라는 부담을 줍니다. 리액트는 외부 도구 없이도 이 문제를 우아하게 해결할 수 있는 강력한 내장 도구인 리액트 Context API와 useReducer를 제공합니다. 이번 글에서는 외부 라이브러리 없이 깔끔하게 전역 상태 관리를 구현하는 방법을 쉽게 풀어보겠습니다.

    Props Drilling과 전역 상태 관리의 필요성

    우리가 리액트에서 데이터를 다룰 때 기본적으로 데이터는 위에서 아래로, 즉 부모에서 자식 방향으로만 흐릅니다. 예를 들어 사용자의 로그인 정보나 다크 모드 설정 같은 데이터는 앱 전체에서 공유되어야 합니다. 만약 가장 최상위 컴포넌트인 App에 이 상태를 두고, 아주 깊숙이 위치한 설정 페이지 컴포넌트에서 이를 사용해야 한다면 어떻게 될까요?

    이 중간에 위치한 수십 개의 컴포넌트들은 정작 본인에게는 필요도 없는 사용자 정보 프로퍼티를 아래로 전달하기 위해 코드로 품고 있어야 합니다. 이는 마치 택배 박스를 최종 목적지까지 보내기 위해 중간에 서 있는 모든 사람들이 손에서 손으로 계속 전달하는 비효율적인 상황과 같습니다. 중간 컴포넌트 중 하나라도 프로퍼티 이름을 잘못 전달하거나 빠뜨리면 앱 전체가 오작동하게 됩니다.

    이러한 비효율을 해결하기 위해 전역 상태 관리가 필요합니다. 중간 단계의 전달자들을 모두 건너뛰고, 데이터를 필요로 하는 컴포넌트가 직접 데이터를 가져다 쓸 수 있는 '중앙 보관소'를 만드는 것입니다. 리액트에서는 이를 내장 기능만으로 아주 간단하게 구축할 수 있습니다.

    Context API, 컴포넌트 트리를 가로지르는 지름길

    리액트 Context API는 컴포넌트 트리 전체에 데이터를 공급해 주는 일종의 '무선 와이파이 공유기'와 같습니다. 공유기를 거실에 설치해 두면, 집 안 어디에 있든 비밀번호만 입력하면 무선으로 인터넷에 접속할 수 있는 것과 같은 원리입니다. 중간에 벽(컴포넌트)이 가로막고 있어도 선을 복잡하게 연결할 필요가 없습니다.

    Context API를 사용하는 방법은 크게 세 단계로 나뉩니다.

    • 컨텍스트 생성 (createContext): 데이터를 담을 빈 저장 공간을 정의합니다.
    • 제공자 배치 (Provider): 데이터를 공유할 컴포넌트 범위의 최상단에 공급자를 감싸고 보낼 데이터를 지정합니다.
    • 데이터 소비 (useContext): 데이터를 직접 사용하려는 하위 컴포넌트에서 호출하여 꺼내 씁니다.

    이 방식을 사용하면 중간 컴포넌트들은 데이터의 존재조차 알 필요가 없으며, 오직 데이터를 생산하는 곳과 소비하는 곳만 직접 연결됩니다. 코드의 가독성이 획기적으로 올라가고 유지보수 역시 매우 간편해집니다.

    useReducer로 복잡한 상태 변화 정리하기

    단순한 텍스트나 토글 상태라면 Context API만으로도 충분합니다. 하지만 상태가 조금만 복잡해지면 상황이 달라집니다. 예를 들어 쇼핑몰 장바구니 기능을 구현할 때, 단순히 상품을 넣고 빼는 것뿐만 아니라 수량을 변경하고 할인 쿠폰을 적용하는 등 다양한 방식으로 상태가 변하게 됩니다. 이때 useState만 여러 개 사용하면 상태 변경 로직이 사방으로 흩어져 관리가 어려워집니다.

    이럴 때 유용하게 쓰이는 도구가 바로 useReducer입니다. useReducer는 상태 변경 로직을 컴포넌트 외부로 분리하여 한곳에서 모아 처리할 수 있게 돕는 리액트 훅입니다. 이는 은행 창구의 업무 방식에 비유할 수 있습니다. 고객(컴포넌트)이 돈을 직접 금고에서 빼는 것이 아니라, '출금 요청서(Action)'를 작성해서 행원(Reducer)에게 전달하면 행원이 금고의 돈(State)을 안전하게 변경하는 흐름입니다.

    이 방식을 적용하면 컴포넌트 내부에는 비즈니스 로직이 사라지고, 오직 '어떤 행동을 원하는지(Dispatch)'만 호출하면 되기 때문에 코드가 매우 단순해집니다. 또한 상태 변화의 흐름이 한눈에 보이기 때문에 버그를 추적하기도 훨씬 쉬워집니다.

    Context API와 useReducer의 결합, 그리고 주의할 점

    이제 이 두 개의 강력한 무기를 결합할 시간입니다. useReducer로 복잡한 상태 관리 로직을 정돈하고, 이렇게 만들어진 상태와 액션 전달 함수(dispatch)를 Context API의 Provider를 통해 전역으로 뿌려주는 것입니다. 이 패턴은 외부 전역 상태 관리 라이브러리인 Redux가 작동하는 핵심 원리와 완벽히 일치합니다.

    하지만 이 무적 같은 조합에도 한 가지 치명적인 약점이 있습니다. 바로 '불필요한 리렌더링' 문제입니다. Context의 Provider에 담긴 값이 아주 조금이라도 변경되면, 해당 컨텍스트를 구독(useContext 사용)하고 있는 모든 하위 컴포넌트가 강제로 다시 그려집니다. 만약 잦은 업데이트가 발생하는 대규모 데이터에 이 방식을 무턱대고 적용하면 앱의 성능이 크게 떨어질 수 있습니다.

    따라서 성능 최적화를 위해서는 상태 전용 컨텍스트와 디스패치 전용 컨텍스트를 분리하여 제공하는 것이 좋습니다. 또한 상태 변화가 밀리초 단위로 빈번하게 일어나는 복잡한 대형 애플리케이션이라면, 컨텍스트 분할에 힘을 쏟기보다 Zustand나 Recoil 같이 미세한 렌더링 제어를 지원하는 전문 라이브러리를 도입하는 편이 현명합니다.

    결론

    리액트 Context API와 useReducer는 별도의 라이브러리 설치 없이도 정교한 전역 상태 관리를 가능하게 해주는 훌륭한 파트너입니다. 외부 의존성을 낮춤으로써 프로젝트 용량을 가볍게 유지할 수 있고, 리액트 고유의 동작 방식을 더 깊이 이해하는 계기가 됩니다.

    새로운 도구를 무조건 도입하기 전에 리액트가 기본적으로 제공하는 내장 기능을 먼저 십분 활용해 보는 것을 권장합니다. 기술의 본질을 이해하고 나면, 향후 어떤 복잡한 상태 관리 라이브러리를 만나더라도 흔들리지 않는 튼튼한 개발 근육을 기를 수 있을 것입니다.