React 상태 관리 도구 살펴보기

전원철
직방 기술 블로그
12 min readJun 21, 2021

--

React는 상태를 기반으로 UI를 제어하는 방식의 front-end 라이브러리 입니다. 그리고 이 상태(state)를 관리하는 것은 React 앱 구축에 중요한 부분 중 하나 입니다. 어떤 도구를 어떻게 사용해야 할지에 대해서 많은 고민과 노하우가 필요하며, 이에 따라 과거부터 지금까지 다양한 상태관리 도구들이 생겨났습니다. 이번 글에서는 각각의 상태관리 도구들에 대해서 살펴보려고 합니다.

📖 용어 사전

시작하기 전에 일반적으로 사용되는 용어들을 정리 하겠습니다. 아래의 분류는 정식 명칭은 아니지만, 각 상태의 성격을 구분하는데 용이합니다.

UI 상태

UI의 상호작용을 제어하는데 사용되는 상태입니다. (예: Toast, Modal, 지도 필터 등)

서버 응답 캐시 상태

빠른 액세스를 위해 클라이언트 측에 캐시하는 서버 응답에 대한 상태입니다 (예 : API 호출, 결과 저장)

Form 상태

Form의 다양한 상태 (예 : 우리집 내놓기 submit, input disable, validation 등). controlled & uncontrolled form state.

URL 상태

브라우저에서 관리하는 상태. screen(page)별로 고유하게 가지고 있는 상태. 페이지 새로고침해도 상태가 유지 (예 : query string, path parameter)

상태관리 도구들

React의 컴포넌트로 인해 재사용 가능하고 composable한 앱을 만들 수 있게 되었습니다. 각 컴포넌트에는 자체 로컬 state가 있지만, 앱이 더 복잡해지면서 컴포넌트간에 로직을 더 쉽게 공유 할 수있는 새로운 솔루션들이 등장했습니다.

  • 2014: Flux (alt 등..)
  • 2015: Redux
  • 2016: MobX
  • 2018: React.Context
  • 2019: React Query, SWR, Zustand, xState
  • 2020: Jotai, Recoil, Valtio
  • 2021: useSelectedContext

Redux

Redux 는 원래 2014 년 Facebook에서 처음 제안한 패턴 인 “ Flux Architecture “ 의 구현체로 만들어졌습니다 . Redux는 2015 년에 나왔고 Flux에서 영감을 받은 많은 라이브러리 중에서 가장 인기가 있었습니다. UI 상태와 서버 캐싱 상태를 모두 캡슐화 한 도구이자 라이브러리 에코 시스템(redux-xxx)입니다.

Redux는 가장 초기에 나왔지만 지금까지도 가장 인기 있는 라이브러리 입니다.

Mobx

Mobx는 Redux와 비교했을때 러닝커브는 낮은편이고, 보일러플레이트 코드가 거의 없어서 진입 장벽이 낮아, redux의 대항마로 주목받았습니다. proxy를 사용하여 불변성을 유지하기 위한 번거로운 작업도 없습니다. side effect 제어를 위한 별도의 툴(redux-saga 같은..)이 필요하지 않습니다. action이나 computed를 통해 역할 분리와 성능 최적화를 제어할 수 있습니다.

Mobx는 도메인 단위로 상태와 action, computed이 객체로 묶어 표현하기 때문에 OOP로 구조를 설계하기에 용이합니다. 그만큼 아키텍쳐 구성이 자유롭다는 특징이 있지만, 그만큼 유지보수가 어려운 코드가 되기도 쉽습니다.

React.Context

React는 v16.3 부터 React는 Context API를 통해 컴포넌트 사이에 로직을 공유할 수 있는 기능을 제공해 주었습니다. 이는 여러 수준의 중첩 된 컴포넌트간에 props로 값을 전달해야 하는 문제(prop-drilling)를 해결합니다.

React Context 자체는 상태 관리 도구가 아닙니다 . 그러나 useReducer와 같은 hook과 함께 사용하여 상태 관리 도구로 사용 할 수 있습니다.

그렇다 하더라도 context API를 상태관리 도구로 사용하기엔 한계가 있습니다. 큰 규모의 앱의 경우 React.Context 기반 상태 관리 솔루션이 과도한 rerendering 문제를 일으킬 수 있습니다. context의 값이 변경되면 해당 값을 소비(useContext, consumer)하는 모든 컴포넌트는 다시 렌더링 됩니다. 왜냐하면 Context API는 데이터 서브셋을 대상으로 변경을 감지하고 업데이트할 수 없기 때문입니다. 그리고 이러한 문제점은 Recoil이 나오게 된 계기 중의 하나가 되었습니다.

React 팀은 대규모 컨텍스트의 성능 문제를 방지하기 위한 useSelectedContext를 제안했습니다 . 이는 2019년 7월에 도입 되었으며, 2021년 1월 부터 진행이 시작 되었습니다. 이 hook를 사용하면 context의 "slice"을 선택 할 수 있고, 해당 slice가 변경 될 때만 다시 렌더링 할 수 있습니다. 유사한 접근 방식을 취하는 라이브러리인 useContextSelector 도 있습니다.

서버 응답 캐싱 상태 (SWR, React Query, apollo-client)

리엑트 초기에는 서버를 통해 받은 API 응답을 앱 전반에 걸쳐 사용하기 위해 캐싱하는것에 한정적이었습니다. 과거에는 API 응답 상태만 관리하는 방법이 없었기때문에, Redux나 mobx와 같은 라이브러리에 의존했습니다. 또한 Redux에서는 서버의 데이터를 효율적으로 관리하기 위해 정규화(normalizing) 개념을 사용합니다.

이후 React Hooks가 출시됨에 따라 비즈니스 로직을 hook을 공유하여 캡슐화하는 것이 훨씬 쉬워졌습니다. 이와 발맞춰서 hook를 이용해 서버 데이터 관리 문제를 해결하는 SWR, React Query, apollo-client 와 같은 도구들 등장했습니다.

이같은 도구들은 컴포넌트에서 API를 직접 fetch하지 않고, 각각 필요한 컴포넌트에서 query를 실행하며, 라이브러리가 실제 API fetch에 대한 최적화를 관리해줍니다.

“왜 서버 캐싱 상태를 위한 별도의 라이브러리가 필요한가?”라고 생각할 수 있습니다. 하지만 서버 캐싱 상태는 UI 상태와 다른 문제를 해결합니다. 아래는 위 라이브러리들이 처리하는 몇 가지 항목입니다.

  • 최신 데이터 유지를 위한 일정 간격의 데이터 폴링
  • 포커스에 따른 데이터 재 검증
  • 네트워크 복구에 따른 데이터 재 검증
  • 로컬 상태 변형(mutation)
  • retry logic
  • 목록 pagination과 스크롤 포지션 복구

상태 머신(xState)

xState는 유한상태머신을 구현한 라이브러리 입니다. 아래는 위키피디아의 정의입니다.

Finite State Machine(유한 상태 머신)은 유한한 개수의 상태를 가질 수 있는 추상 기계라고 할 수 있다. 이러한 기계는 한 번에 오로지 하나의 상태만을 가지게 되며, 현재 상태(Current State)란 임의의 주어진 시간의 상태를 칭한다. 이러한 기계는 어떠한 사건(Event)에 의해 한 상태에서 다른 상태로 변화할 수 있으며, 이를 전이(Transition)이라 한다. 현재 상태로부터 가능한 전이 상태와, 이러한 전이를 유발하는 조건들의 집합으로서 정의된다.

내용이 다소 어렵게 느껴질 수 있지만, jira의 workflow를 떠올려 보시면 쉽게 이해하실 수 있을겁니다.

예제 — 전구 상태 관리

Boolean을 사용하여 함수의 상태를 관리하는 데는 몇 가지 근본적인 문제가 있습니다. 첫 번째는 함수에 추가되는 모든 Boolean은 가능한 상태의 수를 2^n의 비율로 늘린다는 것입니다.

두 번째 문제는 이러한 상태 중 많은 부분이 “불가능 상태”이며 심지어 절대로 존재해서는 안된다는 것입니다. 아래 코드에서는 전구가 켜져있으며(isLit) 깨진 상태(isBroken) 가 가능합니다.

Boolean을 사용한 상태관리

이를 xState 기반의 상태 머신으로 변경해보면 표현 가능한 상태를 하나만 두고, 전구가 깨진 상태전구에 불이 들어온 상태를 함께 관리할 수 있습니다.

xState 기반의 상태관리

xState를 사용하면 전체 상태 머신을 온라인으로 시각화 할 수도 있습니다 .

유한 상태 머신(Finite State Machines) 및 Statecharts 는 컴퓨터 과학 개념이므로 React와 관련된 것은 아닙니다. 하지만 상태 머신은 데이터베이스, 전자 제품 등을 포함하여 여러 곳에서 잘 적용됩니다. React 생태계에서 상태 관리가 발전함에 따라 이런 오래된 아이디어가 현대의 상태 관리 문제를 해결할 수 있음을 깨달았습니다. 특히 상태 머신은 form 상태를 해결(예: 미작성 → 작성중 → 제출 완료)하는 데 가장 많이 사용됩니다.

유한 상태 머신을 사용하면 앱 또는 컴포넌트가 갖는 상태의 수가 한정되어 있습니다. 실제로 State Machine은 엣지 사례를 고려하고 정의해야 하는 상황에서 버그를 발견하는 데 도움이됩니다.

Zustand, Recoil, Jotai, Valtio

React 상태 관리를 위한 다양한 라이브러리가 존재하는 이유는 무엇일까요?

앱이 규모가 커질수록, UI가 구성이 복잡해 질수록, 복잡한 상태를 관리하기 위한 솔루션이 필요합니다. 좋은 사용자 경험을 위해서는 성능과 프레임 속도가 중요하므로 다시 렌더링하는 시기와 방법을 제어해야 합니다. 이와 같은 사용 사례로 인해 상태 관리 공간(store)에서 많은 연구가 이루어졌습니다.

Recoil

atoms (공유 상태)에서 selectors (순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만듭니다. 최소한의 상태 집합만 atoms에 저장하고, 다른 모든 파생되는 데이터는 selectors를 통해 계산합니다. (쓸모없는 상태의 보존을 방지 합니다.)

  • Atoms: 컴포넌트가 구독할 수 있는 상태의 단위
  • Selectors: atoms 값을 기반으로 파생데이터를 계산하고 변환

Valtio

proxy를 사용하여 mutation-style API를 제공합니다.

Jotai

recoil과 컨셉이 비슷합니다. selector과 비슷하게 “파생된 atom”을 통해 computed 값을 생성합니다. (Jotai는 일본어 狀態(상태) 를 그대로 발음 한 것이라고 합니다.)

Zustand

모듈화된 상태에 초점을 맞춘, 매우 가벼운 라이브러리입니다.

아래는 나와 맞는 라이브러리를 찾는 방법이라고 합니다.

NPM trend를 보면 recoilzustand의 사용량이 높은것을 알 수 있습니다.

URL 상태 (react-navigation, react-router)

URL 상태는 페이지(스크린) 단위로 상태를 저장하고 관리합니다. 즉 페이지 단위의 스토어라고 볼 수 있습니다.

직방 지도 페이지를 예를 들어 보면, 원룸 지도의 특정 역으로 이동했을때, 지도의 좌표정보(lat, lng, zoom)를 URL의 쿼리스트링으로 관리합니다. 페이지를 새로고침하거나, 이 URL을 다른사람에게 공유했을때 동일한 결과를 볼 수 있습니다.

url의 상태는 react-navigation, react-router등의 라이브러리를 통해 관리 할 수 있습니다.

직방은 react-navigation을 사용하지만 보통 navigation을 props로 접근 하고 있습니다. 이는 screen 컴포넌트에서 접근하기에는 용이하지만, 분리된 하위 컴포넌트에서 접근하기 위해선 props drilling이 불가피합니다.

분리된 컴포넌트에서 navigation 접근하기 위해 HOC(withNavigation)이나 hooks(useNavigation)를 사용할 수 있습니다. react-navigation v4 에서도 hooks를 사용할 수 있는 라이브러리를 제공합니다.

마무리

지금까지 여러 상태관리 도구들에 대해서 살펴보았습니다. 과거에는 하나의 상태관리 도구로 모든 상태를 제어하고자 했다면, 최근에는 각 목적에 맞는 관리 도구를 적절히 사용하는 것도 중요해 지고 있습니다. 직방 프론트엔드 팀도 최근 다양한 상태관리 도구들에 대한 조사하며 더 나은 구조를 위해 고민하고 있습니다. 추후에는 프로젝트 적용 사례에 대해서 공유드릴 수 있게 되기를 기대해봅니다. 긴 글 읽어주셔서 감사합니다!

[Ref]

--

--

전원철
직방 기술 블로그

평생개발을 꿈꾸는 직방 프론트엔드 개발자 입니다