📝

React의 상태관리 (전유진, 여다희)

 

1-1. 상태 관리란?

1-1-1. 상태 관리

상태, State는 데이터를 담을 수 있는 React의 내장 객체 중 하나입니다. 상태에 담긴 값은 사용자의 액션이나 네트워크 등 개발자의 의도에 따라 값이 변경될 수 있는데, 이 때 상태값을 사용하고 있는 컴포넌트는 변경된 값을 반영해 다시 렌더링됩니다. 즉, 상태는 컴포넌트에 영향을 주는 동적인 데이터입니다.
친구의 인스타그램을 구독하려 한다고 가정해봅시다. 프로필 화면에 들어가면 “팔로우” 버튼이 보이는데, 이 것을 누르면 버튼의 텍스트는 “팔로잉”로 변경되며 친구의 팔로워 숫자가 1 증가하게 됩니다. 이 경우에는 “사용자를 팔로우”하는 액션으로서 “내가 이 사용자를 팔로우하고 있는가”와 “이 사용자는 몇 명의 팔로워를 가지고 있는가”에 관한 상태 값을 변경한 것입니다.
notion imagenotion image
notion imagenotion image
notion imagenotion image
위의 예시와 같이, 상태는 시간이 흐르며 실시간으로 변화합니다. “팔로워 수” 상태가 증가하거나 감소하기도 하고, 비동기 요청에 대한 응답이 돌아왔는지에 따라 “로딩중” 상태가 true 혹은 false로 변경되기도 합니다. 따라서 프론트엔드 개발자는 1)상태를 UI/UX에 맞게 정제해 사용자에게 보여주고 2)상호작용에 의한 상태의 변경을 다루어야 하는데, 이 것을 상태 관리라고 합니다.
프로젝트의 규모가 커지게 되면 상태가 점점 많아지고 하나의 상태를 공유하는 컴포넌트가 늘어나 구조가 점점 복잡해집니다. 덕분에 상태가 언제, 어디서, 어떻게, 왜 변화했는지 추적할 수 없는 상황까지 도달하게 되는데, 이런 상황을 방지하기 위해 상태를 어디에 저장할 것인지, 네트워크 요청이나 응답에 따라 상태를 어떻게 변경할 것인지 등을 고려해 상태를 잘 설계하는 것이 중요합니다.
 

1-1-2. 지역 상태와 전역 상태

상태를 전역적으로 관리해야 할 때가 있다
 
 

1-2. 전역 상태 관리, 언제 필요할까?

언제 어떤 상황에서 전역 상태관리가 필요할까요?

1-2-1. Prop Drilling 방지

React는 UI 컴포넌트를 재사용 가능한 단위로 잘게 쪼개고, 그것을 합성하는 아키텍처를 권장합니다. 그런 React의 이념을 충실히 따르다 보면 컴포넌트 트리의 깊이가 점점 커지게 되는데, 때문에 하나의 상태를 다양한 컴포넌트가 참조하는 상황에서는 상위의 컴포넌트에서 상태를 만들고, 값이나 setter를 하위 컴포넌트의 prop으로 전달해주어야 합니다. 이 때 props를 오로지 하위 컴포넌트로 전달할 뿐 자신은 상태 props를 필요로하지 않는 컴포넌트가 생기게 되는데, 이것을 Prop Drilling 현상이라고 합니다.
prop drilling 예시 이미지 (대체될 예정)prop drilling 예시 이미지 (대체될 예정)
prop drilling 예시 이미지 (대체될 예정)
Prop Drilling은 ToDoList와 같이 작은 규모의 프로젝트에서도 흔히 볼 수 있는 현상입니다. 하지만 이 때문에 구조를 파악하기 힘들지는 않죠.
그렇다면 Prop Drilling은 언제 문제가 될까요? 리액트에서 상태 관리 문제가 화두되는 것은 항상 프로젝트의 규모가 커지는 시점입니다. 상태를 필요로 하는 컴포넌트의 깊이(depth)가 깊어지고, 점점 더 많은 상태들이 Drilling될 때 코드는 곱절로 복잡해져 코드의 가독성과 유지보수성을 크게 해칩니다. 만약 하위 컴포넌트를 작업하다가 어떤 데이터를 받아오고 있는지 확인하거나 prop의 이름이라도 바꾸고자 하면, 컴포넌트의 여러 계층을 파헤치고 다녀야 할 것입니다.
이런 불편함을 방지하기 위해서는 넓은 범위, 혹은 다양한 계층의 컴포넌트가 참조하는 상태는 전역(global)적으로 관리하는 것을 고려해 보아야 합니다.
 

1-2-2. UI 상태 측면

대부분의 UI 상태는 전역으로 관리하지 않지만 Toast, Modal과 같은 컴포넌트 트리의 외부에서 노출되는 컴포넌트는 전역으로 관리하는 경우가 생깁니다. 예를 들어 어떤 Modal의 상태를 제어하는 컴포넌트가 있고 다른 컴포넌트에서도 해당 Modal을 제어해야할 경우가 생길 때 전역 상태관리의 필요성을 느낄 수 있습니다. (전역상태관리를 적용하지 않고 Context API를 통해 해결할 수도 있습니다.)
Toast(출처 : bootstrap 공식 홈페이지)Toast(출처 : bootstrap 공식 홈페이지)
Toast(출처 : bootstrap 공식 홈페이지)
Modal(출처 : bootstrap 공식 홈페이지)Modal(출처 : bootstrap 공식 홈페이지)
Modal(출처 : bootstrap 공식 홈페이지)
 

1-2-3. 서버상태 측면

사용자의 로그인 정보나 서비스의 데이터와 같은 특정 데이터를 조회하는 어플리케이션이 있다고 생각해보겠습니다. 이 데이터는 하나의 컴포넌트가 아닌 여러 개의 컴포넌트에서 다루게 됩니다. 그런 경우에 데이터 호출을 여러 번 할 경우 불필요한 네크워크 비용이 낭비되기 때문에 특정 컴포넌트에서 데이터를 호출에 관리하면서 다른 여러 컴포넌트로 전달해 주어야 합니다.
 

1-3. 좋은 상태관리란 무엇일까?

1-3-1 전역 상태 최소화

앞서 말했듯 모든 프로젝트에서 전역 상태관리가 필요한 것은 아닙니다. 일반적인 경우에서 상태는 지역 상태로 관리 하는 것을 권장합니다(다수의 컴포넌트간 상태 의존성이 높아지는 경우 제외). 상태를 무분별하게 전역으로 관리하면 불필요한 재렌더링을 일으켜 성능 이슈가 생길 수 있기 때문에, 단순히 Prop Drilling을 피하기 위한 목적이라면 합성 컴포넌트를 적절히 사용하는 것이 하나의 방법이 될 수 있습니다. 또, 데이터가 연관된 컴포넌트들은 최대한 가까이 배치해서 응집도를 높히는 방법으로 전역 상태 관리를 배제할 수 있습니다.
 

1-3-2 서버 캐시 상태의 전역상태관리

전역상태에서 서버캐시 상태를 캐싱하는 것을 특정시점에 데이터가 캡쳐되었다고 합니다. Redux를 예시로 들면, actiondispatch된 시점에 캡쳐된 데이터가 저장이 되었고, 그 이후 클라이언트가 데이터를 변경한다면 서버의 데이터는 다시 actiondispatch하기 전까지 캡쳐된 데이터에 반영되지 않습니다. 즉, 동일한 데이터를 따로 관리하게 된다면, 둘은 “호출한 시점”에서만 같다고 볼 수 있으며 서버에서 데이터를 다시 호출하기 전까지는 그 둘의 “동일성”을 보장할 수 없습니다. 그렇기 때문에 언제, 어떤 데이터를, 어떻게 불러올 것인지를 명확하게 하고 코드를 작성하는게 좋습니다. 리덕스팀은 이러한 단점을 인정하고 rtk-query를 출시했습니다. rtk-query는 서버의 데이터를 캐싱하는것이 아니라 데이터의 호출을 캐싱하는 방법인데, rtk-query 외에도 react-queryswr과 같은 라이브러리가 있습니다.
 
💡
action과 dispatch란? 여기서는 간단하게 언급하고 추후 챕터에서 깊이 설명합니다. action은 Redux에서 상태를 변경시키기 위한 주문서입니다. dispatch는 상태 변경을 감지하여 상태가 변경 되면 재 랜더링이 발생하게 합니다.

1-3-3 역할을 분리한 상태관리

역할 측면에서 상태를 분리하자면 UI 상태, 서버캐시 상태, Form 상태, URL 상태 등으로 나눌 수 있습니다. 각각의 상태는 “본질적인 목적”을 갖고 있습니다. UI상태는 말 그대로 어플리케이션의 인터렉션을 제어하는 상태이며 서버캐시상태는 서버와의 빠른 접근을 위해 데이터를 캐싱하는 상태, Form상태는 disabled과 같은 form과 관련된 상태, URL상태는 쿼리 파라미터와같이 브라우저에서 관리되는 상태입니다. 그렇기 때문에 각 상태들은 섞이지 않고 본질적인 목적에 따라 분리하여 관리해야 합니다.
 

1-3-4 상황에 맞는 상태관리

개발을 하다보면 지역상태관리만으로 충분한 경우, 전역상태관리가 필수 인 경우, 또 그중에서 특정 라이브러리가 적합한경우, 또는 전역스토어를 직접 개발하는것이 적합한 경우 등이 생깁니다. 앞서 상태 라이브러리의 소개에서 이야기 했듯이, 각 라이브러리의 장/단점이 있기 때문에 모든 상황에서 하나의 전역상태관리 방법이 제일 좋다고 말 할 수 없습니다. 이제 각 상태관리 방법에 대해 자세히 들어가며, 상황에 맞는 적절한 상태관리에 대해 알아보겠습니다.
 

1-4. 상태 관리 라이브러리, 무엇을 쓸까?

Redux, MobX, Recoil, Zustand, Jotai 등 전역 상태 관리 이슈를 해결하기 위한 라이브러리들이 이미 만들어져 있습니다. 가장 인기있는 것부터 최근 주목받고 있는 라이브러리까지 많은 선택지가 있지만, 무엇보다도 각 라이브러리들이 어떤 장점과 단점이 있는 지 알고 내가 투입된 프로젝트의 특성에 어울리는 라이브러리를 선택하는 것이 중요합니다. 아래에서는 라이브러리 없이 Context로 전역 상태를 공유하는 방법과 함께 몇 가지 상태 관리 라이브러리들의 장단점을 알아보겠습니다.
 

1-4-1. 라이브러리는 필요 없어요, Context API

상태 관리 라이브러리를 사용하면, 그만큼 프로젝트의 빌드 크기가 늘어나고 보일러 플레이트가 커질 수 밖에 없습니다. 작은 규모의 프로젝트에서는 다른 라이브러리를 사용하지 않고 React의 내장 도구인 Context API를 활용하는 편이 더욱 적합할 수 있습니다. 상태 관리는 React Hook(useState, useReducer)을 이용하고, Context API로 역할에 맞는 Provider를 만들어 하나의 상태를 공유하는 컴포넌트들의 상위에서 감싸주면 Prop Drilling 없이 지역성 있는 상태관리가 가능해집니다.
하지만 Context API를 활용한 방법에서 값이 변화하면 해당 값을 사용하는 컴포넌트만 재렌더링 되는 것이 아니라 Provider로 묶인 전체 컴포넌트 트리가 다시 렌더링된다는 점을 주의해야 합니다. 때문에 이 방법은 반복적이고 복잡한 업데이트가 발생하는 구조보다는, 라이트 모드-다크 모드 테마와 같이 변경 빈도가 낮으면서 다양한 레벨의 컴포넌트에 데이터를 전달할 때 사용하는 것이 바람직합니다.
수정할 예정…수정할 예정…
수정할 예정…
💡
Context API는 상태관리 라이브러리가 아니다!
Context는 상태를 관리하지 않으며 리액트 컴포넌트 트리의 외부에서 상태값을 전달하는 전송 수단입니다. 때문에 단순히 Prop Drilling을 피하기 위한 목적이라면 전역상태관리 라이브러리를 사용하기 보단 Context API 사용을 권장합니다.
 

1-4-2. 가장 강력한 기능, Redux

현재 가장 인기있는 상태 관리 라이브러리는 Redux입니다. 오늘날 대다수의 리액트 프로젝트가 Redux를 상태 관리 라이브러리로 사용하고 있으며, 그만큼 커다란 생태계가 구축되어 있습니다.
Redux는 리액트 컨텍스트 외부에 저장소(Store)를 두는데, Action을 통해 저장소의 State를 불러오거나 업데이트할 수 있습니다. 전역에 단 하나의 저장소를 두고 있기 때문에 데이터의 흐름이 단순하고 상태 변경을 예측하기가 쉽습니다. 더불어 추가적인 로직이 필요할 때 사용할 수 있는 미들웨어, 현재 저장소의 상태를 조회할 수 있는 개발 도구 등이 잘 갖추어져 있다는 점 역시 큰 장점입니다. 다만 Redux는 전역 상태를 조금만 늘리더라도 작성해야 하는 코드의 양이 많아진다는 불편함이 있어 작은 규모의 프로젝트에는 어울리지 않을 수 있습니다.
notion imagenotion image
💡
Redux는 리액트 컨텍스트의 외부 요소이므로, 리액트 뿐 아니라 Vue, Vanilla JS 환경에서도 사용할 수 있습니다.
 

1-4-3. 간편하고 React스러운, Recoil

Recoil은 리액트만을 위해 만들어진 상태 관리 라이브러리입니다. 2020년 초 발표해 많은 관심을 받았고, 2023년 현재 Redux를 대체할 유력 후보로 꼽히고 있습니다.
Recoil은 상태 관리에 있어 Atom이라는 개념을 도입했는데, 상태를 Atom이라는 작은 데이터 조각으로 만들어 해당 Atom을 참조하는 컴포넌트들만 재렌더링을 시키는 방식입니다. Atom은 여러 개를 만들 수 있으면서 서로 독립적이기 때문에, 관심사에 따른 코드 분리가 가능합니다. 컴포넌트를 쪼개고 재사용성을 극대화하는 React의 이념을 잘 반영했다고 볼 수 있습니다.
notion imagenotion image
Redux와 비교했을 때, 별도의 보일러 플레이트가 없기 때문에 작성해야하는 코드의 양이 적고, 상태 공유가 단순해 배우기 쉽습니다. 아직 버전이 많이 나오지 않아 안정성 측면에서의 우려도 있지만 개발이 빠르게 진행되며 안정화되고 있습니다.