Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
374 changes: 374 additions & 0 deletions week-05/김민혁.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,374 @@
# useState만으로 상태 관리를 하기 어려운 이유

React에서 가장 기본적인 상태 관리 방법은 `useState`다.

`useState`는 컴포넌트 내부에서 상태를 만들고, 해당 상태가 변경되면 컴포넌트를 다시 렌더링한다.

```ts
function Counter() {
const [count, setCount] = useState(0);

return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
```

위 코드처럼 하나의 컴포넌트 안에서만 사용하는 상태라면 `useState`만으로 충분하다.

하지만 실제 애플리케이션에서는 하나의 상태를 여러 컴포넌트가 함께 사용해야 하는 경우가 많다.

로그인한 사용자 정보
테마 정보
알림 목록
모달 열림 여부

예를 들어 로그인한 사용자 정보가 `Header`, `Sidebar`, `MyPage`에서 모두 필요하다고 해보자.

이 상태를 `useState`로만 관리하려면 공통 부모 컴포넌트에 상태를 두고 props로 내려줘야 한다.

App
├─ Header
├─ Sidebar
└─ MyPage

상태를 사용하는 컴포넌트가 많아질수록 props를 여러 단계로 전달해야 하고, 상태 변경 로직도 여러 컴포넌트에 흩어질 수 있다.

이런 문제를 해결하기 위해 상태를 컴포넌트 내부가 아니라 컴포넌트 외부의 store에서 관리하는 방식이 등장했다.

useState
└─ 컴포넌트 내부에서 상태 관리

상태 관리 라이브러리
└─ 컴포넌트 외부 store에서 상태 관리

하지만 상태를 외부 store에 두면 새로운 문제가 생긴다.

외부 store의 값이 바뀌었을 때 React는 그 변경을 자동으로 알 수 없다.

따라서 외부 상태의 변경을 React 컴포넌트에 알려주는 구조가 필요하다.

이때 필요한 개념이 `subscribe` 구조다.

* * *

# subscribe 구조와 상태 관리

## subscribe 구조란?

`subscribe`는 특정 상태의 변경을 구독하는 구조를 의미한다.

React 컴포넌트 내부에서 `useState`를 사용하면 상태 변경을 React가 직접 알고 있기 때문에 `setState`가 호출될 때 자동으로 리렌더링이 발생한다.

```ts
function Counter() {
const [count, setCount] = useState(0);

return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
```

위 코드에서는 `setCount`가 호출되면 React가 `count` 상태의 변경을 알고 컴포넌트를 다시 렌더링한다.

하지만 상태가 React 컴포넌트 외부에 있다면 이야기가 달라진다.

```ts
let count = 0;

function increase() {
count += 1;
}
```

위 코드에서 `count` 값이 변경되어도 React는 이 값이 변경되었는지 알 수 없다.

React가 알 수 있는 상태 변경은 `useState`, `useReducer`와 같이 React 내부에서 관리되는 상태 변경이다.

따라서 외부에 있는 상태가 변경되었을 때 React 컴포넌트에게 알려주는 구조가 필요하다.

이때 필요한 구조가 `subscribe` 구조다.

* * *

## subscribe 구조의 기본 흐름

`subscribe` 구조는 다음과 같은 흐름으로 동작한다.

컴포넌트가 store를 구독한다.
store의 상태가 변경된다.
store가 구독 중인 컴포넌트에게 상태 변경을 알린다.
컴포넌트가 새로운 상태를 읽고 다시 렌더링된다.

즉, `subscribe`는 상태가 변경되었을 때 해당 상태를 사용하고 있는 쪽에 변경 사실을 알려주는 역할을 한다.

간단한 store를 직접 작성하면 다음과 같다.

let state = {
count: 0,
};

const listeners = new Set<() => void>();

function getState() {
return state;
}

function setState(nextState) {
state = {
...state,
...nextState,
};

listeners.forEach((listener) => listener());
}

function subscribe(listener: () => void) {
listeners.add(listener);

return () => {
listeners.delete(listener);
};
}

여기서 핵심은 세 가지다.

getState → 현재 상태를 읽는다.
setState → 상태를 변경한다.
subscribe → 상태 변경을 구독한다.

`setState`가 호출되면 상태만 변경하는 것이 아니라, `listeners`에 등록된 함수들을 실행한다.

function setState(nextState) {
state = {
...state,
...nextState,
};

listeners.forEach((listener) => listener());
}

이 과정을 통해 외부 상태의 변경을 React 컴포넌트와 연결할 수 있다.

* * *

# atom 기반 상태 관리

## atom이란?

`atom`은 상태를 작은 단위로 쪼개서 관리하는 방식이다.

Jotai나 Recoil 같은 라이브러리에서 사용하는 개념이다.

예를 들어 Jotai에서는 다음과 같이 atom을 만든다.

import { atom } from 'jotai';

const countAtom = atom(0);
const userAtom = atom(null);

`countAtom`은 `count`라는 하나의 상태 단위를 의미하고, `userAtom`은 `user`라는 하나의 상태 단위를 의미한다.

즉, atom 기반 상태 관리는 하나의 큰 store 객체 안에 모든 상태를 넣는 방식이라기보다, 상태를 여러 개의 작은 조각으로 나누어 관리하는 방식이다.

countAtom
└─ count 상태 관리

userAtom
└─ user 상태 관리

themeAtom
└─ theme 상태 관리

다만 atom 자체가 실제 값을 직접 들고 있는 것은 아니다.

atom은 상태의 정의에 가깝고, 실제 값은 내부 store에서 관리된다.

* * *

## atom 기반 상태 관리의 특징

atom 기반 상태 관리는 필요한 상태 단위만 구독할 수 있다는 장점이 있다.

function Counter() {
const [count, setCount] = useAtom(countAtom);

return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}

위 컴포넌트는 `countAtom`을 사용한다.

따라서 `countAtom`의 값이 변경되면 이 컴포넌트가 다시 렌더링된다.

반대로 `userAtom`이나 `themeAtom`이 변경되어도 이 컴포넌트는 영향을 받지 않는다.

Counter 컴포넌트
└─ countAtom 구독

Profile 컴포넌트
└─ userAtom 구독

ThemeButton 컴포넌트
└─ themeAtom 구독

이처럼 atom 기반 상태 관리는 상태를 작게 나누고, 컴포넌트가 필요한 atom만 구독하는 방식이다.

* * *

# Zustand의 중앙 집중형 store

## Zustand는 하나의 store 안에서 상태를 관리한다

Zustand는 atom 기반 상태 관리와는 다르게 하나의 store 안에 여러 상태와 상태 변경 함수를 함께 둘 수 있다.

```ts
import { create } from 'zustand';

const useStore = create((set) => ({
count: 0,
user: null,
theme: 'light',

increase: () => set((state) => ({ count: state.count + 1 })),
login: (user) => set({ user }),
changeTheme: (theme) => set({ theme }),
}));
```

위 코드에서 `count`, `user`, `theme`은 모두 하나의 store 안에서 관리된다.

그리고 `increase`, `login`, `changeTheme`과 같은 상태 변경 함수도 같은 store 안에 들어 있다.

Zustand store
├─ count
├─ user
├─ theme
├─ increase()
├─ login()
└─ changeTheme()

이런 구조를 중앙 집중형 store라고 볼 수 있다.

상태와 상태를 변경하는 로직이 하나의 store 안에 모여 있기 때문에, 특정 도메인이나 기능의 상태 흐름을 한 곳에서 파악하기 쉽다.

* * *

## Zustand에서 컴포넌트는 필요한 값만 구독한다

Zustand가 하나의 store를 사용한다고 해서 모든 컴포넌트가 store 전체를 구독해야 하는 것은 아니다.

```ts
function Counter() {
const count = useStore((state) => state.count);
const increase = useStore((state) => state.increase);

return (
<button onClick={increase}>
{count}
</button>
);
}
```

위 컴포넌트는 store 안의 `count`와 `increase`만 사용한다.

Counter 컴포넌트
└─ count 구독
└─ increase 사용

따라서 `user`나 `theme`이 변경되어도 `Counter`가 반드시 다시 렌더링될 필요는 없다.

즉, Zustand의 구조는 다음과 같이 이해할 수 있다.

하나의 store에 상태와 액션을 모아둔다.
컴포넌트는 selector로 필요한 값만 선택한다.
선택한 값이 변경되면 해당 컴포넌트가 다시 렌더링된다.

* * *

# atom 방식과 Zustand 방식 비교

## atom 방식

atom 방식은 상태를 작은 단위로 쪼개서 관리한다.

countAtom
userAtom
themeAtom

각 컴포넌트는 필요한 atom만 사용한다.

Counter → countAtom 사용
UserProfile → userAtom 사용
ThemeButton → themeAtom 사용

이 방식은 상태 단위가 작고 명확하기 때문에, 어떤 컴포넌트가 어떤 상태에 의존하는지 파악하기 쉽다.

하지만 상태가 많아지면 atom의 개수도 많아질 수 있다.

* * *

## Zustand 방식

Zustand는 하나의 store 안에 여러 상태와 액션을 함께 관리한다.

useStore
├─ count
├─ user
├─ theme
├─ increase()
├─ login()
└─ changeTheme()

컴포넌트는 store 전체를 사용하는 것이 아니라, selector를 통해 필요한 값만 구독한다.

Counter → state.count
UserProfile → state.user
ThemeButton → state.theme

이 방식은 상태와 상태 변경 로직을 한 곳에 모아둘 수 있어 구조가 단순하다.

하지만 store가 너무 커지면 하나의 파일이나 객체에 너무 많은 책임이 모일 수 있다.

따라서 Zustand를 사용할 때도 기능이나 도메인 기준으로 store를 적절히 나누는 것이 중요하다.

* * *

# 상태 관리 라이브러리를 바라보는 관점

## 상태 관리의 핵심은 상태 저장보다 변경 알림에 가깝다

상태 관리 라이브러리의 핵심은 `store`와 `subscribe` 구조다.

store에 상태를 저장한다.
컴포넌트가 필요한 상태를 구독한다.
상태가 변경되면 구독자에게 알린다.
React가 필요한 컴포넌트를 다시 렌더링한다.

이 관점에서 보면 Redux, Recoil, Jotai, Zustand는 모두 같은 문제를 해결하기 위한 다른 방식이라고 볼 수 있다.

Redux
└─ 하나의 store와 reducer 중심의 상태 관리

Recoil / Jotai
└─ atom이라는 작은 상태 단위 중심의 상태 관리

Zustand
└─ 하나의 store 안에 상태와 액션을 모아두는 중앙 집중형 상태 관리
Loading