๐ฃ๏ธ Next.js 12์ฅ: Route Handlers โ ์ธ๋ถ์ ๋ํํ๋ ์ฐฝ๊ตฌ
๐ ๊ฐ์
Route Handler๋ก API ์๋ํฌ์ธํธ๋ฅผ ๋ง๋ค๊ณ ์ธ์ฆ, ์คํธ๋ฆฌ๋ฐ, CORS๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ๋ค๋ฃน๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ route.ts ๊ธฐ๋ณธ ๊ตฌ์กฐ โ GET๊ณผ POST ๋ง๋ค๊ธฐ ๐ข
- ๐ฑ Dynamic Route Handler โ URL ํ๋ผ๋ฏธํฐ์ ์ฟผ๋ฆฌ ์คํธ๋ง ๐ก
- ๐ ์ธ์ Route Handler๋ฅผ ์ฐ๊ณ , ์ธ์ Server Action์ ์ฐ๋๊ฐ ๐ก
- ๐ CORS, ์นํ , ํ์ผ ๋ค์ด๋ก๋ ๊ณ ๊ธ ํจํด ๐ด
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 15๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 8๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์ฒ (์ ์
): "Stripe ๊ฒฐ์ ์๋ฃ ํ Stripe์์ ์ฐ๋ฆฌ ์๋ฒ๋ก ์๋ฆผ(์นํ
)์ ๋ณด๋ด์ผ ํ๋์. ๊ทผ๋ฐ Server Action์
use server๋ผ ์ฐ๋ฆฌ Next.js ์ฑ ๋ด๋ถ์์๋ง ํธ์ถ๋์์์. ์ธ๋ถ ์๋น์ค๊ฐ ์ฐ๋ฆฌํํ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ์ฃ ?" - ์ํธ(๋ฆฌ๋): "๋ฐ๋ก ๊ทธ๊ฒ Route Handler๊ฐ ํ์ํ ์๊ฐ์ด์์. Server Action์ด '๋ด๋ถ ์ ํ'๋ผ๋ฉด, Route Handler๋ '์ธ๋ถ์ ๊ณต๊ฐ๋ ๋ํ ์ ํ๋ฒํธ'์์. ์ธ๋ถ ์๋น์ค๊ฐ POST๋ก ๋๋๋ฆด ์ ์๋ ์ค์ HTTP ์๋ํฌ์ธํธ๋ฅผ ๋ง๋ค์ด์ผ ํ ๋ ์ฐ๋ ๊ฑฐ์์."
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
route.ts ๊ธฐ๋ณธ ๋ฌธ๋ฒ โ ๋์ URL ์ฒ๋ฆฌ โ Server Action๊ณผ์ ์ญํ ๋ถ๋ฆฌ โ ์นํ
ยทCORS ๊ณ ๊ธ ํจํด
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
-
app/api/ํ์์route.tsํ์ผ๋ก REST API ์๋ํฌ์ธํธ๋ฅผ ๋ง๋ค ์ ์๋ค - Server Action๊ณผ Route Handler ์ค ์ด๋ค ๊ฑธ ์ธ์ง ์ํฉ์ ๋ฐ๋ผ ํ๋จํ ์ ์๋ค
- Stripe, GitHub ๋ฑ ์ธ๋ถ ์๋น์ค์ ์นํ ์ ๋ฐ๋ ์๋ํฌ์ธํธ๋ฅผ ๋ง๋ค ์ ์๋ค
๐ค ์ ์์์ผ ํ๋๊ฐ
์ฌํ 4์ฅ์์ Server Action์ ๋ฐฐ์ ์ด. ํผ ์ ์ถ, ๋ฒํผ ํด๋ฆญ, ๋ฐ์ดํฐ ๋ณ๊ฒฝ โ ์ด๋ฐ ๋ด๋ถ ๋์์ Server Action์ด ์๋ฒฝํ๊ฒ ์ฒ๋ฆฌํด์ค.
๊ทธ๋ฐ๋ฐ ์ธ์์๋ ์ฐ๋ฆฌ ์ฑ ์ธ๋ถ์์ ์ฐ๋ฆฌ ์๋ฒ๋ก HTTP ์์ฒญ์ ๋ณด๋ด์ผ ํ๋ ์ํฉ์ด ์์ด:
- **๊ฒฐ์ ์๋น์ค(Stripe)**๊ฐ ๊ฒฐ์ ์๋ฃ ํ ์ฐ๋ฆฌ ์๋ฒ์ ์นํ (Webhook) ์ ์ก
- **๋ชจ๋ฐ์ผ ์ฑ(iOS/Android)**์ด REST API ์๋ํฌ์ธํธ๋ฅผ ์ง์ ํธ์ถ
- ๋ค๋ฅธ ํ ์๋น์ค๊ฐ ์ฐ๋ฆฌ DB ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ๊ฐ๊ธฐ ์ํด API ํธ์ถ
- ํ์ผ ๋ค์ด๋ก๋ ๋๋ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง ๊ฐ์ ์คํธ๋ฆฌ๋ฐ ์๋ต
์ด๋ฐ ๊ฒฝ์ฐ์ Server Action์ ์ธ ์ ์์ด. Route Handler๊ฐ ์ด ์ญํ ์ ๋ด๋นํด. App Router ์ธ๊ณ์์ Pages Router์ pages/api/*.ts ๋ฅผ ๋์ฒดํ๋ ๋ฐฉ๋ฒ์ด์ผ.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
ํ์ฌ์๋ ๋ ๊ฐ์ง ์ ํ๊ฐ ์์ด. ๋ด๋ถ ์งํต ์ ํ(์ธํฐํฐ)๋ ์ง์๋ค๋ผ๋ฆฌ๋ง ์จ. ์ธ๋ถ ๋ํ ์ ํ๋ ๊ณ ๊ฐ์ด๋ ์ธ๋ถ ํํธ๋๊ฐ ๊ฑธ์ด์ค๋ ๊ฑฐ์ผ.Server Action์ด "์ง์ ์ธํฐํฐ"์ด๋ผ๋ฉด, Route Handler๋ "ํ์ฌ ๋ํ ์ ํ๋ฒํธ"์ผ.
์ธ๋ถ์์ ์ฐ๋ฆฌํํ ์ฐ๋ฝํ๋ ค๋ฉด ๋ํ ๋ฒํธ(Route Handler)๋ก ์์ผ ํด.
Route Handler์ URL ๊ตฌ์กฐ๋ ํ์ผ ์์น๋ก ๊ฒฐ์ ๋ผ:
app/
api/
posts/
route.ts โ GET /api/posts, POST /api/posts
[id]/
route.ts โ GET /api/posts/123, PUT /api/posts/123, DELETE /api/posts/123
webhooks/
stripe/
route.ts โ POST /api/webhooks/stripe
๐งฉ route.ts ๊ธฐ๋ณธ ๊ตฌ์กฐ โ GET๊ณผ POST ๋ง๋ค๊ธฐ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
route.tsํ์ผ์์ HTTP ๋ฉ์๋๋ณ ํจ์๋ฅผ exportํ๋ ๋ฐฉ๋ฒ์ ์๋คNextRequest์NextResponse๋ฅผ ํ์ฉํด ์์ฒญ/์๋ต์ ์ฒ๋ฆฌํ ์ ์๋ค
route.ts ํ์ผ์์๋ HTTP ๋ฉ์๋ ์ด๋ฆ์ ํจ์ ์ด๋ฆ์ผ๋ก exportํ๋ฉด ๋ผ. ๊ฐ๋จํด.
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/posts โ ๊ฒ์๊ธ ๋ชฉ๋ก ์กฐํ
export async function GET(request: NextRequest) {
// URL ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ์ฝ๊ธฐ
const { searchParams } = request.nextUrl
const page = searchParams.get('page') ?? '1'
const limit = searchParams.get('limit') ?? '10'
const posts = await db.posts.findMany({
skip: (parseInt(page) - 1) * parseInt(limit),
take: parseInt(limit),
orderBy: { createdAt: 'desc' },
})
// NextResponse.json()์ผ๋ก JSON ์๋ต ๋ฐํ
return NextResponse.json({
data: posts,
page: parseInt(page),
})
}
// POST /api/posts โ ๊ฒ์๊ธ ์์ฑ (์ธ๋ถ ํด๋ผ์ด์ธํธ์ฉ)
export async function POST(request: NextRequest) {
const body = await request.json() // Request Body ํ์ฑ
// ๊ฐ๋จํ ์ ํจ์ฑ ๊ฒ์ฌ
if (!body.title || !body.content) {
// ์๋ฌ ์๋ต: ๋ ๋ฒ์งธ ์ธ์๋ก ์ํ ์ฝ๋ ์ง์
return NextResponse.json(
{ error: '์ ๋ชฉ๊ณผ ๋ด์ฉ์ ํ์์์' },
{ status: 400 }
)
}
const post = await db.posts.create({ data: body })
return NextResponse.json({ data: post }, { status: 201 }) // 201 Created
}์ง์ HTTP ๋ฉ์๋ ๋ชฉ๋ก:
| ํจ์ export ์ด๋ฆ | HTTP ๋ฉ์๋ |
|---|---|
GET | GET |
POST | POST |
PUT | PUT |
PATCH | PATCH |
DELETE | DELETE |
HEAD | HEAD |
OPTIONS | OPTIONS |
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
route.ts์์ HTTP ๋ฉ์๋ ์ด๋ฆ์ผ๋ก ํจ์๋ฅผ exportํ๋ฉด ๊ทธ๊ฒ API ์๋ํฌ์ธํธ์ผ. ๋ง์น NestJS์@Get(),@Post()๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ํจ์ ์ด๋ฆ์ผ๋ก ๋์ ํ๋ ๊ฒ์ฒ๋ผ.
๐ฑ Dynamic Route Handler โ URL ํ๋ผ๋ฏธํฐ์ ์ฟผ๋ฆฌ ์คํธ๋ง ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
[id]๋์ ์ธ๊ทธ๋จผํธ์์ params๋ฅผ ๋ฐ์ ํน์ ๊ฒ์๊ธ์ ์กฐํ/์์ /์ญ์ ํ ์ ์๋ค
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
type Params = { params: Promise<{ id: string }> }
// GET /api/posts/123
export async function GET(request: NextRequest, { params }: Params) {
const { id } = await params // Next.js 15: params๋ Promise
const post = await db.posts.findUnique({ where: { id } })
if (!post) {
return NextResponse.json({ error: '๊ฒ์๊ธ์ ์ฐพ์ ์ ์์ด์' }, { status: 404 })
}
return NextResponse.json({ data: post })
}
// PUT /api/posts/123 โ ๊ฒ์๊ธ ์์
export async function PUT(request: NextRequest, { params }: Params) {
const { id } = await params
const body = await request.json()
const updated = await db.posts.update({
where: { id },
data: body,
})
return NextResponse.json({ data: updated })
}
// DELETE /api/posts/123 โ ๊ฒ์๊ธ ์ญ์
export async function DELETE(request: NextRequest, { params }: Params) {
const { id } = await params
await db.posts.delete({ where: { id } })
return new NextResponse(null, { status: 204 }) // 204 No Content
}๐ ์ธ์ Route Handler๋ฅผ ์ฐ๊ณ , ์ธ์ Server Action์ ์ฐ๋๊ฐ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋ ๊ธฐ์ ์ ์ ํํ ์ญํ ๊ฒฝ๊ณ๋ฅผ ์๊ณ , ์ํฉ์ ๋ฐ๋ผ ์ฌ๋ฐ๋ฅธ ์ ํ์ ํ ์ ์๋ค
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
๊ฒ์๊ธ ์์ฑ ํผ ์ ์ถ โ Server Action? Route Handler?
Stripe ์นํ ์์ โ Server Action? Route Handler?
์ด ์ง๋ฌธ์ ์ฝ๊ฒ ๋๋ตํ ์ ์์ผ๋ฉด ์ด ์น์ ์ ์ดํดํ ๊ฑฐ์ผ.
ํต์ฌ ํ๋จ ๊ธฐ์ค: ๋๊ฐ ํธ์ถํ๋๊ฐ?
| ํน์ง | Server Action | Route Handler |
|---|---|---|
| ํธ์ถ ์ฃผ์ฒด | ์ฐ๋ฆฌ React ์ฑ ๋ด๋ถ | ์ธ๋ถ ์๋น์ค, ๋ชจ๋ฐ์ผ ์ฑ, ๋ธ๋ผ์ฐ์ ์ง์ fetch |
| HTTP ๋ฉ์๋ | POST ๊ณ ์ | GET, POST, PUT, DELETE ๋ชจ๋ |
| ํ์ ์์ | ์๋ (ํจ์ import) | ์๋ (req/res ํ์ฑ) |
| ์ ์ง์ ํฅ์ | โ JS ์์ด๋ ๋์ | โ ์ง์ ์ ํจ |
| ์บ์ฑ | ์บ์ ์ ๋จ | GET ์์ฒญ ์บ์ ๊ฐ๋ฅ |
| ์ฃผ์ ์ฌ์ฉ์ฒ | ํผ, ๋ฒํผ, ๋ฐ์ดํฐ ๋ณ๊ฒฝ | ์นํ , ์ธ๋ถ API, ํ์ผ ๋ค์ด๋ก๋ |
// โ
Server Action์ผ๋ก ์จ์ผ ํ๋ ๊ฒฝ์ฐ
- ๊ฒ์๊ธ ์์ฑ ํผ ์ ์ถ
- ๋๊ธ ์ญ์ ๋ฒํผ ํด๋ฆญ
- ์ข์์ ํ ๊ธ
// โ
Route Handler๋ก ์จ์ผ ํ๋ ๊ฒฝ์ฐ
- Stripe ๊ฒฐ์ ์๋ฃ ์นํ
์์ (์ธ๋ถ์์ POST)
- ๋ชจ๋ฐ์ผ ์ฑ์ด ๊ฒ์๊ธ ๋ชฉ๋ก ์์ฒญ (์ธ๋ถ์์ GET)
- CSV ํ์ผ ๋ค์ด๋ก๋ (์คํธ๋ฆผ ์๋ต)
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"ํธ์ถ์๊ฐ ์ฐ๋ฆฌ ์ฑ์ด๋ฉด Server Action, ์ธ๋ถ ์๋น์ค๋ฉด Route Handler." ์ด ํ ๋ฌธ์ฅ์ด ์ ๋ถ์ผ.
๐ CORS, ์นํ , ํ์ผ ๋ค์ด๋ก๋ ๊ณ ๊ธ ํจํด ๐ด
๐ CORS ํค๋ ์ค์
๋ชจ๋ฐ์ผ ์ฑ์ด๋ ๋ค๋ฅธ ๋๋ฉ์ธ์์ ์ฐ๋ฆฌ API๋ฅผ ํธ์ถํ ๋ CORS ์๋ฌ๊ฐ ๋ฐ์ํด. Response์ ํค๋๋ฅผ ์ง์ ์ถ๊ฐํ๊ฑฐ๋, next.config.ts์์ ์ค์ ํ ์ ์์ด.
// app/api/posts/route.ts โ CORS ํค๋ ์ถ๊ฐ
export async function GET(request: NextRequest) {
const posts = await db.posts.findMany()
return NextResponse.json({ data: posts }, {
headers: {
'Access-Control-Allow-Origin': 'https://our-mobile-app.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
// OPTIONS preflight ์์ฒญ๋ ์ฒ๋ฆฌ
export async function OPTIONS() {
return new NextResponse(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
},
})
}๐ Stripe ์นํ ์์ ํจํด
์ธ๋ถ ์๋น์ค์ ์นํ ์ ๋ฐ์ ๋๋ ์๋ช (Signature) ๊ฒ์ฆ์ด ํ์์ผ. ์ ํ๋ฉด ๋๊ตฌ๋ ๊ฐ์ง ์์ฒญ์ ๋ณด๋ผ ์ ์์ด.
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: NextRequest) {
// Raw body๊ฐ ํ์ํ Stripe ์๋ช
๊ฒ์ฆ์ ์ํด text()๋ก ๋ฐ์
const body = await request.text()
const headersList = await headers()
const signature = headersList.get('stripe-signature')!
let event: Stripe.Event
try {
// Stripe๊ฐ ์์ฒญ์ ์๋ช
์ ๋ฃ์ด์ฃผ๋๋ฐ, ์ด๊ฑธ ๊ฒ์ฆํด์ผ ๊ฐ์ง ์์ฒญ์ ๊ฑธ๋ฌ๋ผ ์ ์์
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
console.error('์นํ
์๋ช
๊ฒ์ฆ ์คํจ:', err)
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
// ์ด๋ฒคํธ ํ์
๋ณ ์ฒ๋ฆฌ
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object as Stripe.PaymentIntent
await db.orders.update({
where: { paymentId: paymentIntent.id },
data: { status: 'PAID' },
})
break
case 'customer.subscription.deleted':
// ๊ตฌ๋
์ทจ์ ์ฒ๋ฆฌ
break
}
return NextResponse.json({ received: true })
}๐ฅ ํ์ผ ๋ค์ด๋ก๋ (์คํธ๋ฆฌ๋ฐ ์๋ต)
// app/api/export/posts/route.ts โ CSV ํ์ผ ๋ค์ด๋ก๋
export async function GET() {
const posts = await db.posts.findMany()
// CSV ๋ฌธ์์ด ์์ฑ
const csv = [
'id,title,createdAt',
...posts.map((p) => `${p.id},"${p.title}",${p.createdAt}`),
].join('\n')
return new NextResponse(csv, {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': 'attachment; filename="posts.csv"',
},
})
}๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
โ route.ts์ page.tsx๊ฐ ๊ฐ์ ๊ฒฝ๋ก์ ์์ด์ ์ถฉ๋
์ธ์ ๋์ค๋๊ฐ?
Error: You cannot have both a `page` file and a `route` file at the same path.
์์ธ: app/api/posts/page.tsx์ app/api/posts/route.ts๋ฅผ ๊ฐ์ ๊ฒฝ๋ก์ ๋์ ๋.
ํด๊ฒฐ์ฑ
: Route Handler๋ app/api/ ํ์์, ํ์ด์ง๋ app/ ์ง์ ํ์์ ๋ถ๋ฆฌํด์ ๊ด๋ฆฌํด.
โ GET ์์ฒญ์ธ๋ฐ POST ๋ฉ์๋ ์๋ฌ
์ธ์ ๋์ค๋๊ฐ?
{ "error": "Method Not Allowed" }์์ธ: route.ts์์ GET ํจ์๋ฅผ exportํ์ง ์์๋๋ฐ GET ์์ฒญ์ด ๋ค์ด์จ ๊ฒฝ์ฐ.
ํด๊ฒฐ์ฑ : ํ์ํ HTTP ๋ฉ์๋๋ง๋ค ํจ์๋ฅผ exportํด์ผ ํด. ์ฒ๋ฆฌํ์ง ์์ ๋ฉ์๋๋ export ์์ฒด๋ฅผ ์ ํ๋ฉด ์๋์ผ๋ก 405 ์๋ต์ ๋ฐํํด์ค.
๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ ํต์ฌ ํจํด
| ์ํฉ | ์ฝ๋ ํจํด |
|---|---|
| GET ์๋ต | return NextResponse.json({ data }) |
| ์๋ฌ ์๋ต | return NextResponse.json({ error }, { status: 400 }) |
| ๋น ์๋ต | return new NextResponse(null, { status: 204 }) |
| URL params | const { id } = await params |
| ์ฟผ๋ฆฌ ์คํธ๋ง | request.nextUrl.searchParams.get('page') |
| Request Body | const body = await request.json() |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| ์ํฉ | โ ๋์ ์ | โ ์ข์ ์ |
|---|---|---|
| ๋ด๋ถ ํผ ์ ์ถ | Route Handler | Server Action |
| ์นํ ์์ | Server Action | Route Handler |
| ์นํ ์๋ช ๊ฒ์ฆ ์ ํจ | body๋ง ํ์ฑ | stripe.webhooks.constructEvent() ๊ฒ์ฆ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. Server Action ๋์ Route Handler๊ฐ ๋ ์ด์ธ๋ฆฌ๋ ๋ํ ์ํฉ์?
โ ์ ๋ต: ์ธ๋ถ ์๋น์ค๊ฐ ์ฐ๋ฆฌ ์ฑ์ผ๋ก HTTP ์์ฒญ์ ๋ณด๋ด๋ ์นํ ์ด๋ ๊ณต๊ฐ API๊ฐ ํ์ํ ๋๋ค.
๐ก ์์ธ ํด์ค: Server Action์ ์ฑ ๋ด๋ถ์ mutation ํ๋ฆ์ ๊ฐํ๊ณ , Route Handler๋ ๋ช ์์ ์ธ HTTP ๋ฉ์๋์ ์๋ต์ ์ค๊ณํ๋ ๋ฐ ๊ฐํ๋ค.
Q2. ๊ฐ์ ์ธ๊ทธ๋จผํธ์ page.tsx์ route.ts๋ฅผ ํจ๊ป ๋ ์ ์๋ ์ด์ ๋?
โ ์ ๋ต: ํ๋์ ๊ฒฝ๋ก ์ธ๊ทธ๋จผํธ๊ฐ UI์ API ์๋ต์ ๋์์ ๋ด๋นํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
๐ก ์์ธ ํด์ค: /posts๊ฐ ํ์ด์ง์ธ์ง API์ธ์ง ๋ชจํธํด์ง๋ฉด ๋ผ์ฐํฐ๊ฐ ๊ฒฐ์ ํ ์ ์๋ค. ๋ณดํต API๋ app/api ์๋์ ๊ฒฉ๋ฆฌํ๋ค.
Q3. ์์ฒ ์ด์ ํ ์คํธ ํ์: ๊ฒฐ์ ์๋ฃ ์นํ ์ ๋ฐ์ ์ฃผ๋ฌธ ์ํ๋ฅผ ๋ฐ๊ฟ์ผ ํ๋ค. ๋ฌด์์ ๋ง๋ค๊น?
โ ์ ๋ต: POST Route Handler๋ฅผ ๋ง๋ค๊ณ ์๋ช ๊ฒ์ฆ, ๋ฉฑ๋ฑ์ฑ ์ฒ๋ฆฌ, ์ํ ๋ณ๊ฒฝ์ ์๋ฒ์์ ์ํํ๋ค.
๐ก ์์ธ ํด์ค: ์ธ๋ถ ๊ฒฐ์ ์๋ฒ๋ ๋ธ๋ผ์ฐ์ ํผ์ ๋๋ฅด์ง ์๋๋ค. ๋ช ์์ ์ธ HTTP ์๋ํฌ์ธํธ์ ์๋ฌธ body ๊ฒ์ฆ์ด ํ์ํ ์์ญ์ด๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ Server Action๊ณผ Route Handler๋ฅผ ๊ฒฝ์ ๊ด๊ณ๋ก ๋ณด์ง ์๊ฒ ๋๋ค.
๐ก "์ฌ์ฉ์ ํผ ๋ณ๊ฒฝ์ Action, ์ธ๋ถ ์์คํ ์ HTTP ํธ์ถ์ Handler๊ฐ ์์ฐ์ค๋ฝ๋ค."
๋ค์ API ์์ ์์๋ ํธ์ถ ์ฃผ์ฒด๊ฐ ๋ธ๋ผ์ฐ์ ์ฌ์ฉ์์์ธ์ง ์ธ๋ถ ์์คํ ์ธ์ง ๋จผ์ ๊ตฌ๋ถํ๊ฒ ๋ค.