프론트엔드 생태계에서 전역 상태 관리라는 개념은 리액트의 컴포넌트 기반 아키텍쳐의 한계에서 고안된 개념이다. 간단명료하게 전역 상태 관리라는 개념이 출현하게 된 과정을 알아보자.
SPA 기반 어플리케이션 개발
서버에서 새롭게 모든 페이지를 렌더링하는 MPA(Multiple Page Application)에서 필요한 콘텐츠만을 갱신하는 SPA(Single Page Application) 형태의 웹 어플리케이션 방식이 보편화됨에 따라, 클라이언트에서 상태를 관리할 필요성이 급증하게 되었다. 이에 SPA를 구현하는 Angular, Backbone 등의 프레임워크가 등장했다.
기존 프레임워크의 패턴
Angular 프레임워크에서는 SPA를 적용하는데 있어 MVC(Model-View-Controller) 패턴을 적용했다. 이 패턴에서는 모델(Model)이 데이터를 관리하고, 뷰(View)가 데이터를 표시하며, 컨트롤러(Controller)가 모델과 뷰를 연결하는 역할을 한다. 이런 MVC 패턴은 데이터의 흐름이 양방향적이라는 점에서 프로젝트의 규모가 커짐에 따라 데이터의 출처를 찾기 어렵게 만드는 것은 물론, 데이터 모델과 DOM을 동기화하는데 많은 비용이 소요되었다.
컴포넌트 기반 아키텍처, React의 등장
데이터의 양방향 전달로 인해 야기되는 문제를 해결하고자 컴포넌트 기반 아키텍쳐인 리액트가 탄생하게 된다. 리액트에서 컴포넌트는 UI를 기능적/성격적 유사성에 따라 조각 형태로 나누어 개발하는 형태를 의미하고, 기존 MVC 패턴과 다르게 데이터의 흐름이 단방향적이라는 점에서 데이터의 흐름을 추적하기 쉬워졌다.
Props drilling
리액트의 컴포넌트 기반 아키텍쳐를 사용하게 되면서, 개발자들의 다양한 목적에 따라 컴포넌트가 분리되었다. 이때 컴포넌트 안에는 또다른 자식 컴포넌트를 가질 수 있었는데, 컴포넌트가 중첩됨에 따라 부모 컴포넌트의 데이터를 props의 형태로 자식 컴포넌트를 거치며 데이터를 전달하게 되었다. 이는 props를 사용하지 않더라도 자식 컴포넌트가 사용한다면 props를 넘겨주어야 함을 의미했다. 또한 데이터의 단방향 메커니즘을 채택한 이상, 하나의 상태를 전달하기 위해 하위 컴포넌트들을 계속 거쳐 전달해야 하는 문제가 발생한 것이다.
Flux 아키텍처
이러한 상황을 해결하기 위해 Facebook에서 Flux 아키텍처를 고안했다. Action이 발생하면 이를 Dispatcher에 전달하고, Store에 있는 데이터를 변경한 뒤 View에 반영하는 형태로 이루어지게 된다. 이를 통해 React의 단방향 데이터 흐름을 그대로 살리면서, 중앙에 있는 하나의 store를 통해 전역적인 상태를 관리할 수 있게 되었다.
전역 상태 관리 라이브러리, Context API
이에 위 Flux 패턴을 고안한 Redux가 처음으로 출시되게 되고, 이후 Atomic 기반의 Recoil, Jotai 등 기존 라이브러리의 엣지 케이스를 해결하는 새로운 라이브러리가 출시하게 되었다. 이런 라이브러리들이 전역 상태 관리라고 불릴 수 있는 이유는, 상태와 상태의 변화를 함께 관리하기 때문이다.
그렇다면 리액트 16.3에 업데이트된 Context API는 무엇일까? 일부는 Context API를 전역 상태 관리라고 얘기하지만, 엄밀히 말하면 Context API 자체는 컴포넌트의 종속성 주입을 위해 고안된 장치일 뿐이다. 어플리케이션 상단에 Provider를 감싸 value를 하위 컴포넌트까지 전달하는 임무만을 맡지, 해당 value를 관리하는 것은 React의 Hook(ex. useState, useReducer)이 담당한다. 즉 Context API 자체를 React Hook과 함께 사용하는 것이 즉 전역 상태 관리를 할 수 있게 만드는 것이다.
그렇다면 전역 상태 관리 라이브러리와 Context API 둘 중 무엇을 쓰는게 더 좋냐 한다면… Context API는 규모가 작은 지역적인 부분에서 상태를 공유할 때 쓰는 것이 유리하고, 전역적으로 다른 위계에 있는 컴포넌트 간의 데이터 공유가 필요할 때는 전역 상태 관리 라이브러리를 쓰는 것이 옳을 것이다.
Context API는 리액트 내부 모듈인만큼, 지역적으로 쓴다면 언마운트가 될 때 자연스럽게 메모리에서 사라지게 되지만, 전역 상태 관리 라이브러리는 메모리에 계속 남아있게 된다. 이 상황에서는 Context API를 이용한 Compound Composition Pattern을 적극적으로 활용할 수 있다.
그럼 반대로 Context API를 전역 상태 관리를 위해 사용한다면 복수의 상태를 관리하기 위해 N개의 provider를 감싸게 되는 일명 Provider Hell 문제가 발생하게 되며, root 부분에 정의하게 되면 상태가 변경됨에 따라 하위 모든 컴포넌트의 리렌더링이 발생하게 된다. 물론 React.memo
, useMemo
, useCallback
등을 사용하여 최적화를 할 수 있지만, 이는 Context의 변경에 따라 최적화 코드를 계속해서 수정해야 하는 단점이 있다. 그러므로 이럴 때에는 중앙에서 상태와 관리를 모두 제어할 수 있으면서도 필요한 컴포넌트 레벨에서만 전역 상태를 호출할 수 있는 별도의 라이브러리를 사용하는 것이 옳을 것이다.