
Next.js의 App Router 기반 프로젝트에서 상태를 어떻게 관리하고,
서버와 클라이언트 사이에서 데이터를 어떻게 주고받는지를 정리해 보았습니다.
Next.js의 서버 컴포넌트에서 데이터를 fetch한 뒤,
클라이언트 컴포넌트에 props로 전달하여 상태로 전환.
// server component
async function Page() {
const data = await getData(); // 서버에서 데이터 fetch
return <Client data={data} />; // 클라이언트 컴포넌트로 전달
}
// client component
'use client';
export default function Client({ data }) {
const [info, setInfo] = useState(data); // 상태화
return <div>{info.title}</div>;
}
📌
서버에서 데이터를 먼저 가져오고, 클라이언트에서는 그 데이터를 상태로 만들어 반응형 UI를 구현
이때 반응형 UI는 화면 크기 대응이 아니라,
데이터 변화에 따라 자동으로 UI가 갱신되는 리액티브한 동작을 의미
Context API는 여러 컴포넌트에서 전역 상태를 공유할 수 있게 해주는 React의 내장 기능
// context/UserContext.tsx
'use client';
import { createContext, useContext, useState } from 'react';
const UserContext = createContext(null);
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
return useContext(UserContext);
}
'use client';
import { useUser } from '@/context/UserContext';
function Profile() {
const { user } = useUser();
return <p>{user ? `${user.name}님 환영합니다` : '로그인하세요'}</p>;
}
✅ UserProvider로 감싸진 모든 자식 컴포넌트는 useUser()로 상태를 꺼내 쓸 수 있다.
// stores/useCounterStore.ts
import { create } from 'zustand';
export const useCounterStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}));
// components/Counter.tsx
'use client';
import { useCounterStore } from '@/stores/useCounterStore';
function Counter() {
const { count, increase } = useCounterStore();
return <button onClick={increase}>Count: {count}</button>;
}
atom() 단위로 쪼개어 관리useAtom()으로 사용, useState()와 비슷한 사용감// atoms/counterAtom.ts
import { atom } from 'jotai';
export const counterAtom = atom(0);
// components/Counter.tsx
'use client';
import { useAtom } from 'jotai';
import { counterAtom } from '@/atoms/counterAtom';
function Counter() {
const [count, setCount] = useAtom(counterAtom);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
| 항목 | Zustand | Jotai |
|---|---|---|
| 상태 구조 | 스토어 하나에 모아 관리 | atom 단위로 쪼개서 관리 |
| 사용법 | useStore() | useAtom() |
| 구조화 | 명시적으로 store 구성 | 파일 흩어짐 주의 |
| 러닝커브 | 조금 있음 | useState랑 비슷 |
| 파생 상태 | 직접 계산 | atom((get) => ...) 사용 가능 |
서버에서 받아온 데이터를 클라이언트에서도 정확하게 유지해야 함
( 그렇지 않으면 hydration mismatch 발생 가능 )
| 전략 | 설명 |
|---|---|
| Props로 전달 후 useState | 가장 기본적인 방법 |
| SWR/React Query | 클라이언트에서 fetch + 캐시로 상태 유지 |
| ISR 사용 | 페이지 자체를 일정 시간마다 서버에서 재생성 |
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
function Profile() {
const { data } = useSWR('/api/user', fetcher, {
refreshInterval: 5000,
});
return <div>{data?.name}</div>;
}
✅ SWR은 브라우저에서 직접 데이터를 가져오고,
가져온 데이터를 캐시에 저장해 재사용하거나 일정 시간마다 새로고침할 수 있다.
refreshInterval vs ISR의 revalidate| 항목 | SWR refreshInterval | ISR revalidate |
|---|---|---|
| 동작 위치 | 브라우저 (클라이언트) | 서버 |
| 대상 | 데이터 | 전체 HTML 페이지 |
| 사용법 | useSWR에서 설정 | 서버 컴포넌트에 설정 |
| 대표 목적 | 사용자 화면에서 실시간 데이터 반영 | 정적 페이지를 주기적으로 최신화 |