Context API vs Zustand: 보일러플레이트 없는 전역 상태 관리

크리스가 만든 쇼핑몰 앱의 컴포넌트 구조가 깊어지기 시작했다. 최상위 App 컴포넌트에 있는 user 정보를 저 깊은 곳에 있는 Profile 컴포넌트까지 전달해야 한다.
// ❌ Props Drilling의 지옥
<App user={user}>
<Layout user={user}>
<Header user={user}>
<UserProfile user={user} /> {/* 드디어 도착! */}
</Header>
</Layout>
</App>중간에 있는 Layout이나 Header는 user 정보가 필요 없는데도, 오직 자식에게 넘겨주기 위해 Props를 받고 있다. 코드는 지저분해지고, 유지보수는 고통스러워진다. 이것이 악명 높은 Props Drilling이다.
이 문제를 해결하기 위해 우리는 전역 상태(Global State)를 도입한다. 데이터가 컴포넌트 트리를 타고 내려가는 것이 아니라, 필요한 곳에서 즉시 꺼내 쓸 수 있게 만드는 것이다. 리액트 내장 기능인 Context API와 최근 가장 핫한 라이브러리인 Zustand를 비교해 보자.
1. Context API: 리액트의 기본 무기
리액트는 별도의 설치 없이 전역 상태를 만들 수 있는 Context API를 제공한다.
사용법
createContext로 공간을 만들고, Provider로 감싸주면 된다.
// UserContext.tsx
import { createContext, useContext, useState } from 'react';
// 1. 컨텍스트 생성
const UserContext = createContext<User | null>(null);
export function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
return (
// 2. Provider로 값 주입
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
// 3. 필요한 곳에서 사용
export function UserProfile() {
const { user } = useContext(UserContext); // Props 없이 바로 접근!
return <div>{user?.name}</div>;
}치명적인 단점: 불필요한 렌더링
Context API는 태생적으로 "상태 관리 도구"라기보다는 "주입(Dependency Injection) 도구"에 가깝다.
만약 UserContext 안에 user 정보와 theme 정보가 같이 들어있다고 가정해보자. theme만 바뀌어도, user만 사용하는 컴포넌트까지 강제로 리렌더링 된다. Provider가 전달하는 value 객체가 새로 만들어지기 때문이다. 이를 막으려면 컨텍스트를 잘게 쪼개거나 useMemo를 덕지덕지 발라야 한다.
2. Redux의 피로감과 Zustand의 등장
과거에는 이 문제를 해결하기 위해 Redux를 썼다. 하지만 Redux는 코드를 한 줄 짜기 위해 파일을 3개(action, reducer, store)나 만들어야 했다. 너무 복잡하고 장황하다(Boilerplate).
그래서 등장한 것이 Zustand다. 독일어로 '상태'라는 뜻이다.
Zustand의 철학: 작고, 빠르고, 쉽다
Zustand는 Redux의 장점(단방향 데이터 흐름)은 가져오고, 복잡함은 걷어냈다. Provider로 감쌀 필요조차 없다.
// useStore.ts
import { create } from 'zustand';
interface UserState {
user: User | null;
login: (user: User) => void;
logout: () => void;
}
// 스토어 생성 (이게 끝이다!)
export const useUserStore = create<UserState>((set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
}));컴포넌트에서 사용하기 (Selector)
Zustand의 가장 큰 장점은 Selector를 통해 필요한 상태만 구독할 수 있다는 점이다.
// UserProfile.tsx
import { useUserStore } from './useStore';
export function UserProfile() {
// state.user가 바뀔 때만 리렌더링 된다.
// login, logout 함수가 바뀌어도 이 컴포넌트는 영향받지 않는다.
const user = useUserStore((state) => state.user);
return <div>{user?.name}</div>;
}3. Context API vs Zustand: 승자는?
크리스는 프로젝트의 성격에 따라 도구를 다르게 선택한다.
Context API를 써야 할 때
Zustand를 써야 할 때
핵심 정리
이것으로 Part 2. 리액트 동작 원리와 상태 관리가 끝났다.
리액트 내부 동작을 마스터했으니, 이제 프론트엔드 개발의 가장 큰 난제인 "서버 데이터(Server State)"를 다루러 갈 시간이다. 서버 데이터는 로컬 상태와는 완전히 다른 방식으로 다뤄야 한다.
Part 3의 첫 번째 주제, "useEffect vs TanStack Query: 왜 데이터를 useEffect로 가져오면 안 될까?" 편에서 계속된다.