Micro State Management with React Hooks 리뷰

1ilsang
클라이밍 하실래염?
#book#review#react#hooks#context#zustand#jotai#valtio
Published

cover

Micro State Management with React Hooks 리뷰를 해보려고 한다.

선택하게 된 계기

이 책은 작년에 읽었었는데, 당시에는 이해가 많이 안 돼서 깊이 있게 생각하지 못했었다.

실무에서 Jotai를 사용하기 시작하면서 사용하는 라이브러리에 대한 이해를 높이고자 다시금 이 책을 읽게 되었다.

이 책은 세 가지 흥미로운 부분이 있다.

  1. 원서다. 영어 기술 서적은 처음 읽어서 꽤 도전적이었다.
  2. Zustand, Jotai 등을 만든 상태관리에 진심인 메인테이너 Daishi Kato가 직접 펴낸 책이다.
  3. React에서의 상태 관리 전략을 다양하게 보여주기 때문에 시야가 넓어진다.

다 읽고 나서 알게 되었는데, 이 책은 최근에 한국어로 번역되었다(-_-). 이때 아니면 언제 원서 읽었겠나 싶어 만족하려고 한다.

간단한 요약

  • 상태란 무엇일까?
  • 상태는 어떻게 존재할 수 있을까?
  • 상태를 변화시키는 방법은 어떤 것들이 있을까?
  • React hooks은 상태 관리 라이브러리에 어떤 영향을 주었을까?
  • 우리는 리렌더링을 어떻게 회피할 수 있을까?

이 책을 읽으면서 얻을 수 있었던 인사이트들이다.

너무 어렵게 들어가지 않으면서 상태 관리의 다양한 기법들을 제시하기 때문에 주니어부터 시니어까지 충분히 배울게 많은 책이라는 생각이 든다.

인상 깊었던 부분

책을 읽으면서 좋았던 예제나 포인트들을 가볍게 소개하고자 한다.

1. React State에 대한 이해

React에서 상태(state)는 UI를 나타내는 모든 데이터다. React는 상태와 함께 렌더링 할 컴포넌트를 처리한다.

책의 서두에서 기존 상태 관리의 문제점에 대해 이야기한다.

React Hooks 이전에는 모놀리식 상태 라이브러리들이 유행했다.
이는 DX 향상에 큰 도움을 주었지만 사용되지 않는 기능들도 포함된다는 문제가 있었다.
 
1. Form 상태는 글로벌 상태와 별도로 다루어져야 하지만, 단일 상태 솔루션에서는 불가능하다.
2. 서버 캐시 상태는 refetching과 같은 다른 상태들과는 다른 독특한 특징을 가지고 있으나 분리가 불가능하다.
3. 브라우저 네비게이션 상태는 원본값이 브라우저 측에 기반한다는 성질이 있어 단일 상태 솔루션에 적합하지 않다.
 
이런 문제들을 해결하는 것이 React Hooks의 목표 중 하나이다.

위 내용을 요약하면 Hooks 이전의 상태는 상태의 순수함이 결여되어 있었다는 점이 문제라고 꼬집는다.

상태는 전역 상태와 지역 상태가 있다. 지역으로 존재해야 할 데이터들이 전역 스토어에 혼재되어 있고 서버/브라우저 상태가 무분별하게 스토어에 들어 있었다.

React Hooks로 위의 내용들을 해결하고자 한다는 내용이 인상적이었다.

2. useState vs useReducer

React 컴포넌트가 상태를 가지고 있으려면 어떻게 해야 할까?

일반 변수나 전역 변수로 선언하고 사용할 수도 있다. 하지만 해당 값이 변경되었다고 한들 컴포넌트는 리렌더링 되지 않는다.

컴포넌트가 지역 상태의 변경을 감지하고 리렌더링 하려면 useState 혹은 useReducer를 사용해야 한다.

+1. React는 전역 상태를 제공하지 않는다.

// CASE 1.
const [count, setCount] = useState(0);
setCount(1);
setCount(1); // Not render. 동일한 값이므로 렌더링 하지 않는다.
 
// CASE 2.
const [state, setState] = useState({ count: 0 });
setState({ count: 1 });
setState({ count: 1 }); // Re-render! 주소 참조는 항상 다른 값이 된다.
 
// CASE 3.
state.count = 1;
setState(state); // Not render. state 주소값은 변하지 않았기 때문에 렌더링 하지 않는다.
 
// CASE 4.
setCount(count + 1);
setCount(count + 1); // 동일하게 두 번 호출되면 +2가 아닌 +1만 될 수 있다. 이를 해결하기 위해선 함수 업데이트가 필요하다.
 
// CASE 5.
setCount((prev) => prev + 1); // 함수로 작성하게 될 경우 아무리 빠르게 눌러도 횟수만큼의 업데이트가 될 것을 보장한다. 이는 내부적으로 함수를 연속적으로 호출하기 때문이다.
 
// CASE 6.
setCount((prev) => prev); // 결과값이 직전과 동일하기 때문에 리렌더링이 일어나지 않는다.
 
/**
 * init 함수는 useState를 호출하기 전에 실행되지 않는다(lazy initialize).
 * 이는 컴포넌트가 마운트 될 때 한 번만 호출함을 뜻한다.
 **/
const init = () => 0;
const [count, setCount] = useState(init);

useState의 다양한 사용 사례를 들어 리렌더링이 되는 경우를 설명한다. 또한 지연 초기화 경우를 들어 상태 관리의 최적화에 관해 설명한다.

const init = (count) => ({ count, text: 'hi' });
const reducer = (state, action) => {
	switch(action.type) {
		case 'INCREMENT':
			return { ...state, count: state.count + 1 };
		case 'SET_TEXT':
			if(!action.text) {
				return state; // THIS IS Bailout! 리렌더링 되지 않음.
		return { ... state, text: action.text };
		default:
			throw new Error(`unkown action type`);
    }
  }
}
 
const Component = () => {
  const [state, dispatch] = useReducer(reducer, 10, init);
	return (
		<div>{state.count}
			<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
			<input value={state.text} onChange={(e) => dispatch({ type: 'SET_TEXT', text: e.target.value })} />

useReducer 부분에는 useState에서는 불가능한, reducer만의 특별한 기능들을 언급하며 왜 useState는 useReducer로 대체 가능하지만, 역은 안되는지에 대해 설명한다.

useState와 다르게 useReducer에서 인라인으로 함수를 선언하면 사이드 이펙트가 발생한다거나(useReducer는 렌더링 단계에서 reducer를 호출하므로) 복잡한 상태 관리를 처리하기 위한 reducer만의 기법들은 좋은 인사이트를 주었다.

3. ContextAPI vs Import

Context를 활용한 상태와 모듈(import) 상태에 대한 비교는 내가 가지고 있던 전역/지역 상태에 대한 시야를 넓혀주었다.

모듈 상태는 ESM 스코프에 특정 상수 혹은 변수를 정의하는 것을 의미.

export const store = {} 와 같다.

우리는 전역 상태가 앱 전반에서 접근할 수 있는 상태라고 알고 있다. 그런데 이 "앱 전반"은 "React 외부에서 접근"한다는 것까지 포함해야 한다.

ContextAPI로 작성된 전역 상태(root provider)는 React 외부에서 접근이 불가능하다. 하지만 모듈로 작성된 코드는 외부에서 접근이 가능하다.

또한 Context는 기본적으로 싱글턴 패턴을 위해 디자인되지 않았다. 여러 프로바이더에서 사용될 수 있으며 여러 서브 트리에서 다양한 상태로 존재할 수 있다. 하지만 모듈 상태는 싱글턴으로 존재한다. 따라서 단일 전역 상태를 위해서는 모듈 상태를 사용해야 한다. 이는 인메모리에 올라간 단일 변수로 취급되기 때문이다.

4. Zustand vs Jotai vs Valtio

Zustand Jotai Valtio
상태의 위치 Module React Component Module
상태의 형태 Immutable Immutable Mutable
상태 변경 전략 Selector ContextAPI Proxy
재사용성 모듈 상태는 재사용이 까다롭다(싱글턴). 재사용을 위해 결국 Context를 쓰게 된다 Provider를 통해 쉽게 재사용이 가능하다 Zustand와 동일
코드 학습량 셀렉터에 대한 이해가 있어야 한다 Context 및 Atoms에 대한 이해가 있어야 한다 순수 자바스크립트로 이루어져 있어 학습량이 거의 없다
리렌더링 최적화 개발자가 셀렉터를 잘 써야 한다 한다 개발자가 Atom 단위를 적절하게 활용해야 한다 Proxy가 자동으로 해준다
비고 셀렉터 최적화, 객체 참조와 메모이제이션에 익숙하다면 편하게 단일 스토어를 사용할 수 있다 Context 기반이므로 Jotai에서 가능한 것들은 Context에서도 가능하다. React LifeCycle과 공존하므로 예측 가능하다(Suspense 지원 등) 자동 리렌더링 최적화 및 순수 JS 기반이라 편하게 사용 가능하다. 불변성을 위해 코드가 복잡해지지 않아도 된다. 하지만 디버깅 과정이 어렵다

저자가 직접 만든 상태 관리 라이브러리들을 비교 하는 부분은 이 책의 하이라이트라고 생각한다.

왜 여러 상태 라이브러리를 만들수 밖에 없었는지, 각 라이브러리의 리렌더링 회피 전략과 차이를 설명한다. 또한 공통점도 설명하는데 세 라이브러리 모두 "코드량이 적다".

저자가 가장 중요하게 생각하는 포인트라고 생각한다.

맺으며

기본적으로 React를 어느정도 이해하고 있는 개발자를 대상으로 작성된 책이지만 코드 자체가 어렵진 않아서 초심자도 읽어볼 만하다고 생각한다.

책을 읽으면서 상태 관리에 대해 시야가 넓어질 수 있었다.

다음 월간 다이브에는 "상태"를 주제로 해보려고 한다. 책을 통해 배운 것들을 잘 풀어보고 싶다.

상태 관리의 종류와 기법들에 대해 이해하고 싶다면 추천하고 싶은 책이다.

📮 이 포스트에 관심 있으신가요? 이슈를 남겨주세요! 👍
☕ 소주 한 잔 후원하기
(예금주: 이상철)tosskakao