본 글은 Lee Robinson의 포스팅 「Past, Present, and Future of React State Management」을 읽고 요약한 것으로, 오역이 있을 수 있습니다. (원문 바로가기)

 

2013년 5월 등장한 리액트는, 컴포넌트의 상태를 바탕으로 컴포넌트의 모양을 결정합니다. 이처럼 리액트는 "상태"라는 개념에 기초하는데요, 상태는 리액트 애플리케이션을 개발할 때 가장 어려운 부분 중 하나로 손꼽혀왔습니다.

 

 

용어 정리

본격적으로 글을 시작하기 전, 자주 쓰이는 용어들에 대한 정리부터 해볼까요? 이 용어들은 어떤 경우에는 다르게 불리기도 하지만 근본적인 의미는 동일합니다.

 

  • UI 상태(UI State) : 애플리케이션에서 인터랙티브한 부분을 제어하기 위해 사용하는 상태
    • ex - 다크모드 토글 버튼🌜🌞, 모달
  • 서버 캐시 상태(Server Cache State) : 서버의 상태를 의미하며, 빠른 접근을 위해 클라이언트 사이드에 캐싱해두는 상태
    • ex - API 요청을 하고, 결과를 저장한 뒤, 이렇게 저장한 것을 여러 곳에서 사용하는 경우
  • 폼 상태(Form State) : 인풋 등의 폼과 관련한 대한 다양한 상태
    • ex - loading, submitting, disabled 등
  • URL 상태(URL State) : 브라우저가 관리하는 상태
    • ex - 쿼리 파라미터에 저장하는 경우
  • State Machine : 시간 경과에 따른 명시적인 상태 모델

 

 

과거

리액트의 컴포넌트 모델을 통해, 재사용 하거나 합치는 것이 가능한 애플리케이션을 만들 수 있습니다. 컴포넌트마다 각자의 로컬 상태를 가지죠. 웹 앱이 복잡해질수록, 컴포넌트 간 로직을 공유하는 것이 더 쉬워지도록 돕는 새로운 솔루션들이 속속 등장했습니다.

 

상태 관리 타임라인

 

리덕스(Redux)

페이스북이 2014년에 제안한 Flux 아키텍처가 구현되어 탄생한 것이 바로 리덕스입니다. 리덕스는 여전히 인기가 많고 널리 쓰이고 있죠.

 

서버 캐시 상태(Server Caching State)

리액트 등장 초창기의 많은 상태 관리들은, API를 통해 데이터를 가져와서 애플리케이션에서 사용하기 위해 캐싱하는 것으로 요약될 수 있습니다. 커뮤니티는 리덕스와 같은 라이브러리에 많이 의존했는데요, 서버 캐시 상태만을 관리할 수 있는 쉬운 방법이 없었기 때문입니다.

 

훅(React Hooks)이 도입되면서, 훅에 로직을 캡슐화하는 것이 훨씬 쉬워졌습니다. 이에 힘입어 SWR과 React Query와 같은 라이브러리들이 서버 캐시 상태를 전문적으로 다루기 위해 탄생했습니다.

 

"왜 서버 캐시 상태만을 위한 별도의 라이브러리를 두는 건가요?"하고 궁금해하실 수 있습니다. 음, 왜냐면, 캐싱이 어렵기 때문이랄까요? 🤠 서버 캐시 상태는 UI 상태와는 다른 문제들과 관련이 있습니다. 서버 캐시 상태를 관리하는 라이브러리들이 우리 대신 담당해주는 일들은 다음과 같습니다:

  • 폴링(polling)
    • 위키백과에 따르면 폴링이란 "하나의 장치(또는 프로그램)가 충돌 회피 또는 동기화 처리 등을 목적으로 다른 장치(또는 프로그램)의 상태를 주기적으로 검사하여 일정한 조건을 만족할 때 송수신 등의 자료처리를 하는 방식"을 의미하는데, 쉽게 말해 원하는 응답을 얻을 때까지 서버에 반복적으로 확인 요청을 하는 것을 의미한다. (참고)
  • 포커스 시 재검증
  • 네트워크 복구 시 재검증
  • 로컬 뮤테이션 (옵티미스틱 UI)
    • 옵티미스틱 UI(Optimistic UI)란 특정 요청이 성공 할 것이라 가정을 하고 먼저 그 요청의 결과를 보여주는 방식을 의미한다. (참고) 이는 사용자 경험 향상을 위한 기법 중 하나로, 요청이 올 때까지 기다리는 대신 우선 UI를 변경해두고, success response가 오지 않을 경우에는 에러 처리 또는 롤백 처리를 한다.

 

Context

Context는 컴포넌트 간 로직 공유를 위해 리액트에서 직접 제공한 솔루션(first-party solution)입니다. Context 덕분에 prop-drilling 같은 일을 하지 않아도 되게 되었죠.

 

여러 개의 컴포넌트가 중첩되어 있을 때 상위 컴포넌트로부터 하위의 하위의 하위의 ... 컴포넌트들로 props를 전달하는 것을 prop-drilling이라고 하는데요, 이와 관련하여 Kent C. Dodds가 작성한 유명하고 유익하고 재미있는(?) 포스팅이 있으니 아직 못 보신 분들은 꼭 한번 읽어보시길 추천드립니다! (영어 / 한국어)

 

React Context 자체는 상태 관리를 하지 않지만, useReducer와 같은 훅과 엮어서 상태 관리 솔루션으로 만들 수 있습니다! ✨

이미지 출처: 원문

 

 

현재

2021년 현재, 리액트에는 상태 관리를 위한 다양~~~한 방법들이 존재합니다. 각각의 특수한 상황을 전문적으로 다루는 라이브러리들도 많이 등장했습니다.

 

State Machines

여기 어떤 switch문이 있습니다.

switch (state) {
  case state === 'loading':
    // show loading spinner
    break;
  case state === 'success':
    // show success message
    break;
  default:
  // show error message
}

state의 값이 case들 중 어느 하나에 해당된다면, 그에 대응하는 코드가 실행됩니다. 여기서 정의된 case들은 한정되어있죠. 이 switch문이 바로 state machine의 가장 단순한 예입니다.

 

Finite State MachinesStatecharts는 컴퓨터 과학의 기본 개념으로서 리액트만의 특별한 것이 아닙니다. 즉 State Machines는 데이터베이스, 전자제품, 자동차 등 어디에나 차용할 수 있죠. 리액트 생태계에서 상태 관리가 진화해가면서, 우리는 이러한 오래된 개념들이 현대의 상태 관리 이슈들을 해결할 수 있다는 것을 깨달았습니다. State Machines는 폼 상태를 다루는 가장 일반적인 솔루션입니다. Finite State Machine에선 애플리케이션 또는 컴포넌트가 갖출 수 있는 상태가 한정적입니다. 더 자세한 내용이 궁금하시면 xState를 한번 살펴보세요!

 

Zustand, Recoil, Jotai, Valtio, ... 😯

리액트 상태 관리를 위한 라이브러리들이 왜 이렇게 많이 존재하는 걸까요?? 이들은 아래와 같이 각각 특수한 분야에 대해 전문적으로 상태를 관리합니다.

  • Zustand모듈 상태를 전문적으로 다룹니다.
  • Recoildata-flow 그래프(말 그대로 데이터 흐름을 보여주는 그래프)를 사용하는 실험적인 라이브러리고요.
  • Jotai연산된 값(computed values)과 비동기 액션에 최적화 되어있습니다.
  • Valtiomutation-style API를 제공하기 위해 프록시를 사용합니다.

물론 복잡한 상태를 다뤄야 한다고 해서 반드시 써드 파티 라이브러리를 사용할 필요는 없습니다. 리액트와 자바스크립트를 이용해 스스로 어디까지 구현할 수 있는지 봐도 되죠. 명백하게 필요한 경우가 아니라면 앞서 언급한 라이브러리들을 사용하지 마세요.

 

불변 상태(Immutable State)

mutable state(변할 수 있는 상태)와 immutable state(불변 상태)에 대한 논란(?)이 있는데요, 정답은 없습니다. 만약 바닐라 자바스크립트로 상태 관리를 한다면 mutable state로 구현을 할 가능성이 큽니다. 반면 immutable state는 리액트를 통해 큰 인기를 얻게 되었습니다. (*원문에서 소개한 의견들을 종합해보면 mutable state, 즉 상태를 직접적으로 변경할 경우 상대적으로 편리할 수 있으나 그만큼 덜 안전하며, 버그가 발생할 위험이 더 크다고 함.)

 

Immer와 같은 솔루션들은, 우리가 mutable 코드를 작성해도 이를 immutable하게 실행합니다. 멋있죠?

import produce from "immer"

const nextState = produce(baseState, draft => {
    draft[1].done = true
    draft.push({title: "Tweet about it"})
})

출처: Immer

 

URL 상태(URL State)

아마존 같은 웹사이트를 짓는다고 치고, 사용자가 별점이 4개 이상인 리액트 서적를 검색하는 경우를 생각해봅시다. 이 때의 검색 조건(별점 4점 이상 + 리액트 서적)이 쿼리 파라미터에 담겨서 유지되고 브라우저에 의해 이러한 "URL 상태"가 관리되어야 하겠죠? 그렇게 하면 페이지를 새로고침하는 경우에도 동일한 상품 목록이 보여질 것입니다. 그리고 이 URL을 다른 이들에게 공유하고 그들이 그 URL을 클릭하여 우리 웹사이트에 들어왔을 때도 동일한 검색 결과가 보여지게 됩니다.

 

 

미래! 🔮

규모가 큰 애플리케이션에서 순수한 React Context + useReducer 기반의 상태 관리 솔루션을 사용하게 될 경우 불필요한 리렌더링으로 인한 이슈가 발생할 수도 있습니다. (불필요한 리렌더링이 일어나는지 여부는 가시적으로 확인할 수도 있고, 만약 가시적으로 확인이 되지 않는 경우라면 React Dev Tools를 통해 검토할 수 있습니다.)

 

리액트 팀은 Context 사용으로 인한 성능 이슈를 방지할 수 있는 useSelectedContext 훅을 제안했습니다. 이 훅은 Context의 "slice"(얇은 조각)를 선택해서, 그 slice가 변할 때만 리렌더링이 일어나도록 합니다. 이와 비슷한 역할을 하고 비슷한 이름을 가진 use-context-selector라는 라이브러리도 있습니다.

 

더 먼 미래(아주 먼 미래..?)에는 리액트가 어떤 컴포넌트가 리렌더링이 되어야 하는지 자동적으로 판별할 수 있을 예정이라고 하네요! (auto-memoization)

 

 

다양한 상태 관리 옵션들

폼 상태

경험 학습 욕구 프로젝트 규모/팀 규모 Solution
초급자 낮음 작음 useState
초급자 보통 작음 ~ 중간 폼 라이브러리 (Formik, Final Form)
초급자 보통 ~ 높음 테크 리드에게 물어보세요
중급자 낮음 작음 ~ 중간 폼 라이브러리 (Formik, Final Form)
상급자 보통 중간 State Machines
상급자 높음 중간 State Machines
상급자 높음 State Machines

 

UI 상태

경험 학습 욕구 프로젝트 규모/팀 규모 Solution
초급자 낮음 작음 useState
초급자 보통 작음 ~ 중간 useContext + useReducer
초급자 보통 ~ 높음 테크 리드에게 물어보세요
중급자 낮음 작음 ~ 중간 Redux Toolkit
상급자 보통 중간 useContext + useReducer
상급자 높음 중간 Jotai, Valtio
상급자 높음 Recoil (GraphQL을 쓰신다면 Relay)

 

서버 캐시 상태

달리 표로 나타낼 필요도 없이, 어떤 경우든 SWR과 React Query 둘 다 훌륭한 솔루션입니다. (GraphQL을 쓰신다면 Apollo는 이미 알고 계시겠죠?)



끝~

 

긴 글 읽어주셔서 감사합니다! 오역이 있을 경우 댓글이나 이메일로 알려주시면 감사하겠습니다!

복사했습니다!