Post
[Next.js 5/6] SWR + Optimistic Update로 To-do CRUD 연동
[Next.js 5/6] SWR + Optimistic Update로 To-do CRUD 연동
이번 편에서는 화면 상태를 실제 API와 연결합니다.
1) 패키지 설치
npm i swr
2) SWR 훅 생성
src/hooks/useTodos.ts
'use client';
import useSWR from 'swr';
import type { Todo } from '@/types/todo';
const fetcher = async (url: string): Promise<Todo[]> => {
const res = await fetch(url, { cache: 'no-store' });
if (!res.ok) throw new Error('failed to fetch');
return res.json();
};
export function useTodos() {
const { data, error, mutate, isLoading } = useSWR<Todo[]>('/api/todos', fetcher);
return {
todos: data ?? [],
error,
isLoading,
mutate
};
}
3) 투두 API 프록시
src/app/api/todos/route.ts
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
const API = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8000';
export async function GET() {
const token = (await cookies()).get('access_token')?.value;
const res = await fetch(`${API}/todos`, { headers: { Authorization: `Bearer ${token}` }, cache: 'no-store' });
const data = await res.json();
return NextResponse.json(data, { status: res.status });
}
export async function POST(req: Request) {
const token = (await cookies()).get('access_token')?.value;
const body = await req.json();
const res = await fetch(`${API}/todos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(body)
});
const data = await res.json();
return NextResponse.json(data, { status: res.status });
}
src/app/api/todos/[id]/route.ts
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
const API = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8000';
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const token = (await cookies()).get('access_token')?.value;
const body = await req.json();
const res = await fetch(`${API}/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(body)
});
return NextResponse.json(await res.json(), { status: res.status });
}
export async function DELETE(_: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const token = (await cookies()).get('access_token')?.value;
const res = await fetch(`${API}/todos/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` } });
return NextResponse.json({}, { status: res.status });
}
4) 페이지에서 낙관적 업데이트
src/app/(dashboard)/todos/page.tsx
'use client';
import { TodoForm } from '@/components/todo/TodoForm';
import { TodoList } from '@/components/todo/TodoList';
import { useTodos } from '@/hooks/useTodos';
export default function TodosPage() {
const { todos, mutate, isLoading } = useTodos();
const onCreate = async (title: string) => {
await mutate(async (prev = []) => {
const optimistic = [...prev, { id: Date.now(), title, done: false, created_at: new Date().toISOString() }];
const res = await fetch('/api/todos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) });
if (!res.ok) throw new Error('create failed');
const created = await res.json();
return [...prev, created];
}, { optimisticData: [...todos, { id: Date.now(), title, done: false, created_at: new Date().toISOString() }], rollbackOnError: true, revalidate: false });
};
const onToggle = async (id: number, done: boolean) => {
await mutate(async (prev = []) => {
const res = await fetch(`/api/todos/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ done }) });
if (!res.ok) throw new Error('toggle failed');
return prev.map((t) => (t.id === id ? { ...t, done } : t));
}, { optimisticData: todos.map((t) => (t.id === id ? { ...t, done } : t)), rollbackOnError: true, revalidate: false });
};
const onDelete = async (id: number) => {
await mutate(async (prev = []) => {
const res = await fetch(`/api/todos/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('delete failed');
return prev.filter((t) => t.id !== id);
}, { optimisticData: todos.filter((t) => t.id !== id), rollbackOnError: true, revalidate: false });
};
if (isLoading) return <main style=>로딩 중...</main>;
return (
<main style=>
<h1>To-do</h1>
<TodoForm onCreate={onCreate} />
<TodoList todos={todos} onToggle={onToggle} onDelete={onDelete} />
</main>
);
}
댓글