⚡상태관리의 필요성
상태란 흔히 웹 애플리케이션을 개발할때 이야기하는 상태는 어떠한 의미를 지닌 값이며, 애플리케이션의 시나리오에 따라 지속적으로 변경될 수 있는 값을 의미함 (UI, URL, form, 서버에서 가져온 값)
5.1.1 리액트 상태관리의 역사
🌕Flux 패턴
- 단방향으로 데이터 흐름을 변경하는 것 (양방향 데이터 바인딩 / 코드의 양이 많아지고 변경시나리오가 복잡
💡 Flux패턴의 용어
- action : 어떠한 작업을 처리할 액션과 그 액션이 발생시 함께 포함시킬 데이터를 의미
- dispatcher : 액션을 스토어로 보내는 역할
- store : 실제 상태에 따른 값과 상태를 변경할 수 있는 메서드를 가짐
- view : 리액트의 컴포넌트에 해당하는 부분
양방향 데이터 바인딩은 데이터 흐름은 모두 액션이라는 한 방향으로 줄어드므로 데이터의 흐름을 추적하기 쉽고 코드를 이해하기가 수월해짐
🌕리덕스의 등장
- 리덕스는 대표적인 Flux 패턴을 사용하는 상태관리 라이브러리이고 Elm 아키텍처를 도입
- 리덕스는 하나의 상태 객체를 스토어에 저장해 두고, 이 객체를 업데이트하는 작업을 디스패치해 업데이트를 수행함
🌕Context API와 useContext
🌕훅의 탄생 그리고 React Query와 SWR
- 두 라이브러리 모두 외부에서 데이터를 불러오는 fetch를 관리하는 데 특화된 라이브러리지만, API 호출에 대한 상태를 관리하기 때문에 HTTP 요청에 특화된 상태 관리 라이브러리임
🌕 Recoil, Zustand, Jotai, Vlatio에 이르기까지
- 16.8 이상의 버전을 요구하고 있고 작은 크기의 상태를 효율적으로 관리하기 좋은 라이브러리
5.2.1 가장 기본적인 방법 : useState와 useReducer
- useState의 등장으로 리액트에서는 여러 컴포넌트에 걸쳐 손쉽게 동일한 인터페이스의상태를 생성하고 관리할 수 있게 됨
- useCounter라는 훅을 만들어서 함수형 컴포넌트 어디서든 사용할 수 있게 구현가능
function useCounter(init: number = 0) {
const[counter, setCounter] = useState(init)
function inc() {
setCounter((prev) => prev + 1)
}
return {counter, inc}
}
- 리액트의 훅을 기반으로 만든 사용자 정의 훅은 함수 컴포넌트에서 재사용 가능하다는 장점
- 실제 useReducer를 타입스트립트로 작성하려면 다양한 오버로딩이 필요함
- useState나 useReducer 모두 약간의 구현 상의 차이만 있을 뿐, 두 훅 모두 지역 상태 관리를 위해 만들어짐
5.2.2 지역상태의 한계를 벗어나보자 : useState의 상태를 바깥으로 분리하기
- useState의 한계는 리액트가 만든 클로저 내부에서 관리되어 지역 상태로 생성되기 때문에 해당 컴포넌트에서만 사용할 수 있다는 것
- useState로 컴포넌트의 리렌더링을 실행하여 최신값을 가져오는 방법은 해당 컴포넌트에서만 유효한 전략임 즉, 반대쪽의 다른 컴포넌트에서는 상태 변화에 따른 리렌더링을 일으킬 무언가가 없기 때문에 렌더링되지 않기 때문
💡함수외부에서 상태를 참조하고 렌더링까지 자연스럽게 일어나려면 다음과 같은 조건을 만족해야 함!
1. 컴포넌트 외부 어딘가에 상태를 두고 여러 컴포넌트가 같이 쓸 수 있어야 함
2. 외부에 있는 상태를 사용하는 컴포넌트는 상태의 변화를 알아챌 수 있어야 하고, 상태가 변할 때마다 리렌더링이 일어나서 컴포넌트를 최신 상태값 기준으로 렌더링해야 함. 상태 감지는 상태를 변경시키는 컴포넌트뿐 아니라 상태를 참조하는 모든 컴포넌트에서 동일하게 작동해야 함
3. 상태가 윈시 값이 아닌 객체인 경우, 객체에 내가 감지하지 않는 값이 변한다 하더라도 리렌더링이 발생해서는 안됨
이 3가지를 만족시킬 수 있는 것이 store임
5.2.3 useState와 Context를 동시에 사용해보기
- useState / useStoreSelector훅의 단점은 스토어를 사용하는 구조는 반드시 하나의 스토어만 가지게 된다는 것
- 하나의 스토어를 가지면 해당 스토어는 마치 전역 변수처럼 작동하게 되어 동일한 형태의 여러 개의 스토어를 가질 수 없게 됨!
🌓만약, 훅을 사용하는 서로 다른 스코프에서 스토어의 구조는 동일하되, 여러 개의 서로 다은 데이터를 공유해 사용하고 싶다면 어떻게 해야 할까?
가장 떠오르는 방법은 createStore를 이용해 동일한 타입으로 스토어를 여러개 만드는 것이지만, 이 방법은 완벽하지도 않고, 번거롭기 때문에 이런 문제에서는 리액트의 Context를 사용하는 것이 좋음!
//createStore를 이용해 동일한 타입으로 스토어를 여러개 만드는 것
const store1 = createStore({ count : 0 })
const store2 = createStore({ count : 0 })
const store3 = createStore({ count : 0 })
....
//스토어와 context 함께 사용
export const CounterStoreContext = createContext<Store<CounterStore>>(
createStore<CounterStore>({ count:0, text: 'Hello' })
);
export const CounterStoreProvider = ({
initialState,children
} : PropsWithChildren<{
initialState : CounterStore
}>) => {
const storeRef = useRef<Store<CounterStore>>()
if (!storeRef.current) {
storeRef.current = createStore(initialState)
}
return (
<CounterStoreContext.Provider value={storeRef.current}>
{children}
</CounterStoreContext.Provider>
)
);
- storeRef를 사용하여 스토어를 제공하는데, 그 이유는 Provider가 넘기는 props가 불필요하게 변경돼서 리렌더링되는 것을 막기 위함이고, useRef를 사용했기 때문에 CounterStoreProvider는 최초 렌더링에서만 스토어를 만들어 값을 전달하게 될것임
🌚Context와 Provider를 기반으로 각 store 값을 격리해서 관리했을 때의 장점
- 스토어를 사용하는 컴포넌트는 해당 상태가 어느 스토어에서 온 상태인지 신경 쓰지 않아도 됨
- Context와 Provider를 관리하는 부모 컴포넌트의 입장에서 자신이 자식 컴포넌트에 따라 보여주고 싶은 데이터를 Context로 잘 격리하기만 하면 됨 (부모와 자식 컴포넌트의 책임과 역할을 이름이 아닌 명시적인 코드로 나눌 수 있어 코드 작성이 한결 용이해짐)
💡리액트에서 상태관리와 렌더링에 대해 넓은 시야를 가지게 될려면 코드를 작성할때 고려할 것
- useState, useReducer가 가지고 있는 한계, 컴포넌트 내부에서만 사용할 수 있는 지역 상태라는 점을 극복하기 위해 외부 어딘가에 상태를 둠
- 외부의 상태변경을 각자의 방식으로 감지해 컴포넌트의 렌더링을 일으킴
5.2.4 상태 관리 라이브러리 Recoil, Jotai, Zustand 살펴보기
🌚 페이스북이 만든 상태 관리 라이브러리 Recoil
- 훅의 개념으로 상태관리를 시작한 최초의 라이브러리 중 하나Recoil의 핵심 AP인 RecoilRoot, atom, useRecoilValue, useRecoilState를 살펴보고 Recoil에서는 상태 값을 어디에 어떻게 저장하는 지 알아보자
🌓 RecoilRoot
- Recoil를 사용하기 위해 RecoilRoot를 애플리케이션의 최상단에 선언해야됨
- Recoil에서 생성되는 상태값을 저장하기 위한 스토어를 생성하는 것을 확인 할 수 있음
- 상태값은 RecoilRoot로 생성된 Context의 스토어에 저장됨
- 스토어의 상태값에 접근할 수 있는 함수들이 있으며, 이 함수를 활용해 상태값에 접근하거나 상태값을 변경할 수 있음
- 값의 변경이 발생하면 이를 참고하고 있는 하위 컴포넌트에 모두 알림
🌓 atom
- 상태를 나타내는 Recoil의 최소 상태 단위
- key 값을 필수로 가지며, 애플리케이션 내부에서 유일한 값이어야 함(atom과 selector를 만들때 주의를 요함)
- defalut는 atom의 초깃값을 의미
- atom의 값을 컴포넌트에서 읽어오고 이값의 변화에 따라 컴포넌트를 리렌더링하려면 useRecoilValue와 useRecoilState를 사용하면 됨
🌕 useRecoilValue
atom의 값을 읽어오는 훅
🌕 useRecoilState
useState와 유사하게 값을 가져오고, 이 값을 변경할 수도 있는 훅
- Recoil에서 한 가지 불확실한 점은 정식 버전인 1.0.0의 출시 시점이며, 0.x.x 버전은 다른 주 버전과 다르게 부 버전이 변경돼도 호환성이 깨지는 변경사항이 발생할 수 있는 초기 버전으로 간주되어 라이브러리르 사용할 때에 추가적인 주의가 필요함!
🌚 Recoil에서 영감을 받은, 그러나 조금 더 유연한 Jotai
- Recoil의 atom 모델에 영감을 받아 만들어진 상태 관리 라이브러리
- 리덕스와 같이 하나의 큰 상태를 애플리케이션에 내려주는 방식이 아니라, 작은 단위의 상태를 위로 전파할 수 있는 구조를 취하고 있음을 의미함
- 리액트 Context의 문제점인 불필요한 리렌더링이 일어난다는 문제를 해결하고자 설계되어 있으며, 추가적으로 개발자들이 메모이제이션이나 최적화를 거치지 않아도 리렌더링이 발생되지 않도록 설계되어 있음
- Recoil의 atom 개념을 도입하면서도 API가 간결하다는 점
🌓 atom
- Recoil과 동일하게 최소 상태 단위를 나타내지만, atom 하나로 파생된 상태까지 만들 수 있음
- 고유한 key를 필요로 했던 Recoil과는 다르게 별도의 key를 내려주지 않아도 됨
- Jotai에서 atom에 따로 상태를 저장하고 있지 않음 > userAtomValue에서 상태를 저장해둠
🌓 userAtomValue
- useReducer에서 반환하는 상태값은 3가지: [version, valueFromReducer, atomFromReducer]
- 첫 번째는 store 버전, 두 번째는 atom에서 get을 수행했을 때 반환되는 값, 세 번째는 atom 그 자체를 의미함
- atom의 값은 store에 존재한다는 것을 알 수 있으며, store에 atom 객체 그 자체를 키로 활용해 값을 저장함
- 이러한 방식을 위해 WeakMap이라고 하는, 자바스크립트에서 객체만을 키로 가질 수 있는 독특한 방식의 Map을 활용해 recoil과는 다르게 별도의 key를 받지 않아도 스토어에 값을 저장할 수 있음
💡rerenderIfChanged (리렌더링을 일으키기 위해 사용함)가 일어나는 경우
- 첫째, 넘겨받은 atom이 Reducer를 통해 atom과 달라지는 경우
- 둘째, ubscribe를 수행하고 있다가 어디선가 이 값이 달라지는 경우
- 이러한 로직 덕분에 atom의 값이 어디서 변경되더라도 useAtomValue로 값을 사용하는 쪽에서는 언제든 최신 값의 atom을 사용해 렌더링할 수 있게 됨
🌓 useAtom
- useState와 동일한 형태의 배열을 반환함
- 첫 번째로는 atom의 현재 값을 나타내는 useAtomValue 훅의 결과를 반환하며, 두 번째로는 useSetAtom 훅을 반환함
- 해당 훅은 atom을 수정할 수 있는 기능을 제공함
🌚작고 빠르며 확장에도 유연한 Zustand
- Zustand는 리덕스에 영감을 받아 만들어짐
- 즉, atom이라는 개념으로 최소 단위의 상태를 관리하는 것이 아니라 Zustand에서는 하나의 스토어를 중앙 집중형으로 활용해 이 스토어 내부에서 상태를 관리하고 있음
- 따라서 Zustand에서는 하나의 스토어를 중앙 집중형으로 활용해 이 스토어 내부에서 상태를 관리하고 있음
- Zustand는 특별히 많은 코드를 작성하지 않아도 빠르게 스토어를 만들고 사용할 수 있다는 장점이 있음
'Study > 모던 리액트 Deep Dive' 카테고리의 다른 글
| 모던 리액트 Deep Dive - Next.js (0) | 2025.11.23 |
|---|---|
| 모던 리액트 Deep Dive - 서버 사이드 랜더링을 위한 리액트 API (0) | 2025.11.10 |
| 모던 리액트 Deep Dive - 리액트 훅과 고차 컴포넌트 (0) | 2025.11.01 |
| 모던 리액트 Deep Dive - 리액트 훅 (1) | 2025.10.22 |
| 모던 리액트 Deep Dive - 메모이제이션 (0) | 2025.10.14 |