서론
Next.js 13 이후로 서버 컴포넌트가 도입되면서 기존의 전역 상태 관리 패턴이 그대로 통하지 않게 됐습니다.
클라이언트 전용 상태 관리 라이브러리(Zustand, Jotai 등)를 서버 컴포넌트와 어떻게 조합해야 할까라는 생각에 찾아봤습니다.
프로젝트를 진행하던 중 이런 상황이 마침 발생해서 직접 적용해봤습니다.
우선 페이지를 이동할 때마다 큰 변화가 없는 데이터에 대한 API 호출은 비효율적이라고 생각했고 한번 불러서 저장해서 사용해보도록 했습니다.
그 중 가장 간단하고 쉽게 할 수 있는 Zustand를 활용하게 됐습니다.
서버 컴포넌트 vs 클라이언트 컴포넌트의 역할
| 구분 | 서버 컴포넌트 | 클라이언트 컴포넌트 |
|---|---|---|
| 실행 위치 | 서버 | 브라우저 |
| 데이터 요청 | 가능 (fetch 직접 사용) |
불가능 (보통 React Query 사용) |
| 상태 관리 | 불가능 | 가능 (useState, Zustand 등) |
"use client" 필요 여부 |
❌ | ✅ |
Zustand Store 정의
import { create } from 'zustand'
export interface LottoResult {
round: number
date: string
numbers: number[]
bonus: number
}
export interface Stat {
number: number
freq: number
}
interface LottoStore {
latestResult: LottoResult | null
recentStats: Stat[]
last50Rounds: LottoResult[]
setLatestResult: (result: LottoResult) => void
setRecentStats: (stats: Stat[]) => void
setLast50Rounds: (results: LottoResult[]) => void
}
export const useLottoStore = create<LottoStore>((set) => ({
latestResult: null,
recentStats: [],
last50Rounds: [],
setLatestResult: (result) => set({ latestResult: result }),
setRecentStats: (stats) => set({ recentStats: stats }),
setLast50Rounds: (results) => set({ last50Rounds: results }),
}))여기서 핵심은
• 이 store는 클라이언트에서만 작동해야 하므로 서버 컴포넌트에서는 import하지 않음
• 로또 결과, 통계 등 전역으로 재사용할 데이터를 저장
• 이후 다른 컴포넌트에서 setLatestResult() 형태로 상태 업데이트 가능
서버 데이터 → 클라이언트 동기화 (React Query + Zustand)
"use client"
import { useQuery } from "@tanstack/react-query"
import { LottoResult, useLottoStore } from "../stores/useLottoStore"
export function useLatestLotto() {
const latestResult = useLottoStore((state) => state.latestResult)
const setLatestResult = useLottoStore((state) => state.setLatestResult)
const query = useQuery<LottoResult>({
queryKey: ["lotto", "latest"],
queryFn: async () => {
const res = await fetch("/api/lotto/latest")
if (!res.ok) throw new Error("Failed")
const data = await res.json()
setLatestResult(data)
return data
},
enabled: !latestResult,
refetchInterval: 1000 * 60 * 30,
staleTime: 1000 * 60 * 29,
})
return {
data: latestResult || query.data,
isLoading: !latestResult && query.isLoading,
isError: query.isError,
}
}여기서 핵심은
• React Query: 서버에서 데이터 가져옴
• Zustand: 클라이언트 상태로 보관 (리렌더링 간 유지)
• enabled 조건: Zustand에 데이터가 이미 있으면 불필요한 refetch 방지
전역으로 유지할 값만 Zustand에 넣는다
실제 클라이언트 UI 컴포넌트
"use client"
import { useLatestLotto } from "@/hooks/queries/useLatestLotto"
import { useLottoStore } from "@/hooks/stores/useLottoStore"
import { Card } from "@/components/ui/card"
import { LottoBall } from "./LottoBall"
export function LatestResults() {
const { latestResult } = useLottoStore()
const { isLoading, isError } = useLatestLotto()
if (isLoading) return <Card>불러오는 중...</Card>
if (isError || !latestResult) return null
return (
<Card className="p-4 space-y-3">
<h2 className="text-sm font-bold">최신 당첨번호</h2>
<div className="flex gap-2 justify-center items-center">
{latestResult.numbers.map((num, i) => (
<LottoBall key={i} number={num} />
))}
<span>+</span>
<LottoBall number={latestResult.bonus} />
</div>
</Card>
)
}이 부분에서는 “Zustand가 저장한 상태를 그대로 UI에 반영”
→ React Query의 데이터 흐름과도 완전히 분리됨
후기
상태 관리가 필요한 간단한 데이터를 저장할 때 Zustand는 정말 가볍고 직관적인 해결책이었습니다.
설정도 간단하고, 어디서든 불러서 쓰기 쉬워서 앞으로도 이런 패턴은 Next.js + React Query 프로젝트에서 자주 활용할 것 같습니다.
'프론트엔드 > Next.js' 카테고리의 다른 글
| (Next.js) CSR vs SSR (1) | 2025.11.26 |
|---|---|
| (Next.js) 주요 특징 SSR vs SSG (0) | 2025.05.12 |
| (Next.js) npm run dev와 npm start 차이 (4) | 2025.01.09 |
| (Next.js) PWA 구현 (4) | 2025.01.07 |