Zustand 면접 질문 2026: React 상태 관리와 베스트 프랙티스 완벽 가이드
2026년 프론트엔드 면접에서 자주 출제되는 Zustand 관련 질문을 종합적으로 다룹니다. Zustand vs Context API, 미들웨어, TypeScript 통합, 테스트 패턴까지 실무 코드 예제와 함께 학습할 수 있습니다.

Zustand는 Context API보다 강력하면서도 Redux만큼 복잡하지 않은 React 애플리케이션 상태 관리 라이브러리로 표준적인 선택지가 되었습니다. 최소한의 API와 TypeScript 우선 설계 덕분에, 특히 중급 및 시니어 레벨의 React 포지션 프론트엔드 면접에서 Zustand 관련 질문이 자주 등장하고 있습니다.
Zustand 면접 질문은 세 가지 영역을 테스트합니다. React 내장 상태에 대한 스토어 패턴의 이해, Zustand가 대안보다 뛰어난 상황에 대한 지식, 그리고 유지보수성을 고려한 스토어 구조화 능력입니다.
지원자가 반드시 알아야 할 Zustand 핵심 개념
2024년 후반에 출시된 Zustand v5에서는 v4로부터 몇 가지 주요 변경 사항이 도입되었습니다. create 함수는 더 이상 별도의 useStore 호출이 필요하지 않으며, persist 미들웨어는 기본적으로 동기식 하이드레이션 모델을 사용합니다. 면접관은 지원자가 현재 API를 이해하고 있기를 기대합니다.
Zustand는 useState나 useReducer와 무엇이 다른가요?
Zustand 스토어는 React 컴포넌트 트리 외부에 존재합니다. 이 아키텍처 차이는 세 가지 실질적인 결과를 가져옵니다.
- Context Provider 없이도 컴포넌트의 마운트와 언마운트를 거쳐 상태가 유지됩니다
- 업데이트 시 부모 컴포넌트의 리렌더링을 트리거하지 않고, 구독한 컴포넌트만 리렌더링됩니다
- React 외부에서 동기적으로 상태에 접근할 수 있어 이벤트 핸들러와 비동기 로직에 유용합니다
import { create } from 'zustand'
interface CartStore {
items: CartItem[]
addItem: (item: CartItem) => void
clearCart: () => void
totalPrice: () => number
}
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
clearCart: () => set({ items: [] }),
totalPrice: () => get().items.reduce((sum, item) => sum + item.price, 0)
}))get 함수는 현재 상태에 대한 동기적 접근을 제공하여, useState에서 흔히 발생하는 stale closure 문제를 방지합니다.
Zustand는 리렌더링을 어떻게 처리하나요?
Zustand는 기본적으로 얕은 동등성(shallow equality)을 사용합니다. 컴포넌트가 useCartStore()로 스토어를 구독하면 전체 상태 객체를 받고, 모든 변경에 리렌더링됩니다. 특정 슬라이스를 선택하면 불필요한 리렌더링을 방지할 수 있습니다.
// 나쁜 예: 스토어의 모든 변경에 리렌더링
const { items } = useCartStore()
// 좋은 예: items가 변경될 때만 리렌더링
const items = useCartStore((state) => state.items)
// 여러 값: shallow 비교 사용
import { shallow } from 'zustand/shallow'
const { items, totalPrice } = useCartStore(
(state) => ({ items: state.items, totalPrice: state.totalPrice() }),
shallow
)zustand/shallow의 shallow comparator는 객체 자체가 아닌 객체의 프로퍼티에 대해 참조 동등성을 수행합니다.
Zustand vs Context API: 언제 무엇을 선택해야 하는가
이 비교 질문은 상태 관리를 다루는 거의 모든 면접에서 등장합니다. 정답은 업데이트 빈도와 상태의 복잡성에 따라 달라집니다.
| 기준 | Context API | Zustand |
|---|---|---|
| 번들 크기 | 0 KB (내장) | 1.2 KB gzip |
| 리렌더링 제어 | memo/useMemo로 수동 | 내장 셀렉터 |
| DevTools | React DevTools만 | Redux DevTools 호환 |
| 서버 컴포넌트 | 완전 지원 | 클라이언트 경계 필요 |
| 비동기 상태 | 래퍼 필요 | 네이티브 지원 |
Context API는 테마나 로케일처럼 업데이트 빈도가 낮은 값에 적합합니다. Zustand는 폼 입력, 실시간 데이터, 장바구니 등 상태 업데이트가 자주 발생하는 경우에 탁월합니다.
Zustand 스토어에는 "use client" 디렉티브가 필요합니다. React Server Components를 광범위하게 사용하는 애플리케이션에서는 Zustand 상태가 클라이언트 컴포넌트 내에만 존재하게 됩니다. 하이브리드 아키텍처에서는 서버에서 페치한 데이터를 props로 클라이언트 컴포넌트에 전달한 후 Zustand와 동기화하는 것이 일반적입니다.
Zustand 미들웨어 패턴
미들웨어 질문은 지원자가 "설정보다 구성"을 이해하고 있는지 테스트합니다. Zustand 미들웨어는 스토어 생성자 함수를 래핑하여 코어 API를 수정하지 않고 동작을 추가합니다.
persist 미들웨어는 어떻게 동작하나요?
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface UserPreferences {
theme: 'light' | 'dark'
language: string
setTheme: (theme: 'light' | 'dark') => void
}
export const usePreferencesStore = create<UserPreferences>()(
persist(
(set) => ({
theme: 'light',
language: 'en',
setTheme: (theme) => set({ theme })
}),
{
name: 'user-preferences',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ theme: state.theme, language: state.language })
}
)
)partialize 옵션은 함수와 파생 상태를 영속화에서 제외합니다. 이것이 없으면 직렬화가 실패하거나 불필요한 데이터로 스토리지가 비대해집니다.
여러 미들웨어 조합하기
미들웨어는 안쪽에서 바깥쪽으로 구성됩니다. 가장 안쪽의 미들웨어가 먼저 실행됩니다.
import { create } from 'zustand'
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
export const useStore = create<StoreState>()(
devtools(
persist(
subscribeWithSelector(
immer((set) => ({
// 상태와 액션
}))
),
{ name: 'app-storage' }
),
{ name: 'AppStore' }
)
)immer 미들웨어는 불변 상태를 생성하는 가변 스타일 업데이트를 가능하게 합니다. subscribeWithSelector 미들웨어는 React 컴포넌트 외부에서 상태 슬라이스를 구독할 수 있게 합니다.
React / Next.js 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
TypeScript 통합 패턴
Zustand v5에서는 TypeScript 추론이 크게 개선되었습니다. 이중 함수 호출 create<Type>()() 패턴은 미들웨어 체인에 대한 완전한 타입 추론을 제공합니다.
다른 상태를 참조하는 액션의 타입 지정
interface TodoStore {
todos: Todo[]
filter: 'all' | 'active' | 'completed'
addTodo: (text: string) => void
toggleTodo: (id: string) => void
filteredTodos: () => Todo[]
}
export const useTodoStore = create<TodoStore>()((set, get) => ({
todos: [],
filter: 'all',
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: crypto.randomUUID(), text, completed: false }]
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
filteredTodos: () => {
const { todos, filter } = get()
switch (filter) {
case 'active': return todos.filter((t) => !t.completed)
case 'completed': return todos.filter((t) => t.completed)
default: return todos
}
}
}))파생 상태를 getter가 아닌 함수로 정의하면 오래된 값을 피할 수 있습니다. filteredTodos()를 호출할 때마다 현재 상태를 읽습니다.
대규모 애플리케이션을 위한 스토어 분리
면접에서 자주 나오는 질문은 Zustand를 확장하기 위한 구조화 방법입니다. 슬라이스 패턴은 스토어를 도메인 모듈로 분리합니다.
import { StateCreator } from 'zustand'
export interface AuthSlice {
user: User | null
isAuthenticated: boolean
login: (credentials: Credentials) => Promise<void>
logout: () => void
}
export const createAuthSlice: StateCreator<
AuthSlice & CartSlice, // 결합된 스토어 타입
[],
[],
AuthSlice
> = (set) => ({
user: null,
isAuthenticated: false,
login: async (credentials) => {
const user = await authApi.login(credentials)
set({ user, isAuthenticated: true })
},
logout: () => set({ user: null, isAuthenticated: false })
})import { create } from 'zustand'
import { createAuthSlice, AuthSlice } from './slices/authSlice'
import { createCartSlice, CartSlice } from './slices/cartSlice'
export const useStore = create<AuthSlice & CartSlice>()((...args) => ({
...createAuthSlice(...args),
...createCartSlice(...args)
}))슬라이스는 StateCreator에 전달되는 결합된 스토어 타입을 통해 서로 참조할 수 있습니다.
일반적인 면접 질문과 모범 답변
Zustand가 적합하지 않은 경우는?
Zustand는 다음 시나리오에서 복잡성만 추가하고 이점이 없습니다.
- 런타임에 변경되지 않는 정적 설정 데이터
- React Hook Form 등의 라이브러리로 관리되는 폼 상태
- TanStack Query나 SWR로 처리해야 할 서버 상태
- 2-3레벨 중첩에서의 단순한 부모-자식 props 전달
Zustand를 사용하는 컴포넌트는 어떻게 테스트하나요?
테스트에서는 테스트 간에 스토어 상태를 리셋하고, 선택적으로 스토어 전체를 모킹해야 합니다.
import { act, renderHook } from '@testing-library/react'
import { useCartStore } from './store'
beforeEach(() => {
// 스토어를 초기 상태로 리셋
useCartStore.setState({ items: [], totalItems: 0 })
})
test('addItem increases cart count', () => {
const { result } = renderHook(() => useCartStore())
act(() => {
result.current.addItem({ id: '1', name: 'Product', price: 10 })
})
expect(result.current.items).toHaveLength(1)
})실제 스토어와 상호작용하지 않아야 하는 컴포넌트 테스트에서는 모듈 레벨에서 모킹합니다.
import { vi } from 'vitest'
vi.mock('./store', () => ({
useCartStore: vi.fn(() => ({
items: [{ id: '1', name: 'Mock Product', price: 25 }],
addItem: vi.fn()
}))
}))Zustand는 비동기 작업을 어떻게 처리하나요?
Redux와 달리 Zustand는 비동기를 위한 미들웨어가 필요하지 않습니다. 액션은 직접 async 함수가 될 수 있습니다.
interface ProductStore {
products: Product[]
isLoading: boolean
error: string | null
fetchProducts: () => Promise<void>
}
export const useProductStore = create<ProductStore>((set) => ({
products: [],
isLoading: false,
error: null,
fetchProducts: async () => {
set({ isLoading: true, error: null })
try {
const products = await productApi.getAll()
set({ products, isLoading: false })
} catch (err) {
set({ error: err.message, isLoading: false })
}
}
}))복잡한 비동기 흐름에서는 로딩 및 에러 상태를 해당 데이터와 함께 배치하는 것이 권장됩니다.
면접에 나올 수 있는 Zustand v5 변경사항
Zustand v5에서는 지원자가 최신 정보를 따라가고 있는지 확인하기 위해 면접관이 활용하는 주요 변경사항이 도입되었습니다.
- 기본 export 제거:
create대신{ create }를 import - 더 엄격한 TypeScript:
create<T>()가create<T>()()패턴으로 대체 - 동기식 persist 하이드레이션:
onRehydrateStorage콜백 타이밍 변경 - CJS 지원 종료: ESM 전용 패키지
v4에서 v5로의 마이그레이션에는 import 문 업데이트와 기존 하이드레이션 타이밍에 의존하던 코드 조정이 필요합니다.
Zustand 면접 핵심 포인트
- Zustand 스토어는 React 트리 외부에 존재하여 동기적 접근이 가능하고 Provider 중첩을 방지합니다
- shallow 비교가 포함된 셀렉터는 프로덕션 애플리케이션의 리렌더링을 최적화합니다
- persist 미들웨어는 스토리지에서 함수를 제외하기 위해
partialize가 필요합니다 - 슬라이스 패턴은 타입 안전성을 희생하지 않고 Zustand를 대규모 애플리케이션으로 확장합니다
- 비동기 액션은 Redux 패턴과 달리 미들웨어 없이 직접 동작합니다
- 테스트에서는 테스트 케이스 간에 명시적인 상태 리셋이 필요합니다
- Server Components는 Zustand를 클라이언트 경계로 강제하여 아키텍처 결정에 영향을 줍니다
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
React / Next.js 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 8월 25일 업데이트
태그
공유
관련 기사

Next.js 16 Middleware 완벽 가이드 2026: Edge Runtime, 인증 패턴, 면접 질문
Next.js 16 Middleware의 동작 원리, 인증 패턴, Edge Runtime 제한 사항과 해결 방법, 그리고 면접에서 자주 묻는 질문을 실용적인 코드 예제와 함께 설명합니다.

React 19 Suspense와 Concurrent Rendering 완벽 가이드: Streaming SSR과 면접 대비 2026
React 19의 Suspense, Concurrent Rendering, Streaming SSR의 동작 원리를 상세히 설명합니다. 2026년 프론트엔드 면접에서 자주 나오는 질문과 답변 예시도 포함되어 있습니다.

2026년 React 테스트: Vitest, React Testing Library와 모범 사례
2026년 최신 React 테스트 방법론을 상세히 설명합니다. Vitest, React Testing Library 활용법, 컴포넌트 테스트, 통합 테스트, 기술 면접 대비까지 종합적으로 다룹니다.