글의 시작에 앞서 이 글은 Past, Present, and Future of React State Management를 번역한 글임을 밝히며 한국어 번역은 cadenzah님께서 하셨습니다.
원문은 2021년에 작성된 글이며 원문 그대로를 가져오는것이 목적이므로 정보의 최신화는 하지 않았습니다.
상태 관리에 대한 글을 작성하다 이보다 좋은 내용은 없을 것 같아 글의 출처를 밝히고 가져오게 되었으며 리액트의 상태 관리에 가볍게 알아보자는 취지 하에 개시합니다.
원문 출처 : https://leerob.io/blog/react-state-management?fbclid=IwAR2jI5klYjGLoJXuOLcMIctncQuniOgMQIwmwEIb65T-C3x0-erc86Jvu00
한국어 번역본 출처 : https://velog.io/@cadenzah/history-of-react-state-management
이하의 내용은 출처의 내용과 동일한 내용입니다.
이 글은 Past, Present, and Future of React State Management를 번역한 글입니다.
매끄러운 문맥을 위하여 의역을 한 경우가 있습니다. 원문의 뜻을 최대한 해치지 않도록 노력했으니 안심하셔도 됩니다.
영어 단어가 자연스러운 경우 원문 그대로의 영단어를 적었습니다.
저의 보충 설명은 인용문에 작성하거나, 괄호 안에 역자 주 문구를 통하여 달았습니다.
React 상태 관리의 과거, 현재, 그리고 미래(韓国語版)
React는 2013년 5월에 처음 공개되었습니다. UI는 상태에 대한 함수라는 생각이 React가 제시한 패러다임의 전환이었습니다. 컴포넌트의 상태가 주어지면, React는 컴포넌트가 어떤 모습을 가져야 할지 결정할 수 있게 된다는 것입니다. React는 이와 같은 상태에 대한 아이디어를 근간으로 삼고 있습니다. 하지만, 상태는 지금까지 React 어플리케이션을 개발할 때에 가장 까다로운 부분 중 하나로 여겨져 왔습니다.
React의 상태 관리 도구는 마치 거칠고 투박한 공구 벨트와 같은 것인데, 언제 어떤 공구를 사용해야 하는 건지 누구나 잘 아는 것은 아닙니다. 이 글에서는 상태 관리의 과거, 현재, 그리고 미래에 대하여 설명하면서 여러분의 팀, 프로젝트, 또는 조직에서 올바른 결정을 내리는 데에 도움을 드리고자 합니다.
용어
시작하기에 앞서, 우선 주로 사용되는 용어를 이해해두고 넘어가야 합니다. 아래의 용어들은 공식적인 것은 아닙니다. 몇몇 다양한 변형이 존재하지만, 그 기저에 깔린 근본적인 아이디어는 동일합니다.
UI 상태: 어플리케이션의 상호작용 부분 제어에 사용되는 상태(e.g. 다크 모드 토글, 모달 등)
서버 캐시 상태: 서버 응답에 대한 상태로, 클라이언트 측에 캐시해두어 빠르게 접근하는 데에 사용(e.g. API 호출 결과를 저장하고, 이후 여러 위치에서 활용)
폼 상태: 폼에 대한 다양한 상태(e.g. 로딩중, 전송중, 비활성화, 유효성 검사, 재시도 등). 제어 & 비제어 폼 상태 또한 존재한다.
URL 상태: 브라우저가 관리하는 상태(e.g. 상품을 필터링한 뒤 이를 쿼리 인자에 저장하고, 이후 페이지를 새로고침하더라도 필터된 상품이 동일하게 페이지에 표시).
상태 기계: 시간 경과에 따른 상태 변화에 대한 명시적 모델(e.g. 신호등의 색깔은 초록 → 노랑 → 빨강 순으로 변화하지만, 초록 → 빨강 으로 변화하는 경우는 없음).
과거
React의 컴포넌트 모델은 재사용과 분해가 용이한 어플리케이션을 개발할 수 있게 해주었습니다. 각 컴포넌트는 고유한 로컬 상태를 가집니다. 웹 어플리케이션이 점점 복잡해짐에 따라, 컴포넌트 간에 로직을 쉽게 공유할 수 있는 새로운 솔루션들이 등장하게 되었습니다.
연표
시간이 흐름에 따라 상태 관리가 어떻게 진화하였는지 더 쉽게 이해할 수 있도록, React의 유명한 상태 관리 솔루션들을 대략적인 시간 순으로 정리해보았습니다. 아래 목록은 UI 상태 관리에 치중하고 있습니다. 자세한 설명을 적지는 않았지만, 맥락을 이해하는 데에는 충분할 것입니다.
- 2013 - React 등장
- 2014 - Flux (다양한 라이브러리 등장)
- 2015 - Redux
- 2016 - MobX
- 2018 - Context
- 2019 - Hooks 등장 (+ React Query, SWR)
- 2019 - Zustand
- 2019 - xState
- 2020 - Jotai, Recoil, Valtio
- 2021 - useSelectedContext
위 연표에 적힌 내용이라고 하여 반드시 배울 필요가 있는 것은 아닙니다. 이에 대하여서는 이후에 자세히 설명하겠습니다. 자, 이제 React 상태 관리의 역사에 대하여 본격적으로 이야기해봅시다.
Redux
Redux는 본래 2014년 Facebook에서 처음 제안한 패턴인 "Flux 아키텍처"의 구현체로서 만들어졌습니다. Redux는 2015년에 등장하였고 이후 빠르게 관심을 얻어 Flux에 영감을 받은 것들 중에서 가장 유명한 라이브러리가 되었습니다. Redux의 도구와 라이브러리 생태계는 UI 상태와 서버 캐시 상태의 관리를 통합하였습니다. Redux는 지금도 여전히 가장 유명하고 널리 사용되고 있습니다.
서버 캐시 상태
React가 등장한 초기에는, 대부분 상태 관리 도구들의 주요 기능은 API로부터 불러온 데이터를 어플리케이션 전체에 걸쳐 사용할 수 있도록 캐싱하는 것이었습니다. 서버 캐시 상태를 관리하기 위한 간단하면서 널리 통용되는 방법이 마땅히 없었기 때문에, React 사용자들은 Redux와 같은 라이브러리에 크게 기댈 수 밖에 없었습니다.
React Hooks가 발표되면서, 공유된 Hooks에 로직을 통합하는 것이 훨씬 쉬워졌고 접근성이 향상되었습니다. SWR과 React Query와 같은 라이브러리가 등장하여 이 문제를 중점적으로 해결하였습니다.
"단지 서버 캐시 상태만을 위하여 별도 라이브러리를 사용할 이유가 있을까요?"라고 반문하는 분도 계실 수 있습니다. 글쎄요, 캐시 작업은 까다롭습니다. 서버 캐시 상태는 UI 상태와 다른 유형의 문제를 다룹니다. 서버 캐시 상태와 관련된 라이브러리가 다루는 문제를 간단히 정리해보았습니다.
- 주기적인 폴링(Polling)
- 포커스 시에 재검증
- 네트워크 회복 시에 재검증
- 지역적 상태 변경 (옵티미스틱 UI)
- 향상된 Error Retry
- 페이지네이션 및 스크롤 위치 복구
역자 주: 위 목록은 SWR 공식 문서의 Introduction에서 일부 발췌해온 것입니다.
이러한 기능들을 직접 구현하고 싶으신가요? 아마 아닐 겁니다.
React Context
v16.3 업데이트를 통하여, React Context는 컴포넌트 간에 로직을 공유할 수 있는 퍼스트 파티 솔루션을 제공하게 되었습니다. 또한 이를 통하여 중첩된 여러 컴포넌트들에 props로 값을 전달하는 코드(i.e. "Prop Drilling")를 방지할 수 있게 되었습니다.
역자 주: 위 단락에 링크한 Prop Drilling에 관련된 블로그 글은 번역본이 있어 원문 이를 대신 링크하였습니다.
React Context는 그 자체만 가지고는 상태 관리가 아닙니다. 하지만 React Context는 useReducer와 같은 Hooks와 함께 사용하면 상태 관리 솔루션으로 활용할 수 있습니다. 이 조합은 UI 상태와 관련된 많은 공통 사례들을 해결할 수 있습니다.
역자 주: 위 단락에 링크한 React Context와 Hooks 관련된 블로그 글은 번역본이 있어 원문 이를 대신 링크하였습니다.
현재
2021년 현재, React에서 상태 관리를 다루는 데에는 다양한 방법들이 존재합니다. 커뮤니티가 성장하고 다양한 유형의 상태들을 접하게 됨에 따라, 특정 문제 상황 해결에 중점을 둔 세부적인 라이브러리들이 많이 개발되었습니다.
상태 기계
Switch 구문을 떠올려보죠. 만약 state의 값이 어떤 case에 대응한다면, 이에 대한 코드가 실행됩니다. 한정된 가짓수의 경우가 존재합니다. 이는 가장 단순한 형태의 상태 기계이고, 상태에 대한 명시적인 모델입니다.
case state === 'loading':
// 로딩 스피너 표시
break;
case state === 'success':
// 성공 메시지 표시
break;
default:
// 오류 메시지 표시
}
유한 상태 기계와 상태표는 기초적인 컴퓨터 과학 개념으로, 특별히 React에 국한된 것은 아닙니다. 별도의 서드 파티 라이브러리 없이도 useReducer를 상태 기계로 변환할 수 있습니다.
상태 기계는 데이터베이스, 전자, 자동차 등 모든 분야에 적용되고 있습니다. React 생태계에서 상태 관리의 개념이 점점 진화함에 따라, 이러한 오래된 개념이 현대의 상태 관리 문제를 해결할 수 있다는 것을 깨닫게 되었습니다. 상태 기계는 폼 상태 문제를 해결하는 데에 가장 널리 사용되고 있습니다.
유한 상태 기계를 사용하면, 어플리케이션 또는 컴포넌트가 가질 수 있는 상태의 갯수가 유한하게 결정됩니다. 실무에서는, 상태 기계를 사용하면 엣지 케이스들을 검토하고 정의할 수 있어, 버그를 찾아내는 것이 용이해집니다. 이에 관한 더 자세한 정보를 원한다면, xState에 대한 문서를 확인하시거나 이 강좌를 듣는 것을 권합니다. 또한, 상태 기계를 온라인 상에서 시각화해볼 수도 있습니다.
Zustand, Recoil, Jotai, Valtio, 아놔!
React 상태 관리 라이브러리가 이렇게나 많이 존재하는 이유가 도대체 무엇일까요?
Figma(또는 다른 디자인 도구)를 한번 떠올려보죠. 보시면 제어 툴바들이 여러 개 있는데, 이걸 조작하면 해당 툴바 바깥의 다른 요소에 영향을 미치거나, 컴포넌트가 그려지는 위치 등이 달라집니다.
눈치채셨겠지만, 이 정도 규모의 어플리케이션에서는 복잡한 상태 관리 솔루션이 필요합니다. 좋은 사용자 경험을 위해서는 성능과 프레임레이트가 아주 중요하며, 따라서 리렌더의 시점과 방법에 대한 제어권을 가질 수 있어야 합니다. 이렇게 독특한 사례들로 인하여 상태 관리라는 영역에서 다양한 탐험이 이어지는 것입니다.
Daishi Kato에 따르면, 각 라이브러리들 간의 차이점들은 아래와 같이 간단하게 요약할 수 있습니다.
- Valtio는 값을 직접 변경하는 식(Mutation-style)의 API를 제공하고자 위임자(Proxy)를 사용한다.
- Jotai는 "계산된 값"과 비동기 액션에 최적화되었다.
- Zustand는 모듈 상태에 특히 중점을 둔 매우 가벼운 라이브러리이다.
- Recoil는 데이터 흐름 그래프를 사용한 실험적인 라이브러리이다.
역자 주: 각 라이브러리의 공식 페이지 또는 Github 링크는 아래와 같습니다.
Valtio: Makes proxy-state simple for React and Vanila
Jotai: Primitive, flexible state management for React
Zustand: Bear necessities for state management in React
Recoil: A state management library for React
상태 구조가 복잡하다고 하여 반드시 서드 파티 라이브러리를 사용해야 하는 것은 아닙니다. React와 JavaScript만으로 시작하여 어디까지 가능한지 보는 것도 하나의 방법입니다. 최적화에 상태 관리 라이브러리가 필요하다면, 해당 경우에서 관련 지표(e.g. 프레임률 등)를 추적 및 측정해보고, 문제가 해결되는지를 확인하면 됩니다.
확실한 필요가 없는 상황이라면 위 라이브러리를 굳이 사용하지 마세요.
불변 상태
또 하나의 논쟁거리는 가변 및 불변 상태에 대한 것입니다. 여기엔 정답이 없고, 의견만 있을 뿐입니다. 바닐라 JavaScript로 상태 관리를 수행하고 있다면, 가변 상태로 관리하고 있을 가능성이 높습니다. 변수를 초기화하고, 이후에는 해당 변수를 새로운 값과 동일한 값으로 만드는 것이죠. let이냐 const냐에 대한 논쟁 또한 존재합니다.
불변 상태는 React에서 큰 유명세를 얻었습니다. 불변성을 주장하는 사람들은 상태 관리 솔루션이 상태를 직접 변경할 수 있게 되면 이는 더 많은 버그로 이어질 것이라고 말합니다. 가변성을 주장하는 사람들은 불변성이 오히려 더 복잡성만 키울 뿐이라고 반박합니다. 직접 조작은 간접 조작에 비하면 항상 덜 안전합니다. 편리와 위험 사이의 양자택일 상황이며, 선택은 여러분 그리고 여러분이 속한 팀의 몫입니다.
Immer와 같은 솔루션을 사용하면 코드는 가변적으로 작성되지만 실행은 불변적으로 이루어집니다. 좋네요. 즉, 현재 상태에 대한 위임자(Proxy)에 해당하는 상태 초안에 변경 사항을 우선 적용하는 것입니다. 변경이 완료되고나면, 해당 상태 초안에 이루어진 변경 사항을 기반으로 Immer는 다음 상태를 생성합니다.
URL 상태
Amazon과 같은 전자 상거래 웹 사이트를 개발한다고 해봅시다. React 서적을 검색하고, 별점이 4 이상인 항목들만 필터합니다. 이 상태는 쿼리 인자로 유지되고 브라우저가 관리합니다. 페이지를 새로고침해도 동일한 항목들을 볼 수 있습니다. 해당 URL은 다른 사람과 공유할 수 있고 그 사람들도 동일한 결과를 확인할 수 있습니다.
또 다른 흥미로운 예시는 Nomad List입니다. 브라우저 URL 상태를 데이터 함수로 변환할 수 있습니다. 게다가 사람이 읽을 수 있는 형식의 URL을 구성할 수도 있습니다 (이는 Google이 선호하는 형식이기도 합니다).
미래
대형 어플리케이션의 경우, 단순히 Context만 사용한 상태 관리 솔루션(e.g. useReducer)으로는 과도한 리렌더링 문제가 발생할 수 있습니다. Context 값이 변경되면, useContext를 사용하는 모든 컴포넌트에서는 리렌더링이 이루어집니다. 그러면 UI 상호작용이 느리고 버벅대는 것처럼 느껴지지요. 육안으로 확인이 안 되면, React Dev Tools를 사용하여 리렌더링을 확인할 수 있습니다.
React 팀은 Context 사용 시의 성능 이슈를 적절하게 방지하기 위한 useSelectedContext Hook을 소개했습니다. 2019년 7월에 이에 대한 RFC가 공개되었고, 2021년 1월 기준으로 Feature 플래그가 붙어서 개발이 순조롭게 진행되고 있습니다. 이 Hook을 사용하면 Context의 "일부"만을 선택하여 해당 일부분이 변화했을 때에만 리렌더가 이루어지도록 만들 수 있게 됩니다.
이미 리렌더링 성능을 최적화할 수 있는 다른 방법(e.g. useMemo)이 존재하지만, Context를 위한 퍼스트 파티 솔루션이 나온다면 더 좋을 것입니다. useContextSelector라는 커뮤니티 기반 라이브러리 또한 존재하며, 유사한 접근 방식을 취합니다 (Demo). Jotai와 Formik 3은 해당 라이브러리를 내부에서 활용하고 있습니다. useSelectedContext가 React 표준 라이브러리에 포함된다면, 외부 라이브러리 사용으로 인한 복잡성과 코드 번들 크기가 줄어들 것이고, 보다 많은 성능 관련 옵션을 기본으로 제공할 수 있게 될 것입니다.
장기적으로, React는 어떤 컴포넌트를 리렌더링해야 할지 스스로 알아낼 수 있게 될 것입니다("Auto Memoization").
상태 관리 도구 선택지
아래 목록은 모든 라이브러리를 보여주는 것이 아닙니다. 이 글은 오픈 소스이기도 하니, 아래 목록에 동의하지 않거나 뭔가 틀렸다고 느낀다면 PR을 작성해주시기 바랍니다. 웬만하면, 당신이 속한 팀과 팀 내 개발자들이 선호하는 것으로 선택하시기 바랍니다. Redux에 불만이 없으신가요? 그럼 그냥 쓰세요!
폼 상태
경험 | 학습 의욕 | 프로젝트/팀 규모 | 해결책 |
---|---|---|---|
초급 | 낮음 | 소형 | useState |
초급 | 중간 | 중간, 소형 | 폼 라이브러리(Formik, Final Form) |
초급 | 높음, 중간 | 대형 | 테크 리더와 상의 |
중급 | 낮음 | 중간, 소형 | 폼 라이브러리(Formik, Final Form) |
상급 | 중간 | 중간 | 상태 기계 |
상급 | 높음 | 중간 | 상태 기계 |
상급 | 높음 | 대형 | 상태 기계 |
UI 상태
경험 | 학습 의욕 | 프로젝트/팀 규모 | 해결책 |
---|---|---|---|
초급 | 낮음 | 소형 | useState |
초급 | 중간 | 중간, 소형 | useContext + useReducer |
초급 | 높음, 중간 | 대형 | 테크 리더와 상의 |
중급 | 낮음 | 중간, 소형 | Redux Toolkit |
상급 | 중간 | 중간 | useContext + useReducer |
상급 | 높음 | 중간 | Jotaim, Valtio |
상급 | 높음 | 대형 | Recoil (또는 GraphQL을 사용한다면, Relay |
서버 캐시 상태
경험도 또는 팀 규모와 상관 없이, SWR과 React Query가 완벽한 솔루션입니다. 둘 모두 만족스러울 겁니다. GraphQL을 사용한다면, Apollo를 이미 알고 계실 겁니다.
That's all, folks!
React에서 상태 관리는 지난 8년 동안 크게 진화해왔습니다. 대형 웹 어플리케이션 개발에 있어 가장 까다롭고, 미묘한 내용 중 하나입니다. 상태의 다양한 유형들과 각각의 트레이드오프를 이해해야 이를 바탕으로 정보에 입각한 결정을 내릴 수 있습니다. 이 글이 도움이 되기 바라며, 읽어주셔서 감사합니다.