원문

위 아티클의 내용을 번역 및 요약하고, 중간중간 부가 설명을 추가했습니다.

상태관리란?

: 앱에서 text input, 확인, 라디오 버튼 등 UI 요소의 상태를 관리

리액트 전역 상태의 기준

리액트 쿼리의 철학을 따른다면 서버 상태는 리액트 쿼리로 관리하고 클라이언트 상태는 해당 페이지/컴포넌트의 책임인지, 앱 전체의 책임인지에 따라 구분한다.
계정이나 설정 정보같이 서버와 주고받는 데이터, 화면과 무관한 데이터나 컴포넌트를 가로지르는 데이터(form 요소들)은 전역에서 관리하는 게 오히려 낫다.

최근 상태 관리의 움직임을 보면 예전보다 더 전역상태를 많이 지원하고 권한다. 컴포넌트 데이터 교환이 props로 인해 복잡해진다면 전역 상태로 관리해도 무방하다. 다만 기존에 전역 상태를 쓰지 말라고 한 것은 전역 상태를 컴포넌트들이 수정하기 시작하면 어디서 수정된 건지 관리가 힘들기 때문이다. 그래서 최근 상태관리들은 대부분 수정 추적을 할 수 있도록 설계되어있다. 마지막으로 페이지나 UI를 가로지르는 state는 큰 전역으로 해야 추적과 디버깅이 편하다.

결론

  1. 단순 UI 관련 state는 웬만해선 전역으로 넣지 않는다.
  2. 여러 UI에서 공유하는 state는 전역으로 넣는다
  3. View단에 절대 state를 선언하지 않는다

모던 상태관리 라이브러리의 주 4가지 기능

  1. State: 값이 바뀌면 해당 값을 쓰는 페이지나 컴포넌트가 자동 갱신
  2. Mutation: 상태를 바꾸는 작업
  3. Action: 어떤 작업의 결과가 나올때까지 상태가 바뀌지 않게 하고 결과값을 반영
  4. Memoization: 특정 결과값이 상태가 바뀔때만 재계산되게 하는 기능

서론

하나의 리액트 앱 전역에서 공유되는 상태를 어떻게 하면 잘 관리할 수 있을까? 리액트가 제공하는 가이드라인은 따로 없으므로 우리의 앱에 맞도록 잘 선택해야한다. 또, 왠만하면 로컬(밑부분)에서 시작해 필요한 만큼만 확장하자.

요즘의 전역 상태관리 라이브러리들은

각각 다른 접근법으로 동작한다. 또 각자 다른 문제점과 tradeoff를 가진다. 그렇기에 우리 앱에 가장 적절한 라이브러리를 선택하는 게 중요하다.

전역 상태 관리 라이브러리가 해결하려는 문제

첫째, 저장된 상태를 컴포넌트 트리 어디에서든 읽어올 수 있는가? 이 기능을 통해 상태를 메모리에 유지하고 prop drilling을 방지할 수 있다. 초기에 리액트 유저들은 이 문제를 해결하고자 Redux를 무분별하게 사용했다.

상태를 저장하는 두가지 방식

  1. 리액트 런타임 내부에 저장: 상태를 전파하기 위해 리액트 API인 useState, useRef 또는 useReducer를 활용한다. 이 때 신경 써야할 부분은 리렌더링을 최적화하는 것이다.
  2. 리액트 외부의 모듈 상태에 저장: 모듈 상태에는 싱글톤과 유사하게 상태를 저장할 수 있다. 또한 구독(컴포넌트가 저장된 '상태'를 지켜보는 것) 을 통해 상태가 변경될 때만 렌더링하도록 최적화할 수 있다. 다만 메모리 내부의 단일값(싱글톤)이므로 다른 subtree에서 사용되는 상태를 가질 수 없다.

둘째, 저장된 상태를 수정할 수 있는가?

셋째, 렌더링을 최적화하는 메커니즘을 제공하는가? 상태가 업데이트될 때 다시 렌더링할 시기를 감지하고 필요한 것만 다시 렌더링해야한다. 사용자는 셀렉터 함수로 저장된 상태를 구독함으로써 이 프로세스를 수동으로 최적화할 수 있다. 컴포넌트가 selector로 상태를 읽어 상태 변화가 일어났을 때만 리렌더링하는 방식이다. 이 경우 유저가 원하는 대로 구독을 제어할 수 있지만 수동인지라 오류가 발생하기 쉽다. 라이브러리가 자동으로 최적화하는 경우 사용이 편하다는 장점이 있다. Valtio라는 라이브러리는 Proxy를 사용해 상태의 업데이트와 컴포넌트가 다시 렌더링돼야하는 시기를 관리한다.

넷째, 메모리 사용을 최적화하는 메커니즘을 제공하는가? 리액트 라이프 사이클과 상태 저장을 엮으면 컴포넌트가 언마운트될 때 자동으로 가비지 컬렉션되도록 할 수 있다. 단일 전역 저장소 패턴의 Redux는 이를 개발자가 직접 관리해야한다. 가비지 컬렉션이 자동으로 되는 걸 막기 위해 데이터를 계속 참조하고 있을테니 말이다. 또 리액트 외부 모듈 상태에 저장하는 라이브러리를 쓸 때도, 상태가 컴포넌트들과 결합되지 않으므로 수동으로 관리해줘야한다.

상태관리 생태계의 역사

리액트가 처음 나왔을때, 리액트는 MVC 패턴에서 View 역할을 하고자 했다. 상태 관리 부재에 따른 불편함 때문에 페이스북의 Flux 패턴을 따와 상태 관리를 시작하게 되었다. 이 패턴은 단방향 데이터 흐름과 예측 가능한 업데이트에 적합했다. 그렇게 Flux 패턴을 사용한 대표적인 상태관리 라이브러리인 Redux가 사용되기 시작했다. 그러나…

Redux의 약점

  1. Redux는 트리의 어느 곳에서나 상태에 액세스할 수 있다. 적은 엔드 포인트를 통해서만 데이터를 가져오고, 상호 작용이 거의 없는 간단한 앱에서 쓰기에는 부담스럽다.

  2. 모든 상태를 한 단일 저장소에 저장하게 한다. 하향식 접근방식으로, 시간이 흐름에 따라 컴포넌트 트리의 맨 위에서 모든 상태를 빨아들이려 한다. (상태는 트리의 높은 위치에 있고 아래의 컴포넌트는 셀렉터로 필요한 상태를 끌어내린다.) 독립적으로 분리된 복잡한 컴포넌트를 작업하기 어렵다.

그런데 사실, 대부분의 웹 앱이 서버에서 가져온 데이터(서버 상태)를 CRUD하는 것들이라 서버의 캐시 문제를 해결하는게 더 우선이다. 그래서…

더 간단한 접근을 위한 Hooks, Context API의 등장

Redux 같이 무거운 추상화보다 훅과 기본 컨텍스트를 활용하는 방법이 뜨기 시작했다. (예시: useState, useReducer, useContext ) 간단한 앱의 경우 이것들을 사용하는게 더 나았다. 이 경우 리렌더링을 최적화하는게 중요했다.

원격 상태 관리 문제를 해결하기 위한 라이브러리들의 등장

CRUD 스타일인 대부분의 웹앱을 위해선 전용 원격 상태 관리 라이브러리와 결합된 로컬 상태가 유용하다. (React-query, SWR, Relay, 개선된 Redux 등) 예를 들면 react-query는 앱에서 서버 상태를 가져오기(fetch), 캐싱, 동기화 및 업데이트를 하게 도와주며 클라이언트 상태와 서버 상태를 명확히 구분하게 해준다.

주의할 점

State는 관련 컴포넌트들과 최대한 가까이 배치하는 게 좋다. 그래야 사이에 낀 컴포넌트들의 리렌더링과 부수효과를 줄일 수 있다.