0) ๊ฐ๋ ํ ์ค ์์ฝ
app/api/**/route.ts
ํ์ผ์ ๋ง๋ค๋ฉด ์๋ฒ ๋ผ์ฐํธ๊ฐ ๋ฉ๋๋ค.- HTTP ๋ฉ์๋๋ณ๋ก
export async function GET/POST/DELETE
๋ฅผ ๊ตฌํํฉ๋๋ค. - ํด๋ผ์ด์ธํธ์์๋
fetch('/api/...')
๋ก ํธ์ถํฉ๋๋ค.
1) ์๋ฒ: Todo API ๋ง๋ค๊ธฐ
(1) ๊ฐ๋จํ ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์ (ํ์ต์ฉ)
์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์์๋ ํธ์ถ๋ง๋ค ์ด๊ธฐํ๋ ์ ์์ผ๋ ํ์ต/๊ฐ๋ ํ์ธ์ฉ์ ๋๋ค.
app/api/todos/store.ts
export type Todo = { id: string; text: string; createdAt: number };
let todos: Todo[] = [];
export const store = {
findAll: () => todos.slice().sort((a, b) => b.createdAt - a.createdAt),
add: (text: string) => {
const t = { id: crypto.randomUUID(), text, createdAt: Date.now() };
todos.push(t);
return t;
},
remove: (id: string) => {
const before = todos.length;
todos = todos.filter((t) => t.id !== id);
return todos.length !== before;
},
};
(2) ๋ชฉ๋ก ์กฐํ / ์์ฑ (GET, POST)
app/api/todos/route.ts
import { NextResponse } from "next/server";
import { store } from "./store";
export async function GET() {
// ์บ์ ๋นํ์ฑํ (ํญ์ ์ต์ ๋ฐ์ดํฐ)
return NextResponse.json(store.findAll(), {
headers: { "Cache-Control": "no-store" },
});
}
export async function POST(req: Request) {
try {
const body = await req.json();
const text = String(body?.text ?? "").trim();
if (!text) {
return NextResponse.json({ error: "text is required" }, { status: 400 });
}
const created = store.add(text);
return NextResponse.json(created, { status: 201 });
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
}
(3) ๊ฐ๋ณ ์ญ์ (DELETE)
app/api/todos/[id]/route.ts
import { NextResponse } from "next/server";
import { store } from "../store";
type Params = { params: { id: string } };
export async function DELETE(_: Request, { params }: Params) {
const ok = store.remove(params.id);
if (!ok) return NextResponse.json({ error: "Not Found" }, { status: 404 });
return NextResponse.json({ ok: true });
}
โ ์ฌ๊ธฐ๊น์ง๋ก /api/todos (GET/POST), /api/todos/:id (DELETE) ์ค๋น ์๋ฃ!
2) ํด๋ผ์ด์ธํธ: API ์ฐ๋ Todo ํ์ด์ง
5๊ฐ์ ๋ก์ปฌ ์ํ๋ง ์ฐ๋ ์์ ๋ฅผ API ํธ์ถ ๊ธฐ๋ฐ์ผ๋ก ๋ฐ๊ฟ๋๋ค.
(์คํ์ผ์ 5๊ฐ์์ ๋ง๋ card.module.scss
, todo.module.scss
๋ฅผ ์ฌ์ฌ์ฉ)
app/todo-api/page.tsx
"use client";
import { useEffect, useState } from "react";
import card from "../../styles/card.module.scss";
import styles from "../../styles/todo.module.scss";
type Todo = { id: string; text: string; createdAt: number };
export default function TodoApiPage() {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const load = async () => {
setLoading(true);
try {
const res = await fetch("/api/todos", { cache: "no-store" });
const data = await res.json();
setTodos(data);
} finally {
setLoading(false);
}
};
const add = async (e: React.FormEvent) => {
e.preventDefault();
const text = input.trim();
if (!text) return;
setSubmitting(true);
try {
const res = await fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
alert(err?.error || "์ถ๊ฐ ์คํจ");
return;
}
setInput("");
await load();
} finally {
setSubmitting(false);
}
};
const remove = async (id: string) => {
const res = await fetch(`/api/todos/${id}`, { method: "DELETE" });
if (!res.ok) {
alert("์ญ์ ์คํจ");
return;
}
// ๋๊ด์ ์
๋ฐ์ดํธ (๋ฆฌ์คํธ์์ ์ฆ์ ์ ๊ฑฐ)
setTodos((prev) => prev.filter((t) => t.id !== id));
};
useEffect(() => {
load();
}, []);
return (
<section className={card.card}>
<h1 className={card.title}>Todo ์ฑ (API ์ฐ๋)</h1>
<p className={card.desc}>API Routes๋ก ์์ฑ/์กฐํ/์ญ์ ๋ฅผ ๊ตฌํํ์ต๋๋ค.</p>
<form className={styles.form} onSubmit={add}>
<inputplaceholder="ํ ์ผ์ ์
๋ ฅํ์ธ์"
value={input}
onChange={(e) => setInput(e.target.value)}
aria-label="ํ ์ผ ์
๋ ฅ"
disabled={submitting}
/>
<button className={styles.add} type="submit" disabled={submitting}>
{submitting ? "์ถ๊ฐ ์ค..." : "์ถ๊ฐ"}
</button>
</form>
{loading ? (
<p style={{ marginTop: "1rem" }}>๋ถ๋ฌ์ค๋ ์ค...</p>
) : (
<div className={card.list}>
{todos.length === 0 && <p>ํ ์ผ์ด ์์ต๋๋ค.</p>}
{todos.map((todo) => (
<div key={todo.id} className={styles.item}>
<span className={styles.text}>{todo.text}</span>
<button className={styles.remove} onClick={() => remove(todo.id)}>
์ญ์
</button>
</div>
))}
</div>
)}
</section>
);
}
3) ์บ์ ์ ๋ต ๊ฐ๋จ ๊ฐ์ด๋
- API ์๋ต ์ต์ ํ๊ฐ ์ค์ํ๋ฉด
- ์๋ฒ:
Cache-Control: no-store
ํค๋(์ ์์) - ํด๋ผ์ด์ธํธ:
fetch(..., { cache: "no-store" })
- ์๋ฒ:
- ์ ์ /์ฃผ๊ธฐ์ ์ฌ๊ฒ์ฆ์ด ์ข๋ค๋ฉด
fetch(url, { next: { revalidate: 60 } })
์ฌ์ฉ(ISR ๋๋)
4) ์ค๋ฅ/์ฃ์ง ์ผ์ด์ค ํ
- ์๋ฒ์์ JSON ํ์ฑ ์ค๋ฅ:
try/catch
๋ก 400 ์๋ต - ํ์ ํ๋ ๊ฒ์ฆ: ๋น ๋ฌธ์์ด ๋ฑ ๋ฐฉ์ง
- ์๋ฒ๋ฆฌ์ค ์ฃผ์: ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์๋ ํธ์ถ๋ง๋ค ์ด๊ธฐํ ๊ฐ๋ฅ โ DB๋ก ๊ต์ฒด ํ์
5) ์ค๋์ ์ ๋ฆฌ
app/api/**/route.ts
๋ก ์๋ฒ ์๋ํฌ์ธํธ ์ ์- GET/POST/DELETE ๊ตฌํ์ผ๋ก Todo API ์์ฑ
- ํด๋ผ์ด์ธํธ ํ์ด์ง์์
fetch
๋ก ์์ฑ/์กฐํ/์ญ์ ์ฐ๋ - ์บ์ ์ ๋ต๊ณผ ์๋ฒ๋ฆฌ์ค ํน์ฑ ์ดํด
๐ฌ ๋๊ธ
โป ๋ก๊ทธ์ธ ํ ๋๊ธ์ ์์ฑํ ์ ์์ต๋๋ค.