diff --git "a/week-05/\352\271\200\353\257\274\355\230\201.md" "b/week-05/\352\271\200\353\257\274\355\230\201.md" new file mode 100644 index 0000000..fa8fbdd --- /dev/null +++ "b/week-05/\352\271\200\353\257\274\355\230\201.md" @@ -0,0 +1,374 @@ +# useState만으로 상태 관리를 하기 어려운 이유 + +React에서 가장 기본적인 상태 관리 방법은 `useState`다. + +`useState`는 컴포넌트 내부에서 상태를 만들고, 해당 상태가 변경되면 컴포넌트를 다시 렌더링한다. + +```ts +function Counter() { + const [count, setCount] = useState(0); + + return ( + + ); +} +``` + +위 코드처럼 하나의 컴포넌트 안에서만 사용하는 상태라면 `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 ( + + ); +} +``` + +위 코드에서는 `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 ( + + ); + } + +위 컴포넌트는 `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 ( + + ); +} +``` + +위 컴포넌트는 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 안에 상태와 액션을 모아두는 중앙 집중형 상태 관리