사내에 도입된 상태 관리 라이브러리를 따라 쓰다보니 '상태 관리'에 대해 깊게 고민해 본 적이 없는 것 같다. 라이브러리를 쓰면서도 사용법만 찾아보고 기능을 구현하는 데 집중했었다. 우연히 '리액트 훅을 활용한 마이크로 상태관리' 책을 빌려 읽게 됐는데, 이 책을 읽으면서 리액트에서 상태를 다루는 여러 방법의 특징과 장단점을 이해하는 시간을 가질 수 있었다.
전부 읽진 못 했지만 읽은 부분 중 기억하고 싶은 부분들을 정리해 기록해보려고 한다. 나머지는 다시 빌려와서 읽어야지...
간단한 소개
리액트를 쓰는 개발자라면 누구나 어렵지 않게 읽고 받아들일 수 책이었다.
이 책은 리액트에서 상태를 다루는 방법들과 각 방법의 특징들을 예제와 함께 설명한다. 뒷부분에서는 이를 바탕으로 잘 알려진 전역 상태 관리 라이브러리들을 소개하고 있다. 나는 딱 라이브러리 설명 전까지 읽었다. 이부분까지 읽는다면 각 라이브러리의 상태 관리 방식을 비교하며 상황에 맞게 도입할 수 있을 것 같다.
책 주요 내용 기록
마이크로 상태관리란?
리액트 훅은 다양한 상태를 각기 특정한 방법으로 처리하는 방향으로 만들어지고 있다. 그렇지만 목적 지향적인 방법으로 처리할수 없는 상태도 있기에 여전히 범용적인 상태관리가 필요하다. 범용적인 상태관리를 위한 방법은 가벼워야 하며, 개발자는 요구사항에 따라 적절한 방법을 선택할 수 있어야한다. 이를 가리켜 마이크로 상태관리라고 한다.
지역상태와 전역 상태
- 지역 상태: 컴포넌트 내에서 정의되고, 컴포넌트 트리 내에서 사용되는 상태에 대한 기본적인 훅 제공
- 전역 상태: 애플리케이션 내 서로 멀리 떨어져 있는 여러 컴포넌트에서 사용하는 상태
- 싱글턴일 필요는 없으며, 싱글턴이 아니라는 점을 명확히 하기 위해 공유 상태라 부르기도 한다.
- 리액트가 지역성이 중요한 컴포넌트 모델에 기반하기 때문에 리액트에서 전역 상태를 구현하는 것은 간단한 작업이 아니다.
전역 상태는 컴포넌트 외부에서 리액트 컴포넌트의 동작을 제어할 때 유용하게 사용할 수 있지만 컴포넌트 동작을 예측하기 어렵다는 장단점이있다. 지역상태를 기본으로 사용하고 전역상태는 보조수단으로 사용하는 것이 좋다.
상태 끌어올리기 기법, 내용 끌어올리기 기법을 통해 충분히 구현 가능하다면 지역상태로 관리하자. 하지만 prop drilling이 발생하게 되거나 리액트 외부에 상태가 있는 상태라면 전역 상태를 고려하자.
그렇다면 전역 상태는 어떻게 구현하고 사용하는 게 좋을까?
전역 상태는 지역상태와는 다르게 상태가 특정 컴포넌트에 속해 있지 않으므로, 상태를 저장하는 위치를 고려해야한다. 또한 전역상태는 여러 컴포넌트에서 사용하기 때문에 리렌더링 최적화가 중요하다.
리액트 컨텍스트 활용해 상태 공유하기
나에게 이 책을 읽기 전에 리액트에서 전역 상태 관리 라이브러리를 쓰지 않고 전역 상태를 구현하라고 하면 큰 고민 없이 컨텍스트 api를 썼을 것이다. 컨텍스트, useContext와 useState (or useReducer)를 이용하면 전역 상태를 위한 사용자 정의 훅을 만들 수 있다. 사실 컨텍스트는 전역 상태를 위해 설계되지 않았다. 일반적으로 전역 상태를 생각했을 때 앱 전반에서 접근 가능한 상태라고 생각하지만 컨텍스트로 작성된 전역 상태는 리액트 외부에서는 접근이 불가능하다. 또한 Context는 싱글턴 패턴을 위해 설계된 것이 아닌 싱글턴 패턴을 피하고 각 하위 트리에 서로 다른 값을 제공하기 위한 기능이다.
구독을 이용해 모듈 상태 공유하기
전역상태를 싱글턴과 유사하게 만들고 싶다면 모듈 상태를 사용하는 것이 싱글턴 값으로 메모리에 할당되기 때문에 더 좋다. 컨텍스트보다 덜 알려진 패턴이지만 기존 모듈 상태를 통합하는 데 자주 사용된다고 한다.
모듈 상태의 엄격한 정의는 ECMA script 모듈 스코프에 정의된 상수 또는 변수다. 이 책에서는 단순하게 모듈상태는 전역적이거나 파일의 스코프 내에서 정의된 변수라고 가정한다. 기본적으로 모듈상태를 다룬다고 하면 리액트 외부에 Set과 같은 별도의 자료구조에 추가해 관리할 수 있다. 그러나 이런 방법은 컴포넌트가 추가될 수록 중복코드가 늘어남으로 좋은 해결책은 아니다. 상태가 갱신됐을 때 알림을 받는 구독(subscribe) 방식을 이용하면 모듈 상태를 리액트 상태에 효과적으로 연결할 수 있다. 여기에 컴포넌트가 필요로 하는 상태의 일부분만 반환하는 선택자(selector)를 도입하면 모듈 상태가 변경될 때 컴포넌트가 리렌더링할 수 있게 만들어 불필요한 리렌더링을 피할 수 있다.
하지만 모듈 상태는 리액트 컴포넌트 외부에 존재하는 전역으로 정의된 싱글턴이기 때문에, 컴포넌트 트리나 하위 트리마다 다른 상태를 가질 수 없다는 한계가 있다. 예를들어 모듈상태를 사용하는 Counter 컴포넌트가 있다고 할 때 다른 모듈 상태를 사용하는 Counter 컴포넌트가 필요하다면 다른 모듈 상태에 맞춰 Counter 컴포넌트를 추가로 작성해야한다. props에 store를 넣으면 Counter 컴포넌트를 재사용할 수 있을 것 같지만 컴포넌트가 깊게 중첩되면 props drilling이 발생한다.
두 방법의 특징을 요약해보았다.
리액트 컨텍스트
- 장점: 각 하위 트리마다 서로 다른 값 제공 가능
- 한계: 리액트 외부에서 접근 불가능, 불필요한 리렌더링 발생 가능
모듈 상태 구독
- 장점: 선택자를 사용해 불필요한 리렌더링을 막을 수 있음
- 한계: 리액트 컴포넌트 외부에 존재하는 전역으로 정의된 싱글턴이기 때문에, 컴포넌트 트리나 하위 트리마다 다른 상태를 가질 수 없음
컨텍스트와 구독 패턴을 함께 사용하기
위의 두 방법을 함께 사용해 전역상태를 구현할 수도 있다. 이렇게 사용할 경우 다음과 같은 이점이 있다.
- 컨텍스트는 하위 트리에 전역 상태를 제공할 수 있고, 컨텍스트 공급자를 중첩하는 것이 가능하다. 컨텍스트를 사용하면 리액트 컴포넌트 생명 주기 내에서 useState와 같은 훅으로 전역 상태를 제어할 수 있다.
- 반면 구독을 사용하면 단일 컨텍스트로는 불가능한 리렌더링 문제를 해결할 수 있다.
이 방법은 어느 정도 규모가 큰 애플리케이션에 유용하다.