React 상태 관리
상태(State) = 시간에 따라 변할 수 있고 시스템이 기억해야 하는 값
리액트에서는 “상태가 바뀌면 화면이 바뀐다.” 라는 철학을 가지고 있다.
리액트는 DOM을 직접 바꾸는 게 아니라, 상태를 바꾸고 그 상태를 기준으로 UI를 다시 계산한다.
useState
const [count, setCount] = useState(0);
// 현재 상태 값: count
// 상태를 바꾸는 함수: setCount
// 초기값: 0
count = count + 1; // 상태는 직접 변경할 수 없다.
setCount(count + 1); // 함수를 호출해서 바꿔야한다.
리액트는 상태가 바뀌었는지 확인할 때 기본적으로 참조값을 보기때문에 기존 상태를 직접 수정하지 않고, 새로운 값을 만들어서 넣는다. (=immutability)
리액트는 기본적으로 컴포넌트 트리 구조이며 부모,자식,형제 관계로 부를 수 있다. 데이터는 위에서 아래로 흐른다 (부모 -> 자식). 형제끼지 직접 데이터를 주고받을 수 없으며, 공통 부모를 거쳐야만 한다. 아래에서 위로 전달하는건 까다롭다.
Props = 부모가 자식에게 내려주는 값
STATE COLOCATION
상태는 그 상태를 사용하는 곳에 최대한 가깝게 둔다.
LIFTING STATE UP
여러 자식이 같은 상태를 필요로 하면, 그 상태를 가장 가까운 공통 부모로 옮긴다.
만약 구조가 더 복잡해진다면?
App
├── Layout
│ └── Header
│ └── Navigation
│ └── AuthArea
│ └── LoginButton
└── LoginModal
# App → Layout → Header → Navigation → AuthArea → LoginButton
중간 컴포넌트들이 실제로는 그 Props를 사용하지도 않는데 계속 전달해야한다.
PROPS DRILLING
중간 컴포넌트들이 실제로 쓰지도 않는 Props를 아래로 계속 전달하는 상황
단점
-
중간 컴포넌트가 불필요한 정보를 알아야 한다.
-
구조 변경이 귀찮아진다.
-
타입스크립트에서는 props 타입이 불필요하게 퍼진다.
위 단점을 해결하기 위해서는 컴포넌트 구조를 변경하거나 Context 또는 전역 상태 라이브러리인 Zustand, Reducx, Jotai 를 사용해야 한다.
CONTEXT API
props를 여러 단계로 직접 전달하지 않고, 아래 컴포넌트들이 공통 값을 거내 쓸 수 있게 해주는 React 내장 기능
Context는 값을 전달하는 기능이다.
App
↓ user
Layout 안 씀, 전달만 함
↓
Header 안 씀, 전달만 함
↓
Navigation 안 씀, 전달만 함
↓
UserMenu 실제 사용
이럴 때 Context를 쓰면 중간 전달을 생략할 수 있다.
import { createContext, useContext } from "react"
// 통로 만들기
const UserContext = createContext(null)
function App() {
const user = { name: "Sungjun" }
return (
// 값 넣기
<UserContext.Provider value={user}>
<Profile />
</UserContext.Provider>
)
}
function Profile() {
// 값 꺼내기
const user = useContext(UserContext)
return <p>{user.name}</p>
}
Context에 너무 많은 정보를 넣으면 나중에 관리하는게 힘들다. 복잡한 경우라면 Zustand 를 사용하자.
자주 바뀌는 값은 Context에 신중하게 넣어야 한다.
서버 데이터는 Context에 넣지 말자. 캐싱, refetching, stale time, mutaion 후 갱신 같은 걸 자동으로 해주지 않기 때문이다. 이 경우에는 TanStack Query가 더 적합하다.
ZUSTAND
Zustand = 리액트 컴포넌트 밖에 전역 store를 만들고, 필요한 컴포넌트가 그 store에서 필요한 값만 꺼내 쓰는 라이브러리
import { create } from "zustand";
type CounterStore = {
count: number;
increase: () => void;
decrease: () => void;
reset: () => void;
};
export const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increase: () => {
set((state) => ({
count: state.count + 1,
}));
},
decrease: () => {
set((state) => ({
count: state.count - 1,
}));
},
reset: () => {
set({
count: 0,
});
},
}));