Skip to content

Commit

Permalink
docs: 리액트 훅을 활용한 마이크로 상태 관리
Browse files Browse the repository at this point in the history
  • Loading branch information
yujiseok committed Nov 24, 2024
1 parent c7ce1bc commit 835de8b
Showing 1 changed file with 256 additions and 0 deletions.
256 changes: 256 additions & 0 deletions content/react-micro-hook.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
---
title: 리액트 훅을 활용한 마이크로 상태 관리
publishedAt: 2024-11-24
summary: 책을 읽고 배운점을 정리합니다.
---

# 전통적인 상태 관리와 리액트 훅

전통적인 상태 관리란 상태 관리를 위한 프레임워크(`Redux/toolkit`, `MobX` 등)를 사용해 목적에 맞게
상태를 관리하는 것을 말합니다.

상태 관리를 위한 기본적인 훅(`useState`, `useReducer` 등)을 리액트 내에서 사용할 수 있게 되면서, 로직의 재사용과 경량화가 가능해졌습니다.
그 결과 훅을 사용한 **마이크로 상태 관리**가 가능해졌습니다.

리액트는 지역 상태를 제어할 수 있는 다양한 기능을 이미 제공하지만,
전역 상태를 제어하기엔 까다롭습니다.

> 💡 왜 전역 상태 관리가 어려울까?
>
> 리액트의 단방향 데이터 흐름이 전역 상태 관리를 어렵게 만드는 게 아닐까 생각해 봅니다.
> 데이터가 위에서 아래(부모에서 자식)로 흐르는 구조로, 계층 구조가 깊어질수록 데이터를 전달하는 것과 그 데이터를 추적하는 것이 어렵습니다.
# 마이크로 상태 관리란?

리액트에서 상태는 UI를 나타내는 데이터입니다.
상태는 시간에 따라 변경되고, 리액트는 상태와 함께 렌더리할 컴포넌트를 처리합니다.

기존의 전통적인 중앙 집중형 상태 관리 라이브러리를 통해 상태를 관리할 수 있지만,
상태 관리를 위한 코드가 비대해집니다. 리액트의 훅을 사용해 목적에 맞는 상태 관리 해결책을 제공할 수 있습니다.
목적에 맞는 상태 관리 해결책을 제공하는 라이브러리들로 폼 관리를 위한 `React Hook Form`, 비동기 상태 관리를 위한 `TanStack Query` 등이 존재합니다.

이처럼 특정한 목적을 가지며 범용적으로 사용할 수 있는 상태를 관리를 **마이크로 상태 관리**라고 합니다.

## 마이크로 상태 관리의 특징

- 가벼움: 범용적인 상태 관리 방법은 가볍게 구현되어야 합니다.
- 유연성: 개발자의 요구사항에 따라 적절한 방법을 선택할 수 있어야 합니다. → 자유도
- 지역성 중시: 컴포넌트 모델에 기반해 컴포넌트 간 격리와 재사용을 강조합니다.

마이크로 상태 관리는 즉 개발자에게 자유도가 높은 오픈 월드 게임과 같습니다.

필수 요소:

- 상태 읽기
- 상태 쓰기
- 상태 기반 렌더링

추가 요소:

- 리렌더링 최적화
- 다른 시스템과의 상호 작용
- 비동기 지원
- 파생 상태
- 간단한 문법 → 러닝 커브 낮음

# 지역 상태 사용하기

리액트 컴포넌트는 트리 구조를 구성하므로, 지역 상태를 만드는 것이 쉽다.
지역 상태 생성 → 컴포넌트, 자식 컴포넌트 상태 사용 → 재사용과 지역성에 대한 이점을 얻을 수 있습니다.

이처럼 상태가 컴포넌트 내에서만 사용되고 다른 컴포넌트에 영향을 주지 않을 경우, 억제되었다고 할 수 있습니다.

지역 상태를 효과적으로 사용하기 위해서 도입할 수 있는 방법은 두 가지입니다.

## 상태 끌어올리기

부모 컴포넌트에서 상태를 만들어 자식 컴포넌트에 전달하는 방법입니다.
상태를 부모 컴포넌트로 끌어올렸기에, 상태가 변경될 경우 하위 모든 자식 요소가 리렌더링 됩니다.

→ 개별적으로 상태를 사용하는 것이 아닌, 부모 컴포넌트에서 하위 계층으로 공유하기 위해서 사용합니다.

## 내용 끌어올리기

상태에 의존하지 않는 컴포넌트가 존재할 경우, 리렌더링이 필요 없습니다.
리렌더링에 의존되지 않는 컴포넌트들을 `children` 프랍을 통해 전달하는 방법입니다.

> 💡 왜 `children` 프랍은 부모의 상태가 변경되어도 리렌더 되지 않을까?
>
> - 실제 계층 관계에서 봤을 때, 부모 - 자식 관계로 children 프랍이 트리에 포함되어 있으나 단순히 렌더의 결과물을 전달해서
> - 실제 부모의 마운트가 완료될 때까지, 자식은 마운트 되지 않음
> - 실제 자식 컴포넌트가 트리에 위치하기 전까지 평가되지 않음
> - 클라이언트 컴포넌트 내에서 서버 컴포넌트를 쓰기 위해 프로바이더를 만드는 것과 비슷한 이치일까? 고민
# 전역 상태 사용하기

## 전역 상태란?

하나의 컴포넌트에 속해있지 않고 여러 컴포넌트에서 공통으로 사용되는 상태를 전역 상태라고 합니다.

두 가지 상황에서 전역 상태를 사용합니다.

- 프랍 드릴링이 발생할 때
- 이미 리액트 외부에 존재하는 상태를 사용할 때 → 브라우저 스토리지, 서버 데이터 등 리액트에 의존하지 않고 취득한 정보

## 전역 상태와 싱글턴

지역 상태를 관리하기 위해선 리액트의 훅인 `useState`, `useReducer`를 사용합니다.

전역 상태는 컴포넌트 간 계층이 먼 경우 여러 컴포넌트에서 사용하는 상태를 말합니다.
전역 상태는 반드시 **싱글턴**일 필요 없으며, 여러 컴포넌트에서 사용되는 공유 상태를 말합니다.

> 📦 싱글턴?
> 싱글턴 패턴은 장난감 상자와 같습니다. 장난감 상자는 다음과 같이 동작합니다.
>
> 1. 상자는 오직 하나: 아무리 많은 상자를 원해도 항상 동일한 상자를 받습니다.
> 2. 모두 같은 상자를 공유: 나 너 혹은 우리 모두 똑같은 상자를 받습니다.
> 3. 상자 안의 물건은 모두의 것: 내가 상자에 장난감을 넣었다면, 다른 사람도 장난감을 가지고 놀 수 있습니다.
>
> 모든 사람이 같은 정보를 통해 같은 내용을 알 수 있습니다.
싱글턴 패턴은 한 번의 초기화를 통해 전역에서 접근할 수 있도록 합니다.

# Zustand

Zustand는 모듈 상태를 생성하도록 설계된 작은 라이브러리입니다.
상태 객체를 수정할 수 없고 항상 새로 만드는 불변 갱신 모델 기반이며
선택자를 통해 수동으로 렌더링 최적화를 진행합니다.
store 생성자 인터페이스가 존재합니다.

# Jotai

Jotai는 전역 상태를 위한 작은 라이브러리로 상태를 변경하는 훅과 아톰이라는 것을 사용합니다.
Zustand와 달리 컴포넌트 상태를 사용하며, 공통적으로 불변 상태 모델입니다.

아톰을 통해 의존성 추적 / 리렌더링 감지가 가능합니다.
아톰 자체로 값이 없으므로 재사용 가능이 가능하며, 배열의 경우 atoms-in-atom 기법으로 렌더링 최적화가 가능합니다.

# Valtio

Valtio는 변경 가능한 모델 기반으로 프록시를 사용해 변경 불가능한 스냅샷을 가져옵니다.
프록시 사용으로 리렌더링이 자동으로 최적화됩니다. 프록시를 통해 직접 객체의 값을 변경할 수 있습니다.
상태 사용 추적이라는 기법을 기반하며, 사용된 부분이 변경될 경우에만 컴포넌트 리렌더링 되게 할 수 있습니다.

# 세 가지 전역 상태 라이브러리의 유사점과 차이점

앞서 살펴본 Zustand, Jotai, Valtio는 기존의 전역 상태 관리 라이브러리인 Redux, Recoil, MobX에 대응합니다.

<table>
<thead>
<tr>
<th>마이크로 상태 라이브러리</th>
<th>전역 라이브러리</th>
</tr>
</thead>
<tbody>
<tr>
<td>Zustand</td>
<td>Redux</td>
</tr>
<tr>
<td>Jotai</td>
<td>Recoil</td>
</tr>
<tr>
<td>Valtio</td>
<td>MobX</td>
</tr>
</tbody>
</table>

## Zustand vs Redux

### 공통점

- 두 라이브러리 모두 단방향 데이터 흐름 기반
- 단방향 데이터 흐름에선 상태를 갱신하는 액션 → 액션을 통한 상태 갱신 → 상태가 필요한 곳으로 갱신되는 과정을 거칩니다.

### 차이점

- Redux는 리듀서 기반으로 상태를 갱신합니다.
- 리듀서는 이전 상태와 액션 객체를 받아 새로운 상태를 반환하는 순수 함수입니다.
- 리듀서를 사용할 경우 예측 가능성이 증가합니다.
- Zustand의 경우 반드시 사용할 필요 없습니다.

- Redux의 경우 디렉토리 구조를 어느 정도 강제합니다. (프레임워크 같은 느낌 - 넥스트)

```bash
src/
├── app/
│ ├── store.js # Redux 스토어 설정 파일
│ └── rootReducer.js # (선택 사항) 여러 slice를 결합하는 루트 리듀서
├── features/ # 기능별 폴더
│ ├── counter/ # 'counter' 기능 관련 폴더
│ │ ├── Counter.js # UI 컴포넌트 파일
│ │ └── counterSlice.js # Redux slice 파일 (상태, 액션, 리듀서 포함)
│ ├── todos/ # 'todos' 기능 관련 폴더
│ │ ├── Todos.js # UI 컴포넌트 파일
│ │ └── todosSlice.js # Redux slice 파일 (상태, 액션, 리듀서 포함)
├── common/ # 공통 유틸리티, 훅, 스타일 등
│ └── utils.js # 유틸리티 함수들
└── index.js # 애플리케이션 진입점
```

이와 같은 폴더 구조를 갖습니다.
반면 Zustand의 경우 구조는 온전히 개발자가 정합니다. (라이브러리 같은 느낌 - 리액트)

- Redux는 기본적으로 `immer`를 통해 불변성을 유지하면서 새로운 상태 객체를 쉽게 만들어줍니다.
- 상태 전파 측면에서 Redux는 컨텍스트를 사용합니다 (프로바이더), Zustand는 모듈 임포트를 사용합니다.
- Redux는 단방향이지만, Zustand는 강제하지 않습니다.

## Jotai vs Recoil

- Recoil은 키 문자열을 갖지만, Jotai는 생략 가능합니다.
- Jotai는 네이밍이 필요 없습니다. 이게 가능한 이유는 Jotai는 기본적으로 `WeakMap`을 활용하고 아톰 객체를 참조하며, Recoil은 객체 참조에 의존하지 않고 키 문자열을 기반으로 합니다.
- 문자열은 직렬화가 가능하고 지속성을 유지할 수 있습니다.(로컬/세션 스토리지)
- Jotai는 통합된 atom 함수를 제공합니다.
- 프로바이더 컴포넌트를 생략할 수 있게 해줍니다. → **정신적인 장벽**을 낮춰 DX 친화적

## Valtio vs MobX

둘의 철학은 다르지만, 개발자가 직접 상태를 변경할 수 있다는 점에서 비슷합니다.
렌더링 최적화의 경우 Valtio는 훅을 MobX는 HOC를 사용합니다.

- MobX는 클래스 기반, Valtio는 프록시 기반으로 상태를 갱신.
- Valtio는 상태 객체에서 함수를 분리하는 패턴을 사용할 수 있습니다.

```ts
const timer = proxy({ secondsPassed: 0 });

export const increase = () => {
timer.secondsPassed += 1;
};

export const reset = () => {
timer.secondsPassed = 0;
};

export const useSecondsPassed = () => useSnapshot(timer).secondsPassed;
```

외부에 갱신 함수를 정의할 수 있습니다. 즉 코드 분할, 최소화, 번들 크기 최적화가 가능합니다.

- 렌더링 최적화의 경우 MobX는 HOC를 사용해 더 예측 가능성 높고, Valtio는 훅 방식을 사용해 동시성 렌더링에 친화적입니다.

## Zustand, Jotai, Valtio 비교

- 상태가 어디에 위치하는가?
리액트에서 상태에 대한 두 가지 방식 - 모듈 상태 - 컴포넌트 상태
모듈 상태는 컴포넌트에 의존하지 않는 상태 / 컴포넌트 상태는 리액트 생명 주기에 생성되고 리액트에 의해 제어되는 상태

Zustand, Valtio는 모듈 상태 기반 / Jotai는 컴포넌트 기반
Jotai 아톰의 예시를 들면 실제 값은 들고 있지 않습니다. → 실제 값은 프로바이더에 존재한다.

- 상태 갱신 스타일은 무엇인가?
Zustand는 불변 상태 기반
`state = { count = state.count += 1 }` 이런 식으로 새로운 객체를 만들어야 합니다.
Valtio는 `state.count += 1`를 사용할 수 있습니다.
불변 상태는 규모가 크고 중첩된 객체의 성능을 향상시킵니다. 또, 리액트 자체가 불변 객체 모델 기반이므로 호환성이 좋습니다.
변경 가능한 상태 모델은 객체가 깊이 중첩되어 있을 경우 편리합니다.

결국 서비스에 맞는 라이브러리를 선택하는 것이 중요하다고 생각됩니다.

개인적으로 Zustand나 Jotai 방식이 마음에 듭니다.
Valtio의 경우 변경 가능한 상태 모델을 기반하여 쉽게 상태 변경을 할 수 있지만, 리액트의 멘탈 모델과 같냐고 물으면 글쎄라고 생각됩니다. 아무래도 리액트에서 객체의 불변성을 유지하는 것이 중요하다고 생각되기 때문입니다.

하지만, 세 라이브러리 모두 전통적인 상태 관리 라이브러리에 비해 코드의 양과 러닝커브가 낮고, 프로바이더 없이 상태를 가져올 수 있어서 정신적 장벽이 낮아진다는 점에서 좋다고 생각합니다.

0 comments on commit 835de8b

Please sign in to comment.