리액트 상태관리 트렌드의 변화
들어가며
리액트를 사용해 프론트엔드 개발을 하다보면 상태관리에 대한 이야기를 수도 없이 듣게 됩니다. 이는 상태관리가 중요하기뿐만 아니라 그만큼 어렵고 정해진 가이드라인도 없기 때문입니다. 수많은 상태관리 라이브러리가 존재하는 것 자체가 어쩌면 상태관리에 정답은 없다는 것을 보여주는 것일 수도 있습니다.
도대체 상태관리가 무엇이길래 우리들을 힘들게 하는 걸까요? 지금부터 상태가 무엇인지부터 시작해 상태관리 트렌드의 변화를 간단히 훑어보며 각 라이브러리들이 어떤 배경에서 탄생했는지 알아보겠습니다.
상태관리의 역사
⏱️ 상태의 정의와 초기 상태관리 방법
리액트에서는 상태(state)를 시간에 따라 변경되는 데이터, 렌더링에 영향을 끼치는 정보를 가진 객체라고 설명합니다. 리액트는 처음 출시될 때 Model View Controller 중 View
를 담당하는 라이브러리로 발표되었기에 상태를 구성하거나 관리하는 방법에 대한 솔루션은 제공하지 않았습니다.
초창기 개발자들은 리액트를 BackboneJS, AngularJS 같은 다른 MVC 기반 프레임워크의 View단에 올려놓는 형태로 사용하거나, 상태와 비즈니스 로직을 Container 컴포넌트에 몰아넣고 Presenter(view)는 데이터를 props로 받아 뿌려주기만 하는 Container-Presenter 패턴으로 개발해 나갔습니다. 하지만 애플리케이션이 커지면서 위 아키텍쳐들의 문제점이 나타나기 시작합니다.
양방향 데이터 이동이 가능한 MVC 구조는 관리해야 할 상태와 해당 상태를 핸들링하는 컴포넌트가 많아지면서 기하급수적으로 복잡해졌으며, 컴포넌트 구조가 복잡해지면서 멀리 떨어진 컴포넌트가 같은 데이터를 필요로하는 경우, 해당 상태를 공통 부모 컴포넌트까지 끌어올리고 다시 하단으로 보내기 위해 해당 상태가 필요하지 않은 중간 컴포넌트들에게도 props로 전달해야하는 prop drilling 문제가 발생했습니다.
🚰 Flux 아키텍쳐와 Redux의 등장
이러한 문제점들을 인식한 Facebook이 단방향 데이터 흐름과 상태를 통합 관리하는 Store 등의 개념이 담겨있는 Flux 아키텍쳐
를 발표합니다.
Flux 아키텍쳐에 대한 자세한 내용은 아주 설명이 잘 되어있는 영상으로 대체하니 꼭 시청하시길 바랍니다.
그리고 이 Flux 아키텍쳐를 기반으로 등장한 상태관리 라이브러리가 Redux
입니다. 중앙집중화 된 단일 Store에서의 상태관리 덕분에 추적 및 디버깅이 용이하며 뛰어난 확장성을 가진 리덕스는 센세이션을 일으켰지만 Action, Dispatcher, Reducer 등 장황한 문법이 필요하고 제대로 활용하기 위해서는 리덕스와 함께 사용되는 수많은 미들웨어들을 익혀야 했기에 입문 난이도는 하늘로 치솟게 되고 많은 입문자들의 PTSD도 일으키게 됩니다.
👁️🗨️ MobX와 Context API
리덕스에 호되게 당한 개발자들을 위해 Observer-Observable 패턴과 데코레이터 사용 등 보다 쉬운 상태관리를 내세운 MobX
등 다른 상태관리 라이브러리도 등장하기 시작합니다. 이들은 대부분 컴포넌트 외부에 Store를 두고 컴포넌트에서 필요한 데이터를 참조하는 형태였는데 리액트에서는 이것과 유사한 개념으로 Provider라는 공통 조상을 만들어 하위 컴포넌트에게 데이터를 제공해 사용할 수 있게 하는 Context API
를 발표하였고 곧이어 리액트계의 혁명이라 할 수 있는 hooks
가 공개되면서 리액트 상태관리에도 대격변이 일어나게 됩니다.
⛓️ Hooks와 Context API의 한계
useContext와 useState, useReducer를 함께 사용하면 다른 라이브러리 없이 쉽게 상태관리가 가능해 많은 개발자들이 이용했지만 이 역시 완벽하진 않았습니다. 대표적으로 Context의 값이 변경될 때 마다 하위 컴포넌트가 모두 리렌더링되는 문제로 이를 최소화하기 위해 작은 Context Provider를 여러 개 만들게 되는데 Provider를 일일이 만드는 것은 리덕스 못지 않게 번거로웠습니다. 또한 멀리 떨어진 컴포넌트에 Context를 제공해야 할 경우 Provider를 위로 끌어올리게 되는데 이렇게 되면 Root 컴포넌트에 Provider들이 몰리게 되는 Provider Hell 현상도 생기게 됩니다.
🔍 특정 종류의 상태에 집중한 라이브러리 등장
여러가지 방법들이 제시되었지만 모두 무언가가 조금씩 아쉬운 상황에서 기존과 다른 시각에서 출발 한 라이브러리가 등장하게 됩니다. 지금까지의 상태관리 라이브러리는 로컬 상태와 전역 상태, 즉 상태의 범위(scope)
에 집중했습니다. 하지만 상태의 성질
에 초점을 맞추자 새로운 패러다임이 나타나게 됩니다. 다양한 종류의 상태가 있지만 결국 모든 상태는 다음과 같이 2개의 큰 그룹으로 분류할 수 있습니다.
클라이언트 상태(UI 상태)
- App의 테마나 모달의 open 여부, TextField에 입력되어 있는 값 등은 클라이언트가 제어권을 가지고 있으며 동기적으로 동작하여 항상 최신 값을 가집니다.서버 상태(비동기 상태)
- 게시판의 글 목록이나 사용자 정보 등은 서버(DB)에서 관리되며 비동기적으로 동작하여 클라이언트에게 보여지는 상태는 조회 한 시점의 Snapshot이기에 최신 값과 다를 수 있습니다.
이처럼 완전히 상반되는 성질을 가진 두 가지를 상태 라는 하나의 보따리에 담으려고 했기에 수많은 문제가 발생했던 것입니다. 특히 이전까지 서버 상태를 효율적으로 관리하기 위해서는 수많은 난관을 극복해야 했는데 대표적으로 다음과 같은 것들입니다.
- 데이터 캐싱
- 중복 요청 방지
- 오류 시 재시도 로직
- 주기적인 폴링
- 포커스 시 재갱신
- 변경 요청(mutation) 이후 동기화
- loading 및 error 등 비동기 라이프사이클 관리
- 페이지네이션, 무한 스크롤 등등..
하나하나가 쉽지 않은데 서버 상태가 늘어날 수록 해당 작업을 반복해야 했고 그럴수록 복잡성은 늘어만 갔습니다. 이를 해결하기 위해 서버 상태 관리에 특화된 라이브러리가 등장하는데 대표적으로 SWR
과 React Query
입니다.
리액트 쿼리(현재는 tanstack query)는 스스로를 공식 홈페이지에 대문짝만하게 강조해놨듯이 비동기(서버) 상태 관리자
라고 소개합니다. 이들은 위에 나열한 서버 상태 관리를 어렵게 만드는 요소들을 쉽게 컨트롤할 수 있는 인터페이스를 제공했고 개발자들은 코드 몇 줄이면 난관들을 극복할 수 있게 되었습니다. > React query에 관하여 Pancake님의 글에 자세한 설명이 있으니 여기서는 간단히 넘어가겠습니다.
🧩 상향식, Atomic 접근법의 부상
리덕스를 포함한 이전까지 나온 상태관리 솔루션은 이른바 하향식(top down) 접근법이었습니다.필요한 상태를 모두 컴포넌트 트리 상단의 Store가 가지고 있으면서 하위 컴포넌트에서 필요한 상태를 가져다 쓰는 방식이었죠. 하지만 리액트 hooks의 등장과 함께 작은 상태나 로직을 가진 hook들을 조합해가며 복잡한 상태까지 관리해나가는 방식이 유용하다는 것이 알려졌고 hooks와 유사한 방식으로 사용할 수 있으면서 리렌더링 등 최적화 이슈를 안고있는 useContext의 문제까지 해결한 라이브러리들이 새롭게 등장합니다.
Recoil
과 Jotai
로 대표되는 이들은 atomic 모델과 상향식 접근법을 채용해 상태를 업데이트와 구독이 가능한 atom이라는 단위로 관리하고 atom이 업데이트 될 때마다 그 atom을 구독 한 컴포넌트가 리렌더링이 되게 하는 방식을 가집니다. atom을 조합해가며 간단한 상태부터 복잡한 상태까지 다룰 수 있으며 atom 의존성에 따른 기본적인 렌더링 최적화가 되어 있기 때문에 추가적인 메모이제이션 작업도 필요없어집니다.
🚂 아직 끝나지 않았다
이렇게 쉴새없이 달려왔지만 웹 프론트엔드 그리고 리액트는 계속 진화하고 있습니다. 대표적으로 hooks의 등장 이후 가장, 아니 어쩌면 리액트에서의 가장 큰 변화일지도 모르는 React Server Component(RSC)가 기다리고 있습니다. RSC가 정식 릴리즈 되고 점점 자리 잡게 된다면 그에 따라 상태 관리를 포함한 아키텍쳐의 많은 변화가 있을 것입니다. 어쩌면 RSC가 외면당하고 리액트의 아성이 흔들리면서 Svelte같은 다른 기술로 눈을 돌리게 될 지도 모르죠.
마치며...?
프론트엔드에서 상태가 무엇인지, 그리고 상태관리의 어려움과 그것을 해결하기 위해 어떤 수많은 과정들을 거쳐 현재에 이르게 됐는지 그 흐름을 살펴봤습니다. 최근 가장 일반적인 형태의 상태관리 방법은 서버 상태 관리 라이브러리와 전역 상태 관리 라이브러리를 하나씩 선택해 적용하는 것으로 보입니다.
현재 프로젝트에서는 React Query와 Jotai를 함께 사용하고 있는데 그래도 글에 코드 한 줄 없는 건 아쉬워서 처음 해당 라이브러리 사용시 흔히 실수 할 수 있는 부분과 그것을 바로잡은 예시와 함께 글을 마무리 하겠습니다.
입문자들의 흔한 실수
🖍️ 이렇게 쓰지 말자!
게시판 화면에 나타낼 게시글 목록을 서버에서 쿼리 해오는 단순한 예제입니다.
리액트 쿼리의 useQuery에서 fetch한 data를 useEffect를 통해 Jotai atom의 value로 저장하고 있습니다.
여기서는 useQuery의 onSuccess 옵션을 통해 query의 결과를 useState의 상태로 전달하고 있습니다.
위처럼 서버에서 가져온 데이터를 useEffect
, onSuccess
를 통해 다른 지역 상태(state)나 전역 상태(atom)로 옮겨 담는 것은 대표적인 안티패턴입니다. (onSuccess를 위처럼 잘못 사용하는 사례가 워낙 많기에 React Query는 해당 API를 deprecated 시켰고 다음 메이져 업데이트에서 삭제 될 예정입니다.) 리액트 쿼리가 이미 상태 관리자이기에 또다른 상태를 만들어 복제하는 것은 불필요한 행위며, 데이터가 나뉘어지게 되면서 예기치 못한 오류가 발생할 가능성이 높아지게 됩니다.
🛠️ 적절한 사용 예시
게시글 조회와 더불어 조건에 따른 검색 기능을 추가해달라는 요구사항이 들어왔습니다. 해당 과정에서 상태를 크게 2가지로 나누면 검색 조건은 클라이언트 상태, 게시글 목록은 서버 상태라 할 수 있습니다.
검색 조건을 지역 상태나 전역 상태나 그때 상황에 맞게 적용하면 되지만 여기서는 전역 상태로 적용해봤습니다.
Jotai와 React query의 올바른 조합입니다. 클라이언트가 filter의 값을 변경시키면 queryKey가 바뀌고 자동으로 쿼리를 실행합니다.
진짜 마치며
어느 IT 분야건 그렇지만 웹 프론트엔드 분야는 특히 빠르게 변화합니다. 리액트가 첫 공개 된 지 이제 10년이 되었고 상태관리 라이브러리의 원조맛집이라 할 수 있는 리덕스의 등장은 2015년이었습니다. 이후 매년 트렌드가 바뀌고 새로운 라이브러리가 등장했습니다. React Query로 서버 상태를 편하게 관리할 수 있게 된 것도 5년이 채 되지 않았으며 마지막에 소개한 Recoil, Jotai는 2020년에 등장 한 라이브러리 입니다. 이 외에도 Zustand, xState, Valtio, HookState 등 수많은 상태관리 라이브러리가 존재하고 앞으로도 등장 할 것이며 기존 라이브러리들도 업데이트 될 것입니다.
자칫 혼란스러울 수 있지만 프로젝트에 요구되는 상태의 범위와 성질을 이해하고 각 라이브러리들이 지향하는 방향성을 알고 있으면 본인에게 필요한 라이브러리가 무엇이고 어떻게 사용해야 하는지 판단하는데 도움이 될 것입니다.