리액트와 상태 관리 라이브러리
상태 관리는 왜 필요한가?
먼저, 상태가 무엇인지 정의할 필요가 있습니다. 상태는 어떠한 의미를 지닌 값이며 시나리오에 따라 변경될 수 있는 값을 의미합니다.
UI: 모달의 열림과 닫힘을 나타내거나 바텀 시트와 같이 상호 작용이 가능한 모든 요소의 현재 값을 의미합니다.
URL: 브라우저에서 관리하는 상태로 해당 값에 따라 보여지는 것을 사용자의 라우팅에 따라 변경합니다.
폼(form): 현재 로딩중인지, 제출이 됐는지, 접근이 불가한지 등의 다양한 정보들을 나타냅니다.
서버에서 가져온 데이터: 클라이언트에서 서버로 요청하여 받아온 데이터도 상태로 볼 수 있습니다.
리액트의 상태 관리의 역사
Flux 패턴의 등장
순수 리액트에서의 전역 상태 관리라고 한다면 Context API를 떠올릴 것입니다. 하지만 Context API는 상태 관리가 아닌 상태 주입을 도와주는 역할입니다. 이러한 Context API는 16버전 부터 나왔으며 그 전까지 리액트 애플리케이션에서 딱히 이름을 널리 알린 상태관리 라이브러리는 없었습니다.
과거 Angular와 같은 프레임워크는 단방향 데이터 흐름 대신, 양방향 데이터 바인딩을 사용했습니다. 뷰가 모델을 변경할 수 있고 모델이 뷰를 변경할 수 있는 것입니다. 서로의 동작을 쉽게 접근할 수 있었지만, 코드의 양이 많아지고 시나리오가 복잡해질수록 데이터의 흐름을 파악하기 어려웠습니다. 이러한 상황속에서 Flux 패턴이 등장했습니다.
액션: 작업을 처리할 액션과 액션 발생시 포함할 데이터를 의미합니다. 이를 디스패쳐를 통해 보냅니다.
디스패처: 액션을 스토어에 보내는 역할을 합니다. 액션이 정의한 타입과 데이터 모두 스토어에 보냅니다.
스토어: 실제 상태에 따른 값과 변경할 수 있는 메서드를 가지고 있습니다. 액션 타입에 따라 상태를 어떻게 변경할지 정의되어 있습니다.
뷰: 리액트 컴포넌트에 해당하는 역할로 화면을 렌더링하는 역할을 합니다.

위의 코드를 본다면 각 타입에 맞게 상태 변경 함수가 정의되어 있는 것을 볼 수가 있습니다. increment
, decrement
, incrementByAmount
이러한 관리 방법으로 개발자들은 데이터 흐름을 모두 액션이라는 한 방향(단방향)으로 줄어드는 것으로 알 수 있었고 쉽게 코드를 이해할 수 있었습니다.
이러한 데이터에 대한 흐름과 방식을 단방향으로 채택한 것이 리액트 기반 Flux의 특징입니다.
그러나 이렇게 등장한 리덕스도 완벽한 해결책은 아니었습니다. 모든 액션에 타입을 선언해야 했으며, 액션을 수행할 creator
, dispatcher
, selector
등이 필요했습니다. 즉 하는 일에 비해 보일러플레이트가 만하는 비판의 목소리가 있었습니다.
Context API와 useContext
리액트가 나왔을 때 상태를 넘겨주기 위해서 prop을 통해 상태를 하위 컴포넌트에 전달하는 방식으로 사용하였습니다. 하지만 단순히 한두 단계를 건네는 방식이 아니라 깊어질수록 props
가 컴포넌트를 그대로 관통해 버리는 Prop drilling
현상이 존재하였습니다. 때문에 리액트 팀은 16.3에서 전역 상태를 하위 컴포넌트에 주입할 수 있는 Context API를 출시하였습니다.
그러나 Context API는 상태 관리를 위한 도구라기보다는 상태 주입을 도와주는 기능에 가깝습니다. 또한, 불필요한 렌더링을 막아주는 기능을 제공하지 않으므로 사용할 때 주의해야 합니다.
훅의 탄생, 그리고 React Query와 SWR
훅 API가 추가되면서 상태를 매우 손쉽게 재사용하기 시작하였습니다. 이러한 훅들이 등장하면서, 기존에는 볼 수 없었던 React Query와 SWR 같은 라이브러리도 등장하게 되었습니다.
두 라이브러리는 모두 외부에서 데이터를 불러오는 fetch를 관리하는데 특화된 라이브러리이지만, API 호출에 대한 상태를 관리하고 있기 때문에 HTTP 요청에 특화된 상태 관리 라이브러리라 볼 수 있습니다.
또한 동일한 키로 호출을 하게 될 경우 재조회하는 것이 아니라 상태를 통해 캐시되어 있는 값을 재활용하여 불필요한 호출을 방지할 수 있습니다. 때문에 이들 또한 상태 관리 라이브러리의 일종이라 볼 수 있습니다.
Recoil, Zustand, Jotai, Valtio에 이르기까지
SWR이나 React Query는 HTTP 요청에 대해서 사용할 수 있는 상태관리 라이브러리입니다. 뿐만 아니라 범용 적인 상태관리 라이브러리들이 나왔는데 이런 라이브러리들의 공통점은 바로 훅을 사용하여 작은 크기의 상태를 효율적으로 관리하는데에 있습니다. 이는 기존에 보일러 템플릿을 많이 작성해야했던 리덕스의 단점을 극복한 것으로 볼 수 있습니다.
리액트 훅으로 시작하는 상태 관리
가장 기본적인 방법: useState와 useReducer
useState가 나오면서 여러 컴포넌트에 걸쳐 쉽고 간단하게 인터페이스의 상태를 관리할 수 있게 되었습니다.
만약 커스텀훅이 존재하지 않았더라면 이런 훅과 관련된 로직들을 컴포넌트마다 구현을 했어야 합니다. 하지만 훅을 격리해 제공할 수 있다는 점을 이용하여 코드를 분리하여 어디서든 손쉽게 재사용 할 수 있다는 장점이 있습니다.
하지만 해당 훅의 한계는 지역 상태로 공유가 되지 않는다는 점입니다. 만약 같은 값을 보게하고 싶다면 상태를 컴포넌트 밖으로 한 단게 끌어올릴 수 있습니다.
이처럼 사우이 컴포넌트에서 useCounter를 사용하고 하위 컴포넌트에 props로 제공합니다. 이럴 경우 같은 상태 state를 보게됩니다.
지역 상태의 한계를 벗어나보자: useState의 상태를 바깥으로 분리하기
useState의 한계는 명확합니다. useState는 리액트에서 만든 클로저 배누에서 관리되어 지역 상태로 생성되기 때문에 해당 컴포넌트에서만 사용할 수 있다는 단점이 있습니다. 이러한 state 상태는 리액트 클로저가 아닌 다른 자바스크립트 문맥 어디에선가 초기화돼서 사용하게 될 경우 오류나 값을 정상적으로 사용할 수 없습니다.
물론 값은 변경이 가능합니다. 하지만 useState의 고유의 값이 변경되므로 리렌더링되어 UI상 다른 화면을 보여줄 수 없습니다.
때문에 다음과 같은 결론에 도달하게 됩니다.
최상단인 window나 global에 있어야 할 필요는 없지만 컴포넌트 외부 어딘가에 상태를 두고 여러 컴포넌트가 가팅 쓸 수 있어야 합니다.
외부에 있는 상태를 사용하는 컴포넌트가 상태의 변화를 알아챌 수 있어야 하며, 리렌더링을 발생시켜 매번 최신 상태 값을 보여주어야 합니다.
원시 값이 아닌 객체의 경우 감지하지 않는 값이 변한다 하더라도 리렌더링이 발생하면 안됩니다.
해당 함수를 구현하기 위해 Meta팀에서는 useSubscription 훅을 구현해두었습니다.
useState와 Context를 동시에 사용해보기
위와 같이 구독 형태로 만들경우 해당 스토어가 필요할 때마다 생성해야하며 의존적인 1:1 관계를 맺고 있으므로 스토어를 만들때마다 useStore와 같은 훅을 동일한 갯수로 생성해야합니다. 이를 만들었다고 해서 끝이나는게 아니라 최종적으로 어느 스토어에서 사용이 가능한지 가늠하려면 훅의 이름이나 스토어 이름에 의지해야 한다는 어려움도 있습니다. 이 문제를 해결하기 위한 좋은 방법은 바로 Context API입니다.
Context를 활용하여 스토어를 하위 컴포넌트에 주입한다면 컴포넌트는 주입된 스토어에 한해서만 접근할 수 있게 됩니다.
CounterStoreContext
를 활용하여 어떤 Context를 만들지 정의를 하였으며 이제 해당 내려주는 값을 내렺기 위해서는 Context에서 제공하는 스토어에 접근하여야하기 때문에 useContext를 사용하여 스토어에 접근할 수 있는 훅을 만들어야 합니다.
이를 이용하여 다음과 같이 사용할 수 있습니다.
위의 코드를 보면 각각 다른 count
값에 접근할 수 있음을 알 수 있습니다. Context와 Provider를 기반으로 store 값을 격리해서 처리할 수가 있었다. 이를 통해 컴포넌트는 해당 상태가 어느 스토어에서 온 상태인지 신경 쓰지 않아도 됩니다.
단지 자식 컴포넌트에 따라 보여질 Context를 잘 격리해서 사용하기만 하면 됩니다. 이는 자식 컴포넌트의 책임과 역할을 명시적으로 나눌 수 있어 코드 작성이 한결 용이해집니다.
상태 관리 라이브러리 Recoil, Jotai, Zustand 살펴보기
Recoil과 Jotai는 Context와 Provider, 그리고 훅을 기반으로 가능한 작은 상태를 효율적으로 관리하는데 초점을 맞추고 있습니다. 그리고 Zustand의 경우 리덕스와 같이 하나의 큰 스토어를 기반으로 상태를 관리합니다. 이는 Context 방식이 아니라 클로저를 기반으로 생성이 되며 해당 스토어를 구독하고 있는 컴포넌트에 전파하여 리렌더링을 알리는 방식으로 사용됩니다.
페이스북이 만든 상태 관리 라이브러리 Recoil
Recoil은 아직 정식으로 출시되지 안은 라이브러리이며 페이스북에서 만든 상태 관리 라이브러리 입니다. 치ㅚ소 상태 개념인 Atom을 리액트 생태계에 선보였습니다.
Recoil에서 영감을 받은 조금 더 유연한 Jotai
공식 홈페이지에도 나와있듯 Recoil의 atom 모델에 영감을 받아 만들어진 상태 관리 라이브러리입니다 Jotai는 Context의 문제점인 불필요한 렌더링 문제점을 해결하고자 설계돼 있으며, 메모이제이션이나 최적화를 거치지 않아도 리렌더링이 발생하지 않도록 설계되었습니다.
Recoil에서는 파생된 값을 만들기 위해서는 selector가 필요했지만, Jotai에서는 selector가 없어도 atom만으로 atom 값에서 또 다른 파생된 상태를 만들 수 있습니다. 이는 Recoil보다 간결하며, TypeScript로 작성되어 타입 지원이 우수합니다.
작고 빠르며 확장에도 유연한 Zustand
Zustand는 Redux에 영감을 받아 만들어졌습니다. 중앙 집중형 방식을 활용해 스토어 내부에 상태를 관리하고 있습니다.
또한 리덕스와 마찬가지로 미들웨어를 지원합니다. 데이터를 영구히 보존 가능한 persist, 리덕스와 함께 사용할 수 있는 리덕스 미들웨어등 다양한 방법들을 제공합니다.
정리
어떤 방식을 사용하더라도, 리렌더링을 유발하는 원리는 대부분 비슷합니다. 따라서 각 라이브러리별로 특징을 잘 파악해야하고 상황과 철학에 만느 상태관리 라이브러리를 사용하는 것이 좋습니다.
또한 다운로드 수가 많고, 이슈가 잘 관리되는 라이브러리를 선택하는 것이 중요합니다.