๐ 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. Stripe ์นํ ์ ์์ ํ๋ ์๋ํฌ์ธํธ๋ฅผ ๋ง๋ค์ด์ผ ํ๋ค. ์ด๋ค ๋ฐฉ์์ ์จ์ผ ํ๋๊ฐ?
- A) Server Action โ
'use server'๋ก ์ ์ธํ ํจ์ - B) Route Handler โ
app/api/webhooks/stripe/route.ts์ POST ํจ์ - C) ์๋ฒ ์ปดํฌ๋ํธ์์ ์ง์ fetch๋ก Stripe API ํธ์ถ
- D) ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์
useEffect๋ก ์ฒ๋ฆฌ
โ ์ ๋ต: B
์ค๋ต ํด์ค:
- A โ Server Action์ ๋ด๋ถ์์๋ง ํธ์ถ ๊ฐ๋ฅ. ์ธ๋ถ ์๋น์ค๊ฐ ์ง์ ํธ์ถ ๋ถ๊ฐ
- C โ ์๋ฒ ์ปดํฌ๋ํธ๋ ๋ ๋๋ง ์์ ์๋ง ์คํ. ์นํ ์์ ๋ถ๊ฐ
- D โ ํด๋ผ์ด์ธํธ์์ ๋น๋ฐ ํค๋ฅผ ๋ค๋ฃจ๋ฉด ๋ณด์ ์ํ
๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "์ธ๋ถ๊ฐ ์ฐ๋ฆฌ๋ฅผ ์ฐ๋ฅธ๋ค โ Route Handler"
Q2. ์๋ ๋น์นธ์ ์ฑ์๋ณด์.
/api/users/42 ์ DELETE ์์ฒญ์ด ์ค๋ฉด ์ฌ์ฉ์๋ฅผ ์ญ์ ํ๋ Route Handler๋ค.
// app/api/users/[id]/route.ts
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await ______
await db.users.delete({ where: { id } })
return new NextResponse(null, { status: ______ })
}โ ์ ๋ต:
params,204ํด์ค:
params๋ Promise๋ผ await ํ์. ์ญ์ ์ฑ๊ณต ์ ๋ฐํํ ๋ด์ฉ์ด ์์ผ๋ฏ๋ก 204 No Content.๐ ํต์ฌ ๊ธฐ์ต๋ฒ: DELETE๋ 204, 404๊ฐ ์์ฃผ ์ง์ผ๋ก ๋์. "์ญ์ ํ๋ฉด ๋ด์ฉ ์์(204), ์๋ ๊ฑฐ ์ญ์ ํ๋ฉด ์ฐพ์ ์ ์์(404)."
Q3. ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
Server Action๊ณผ Route Handler์ ์ฐจ์ด๋ฅผ ์ ํ๊ธฐ ๋น์ ๋ก ์ค๋ช ํด๋ด.
์์ ๋ต๋ณ:
"Server Action์ ์ฌ๋ฌด์ค ์ธํฐํฐ์ด์ผ. ๊ฐ์ ๊ฑด๋ฌผ(์ฐ๋ฆฌ Next.js ์ฑ) ์์์๋ง ํตํ ๊ฐ๋ฅํด. Route Handler๋ ๋ํ ์ ํ์ผ. ์ธ๋ถ ์ฌ๋(Stripe, ๋ชจ๋ฐ์ผ ์ฑ)์ด ๊ฑธ์ด์ฌ ์ ์์ด. ๊ฒฐ์ ์๋ฃ ์นํ ์ฒ๋ผ ๋ฐ์์ ์ฐ๋ฝ์ด ์ค๋ฉด ๋ํ ์ ํ๋ฅผ ์จ์ผ์ง."
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ฐ๋ฆฌ ์๋น์ค์ '์ธ๋ถ์ฉ ๋ํ ๋ฒํธ(Route Handler)'๋ฅผ ๊ฐ์คํ ๋ ์ด์ผ! ๊ทธ๋์ ์ฐ๋ฆฌ ์ฑ ์์์๋ง ์๋ฅ๊ฑฐ๋ฆฌ๋ Server Action๋ง ์๋ค๊ฐ, ์ธ๋ถ ์๋น์ค์ธ Stripe์ ๋ํํ๊ธฐ ์ํ ์๋ํฌ์ธํธ๋ฅผ ์ง์ ๋ง๋๋ ์ ๋ง ์ ๊ธฐํ์ด.
๐ก ์ค๋์ ๊ตํ: "๋ด๋ถ ์ํต์ Server Action, ์ธ๋ถ ์๋น์ค์์ ๋ํ๋ Route Handler๋ฅผ ์ฐ์. ์ ํ๋ฒํธ๋ฅผ ๊ณต๊ฐํ๋ค๋ฉด ๋ณด์ ๊ฒ์ฆ์ ํ์๋ค!"
๋จ์ํ URL๋ง ๋ซ๋ ๊ฒ ์๋๋ผ, ์ธ๋ถ์ ๊ณต๊ฒฉ์ ๋ง๊ธฐ ์ํด ์๋ช (Signature)์ ๊ฒ์ฆํ๊ณ ๋ณด์์ ์ฑ๊ธฐ๋ ๊ณผ์ ์์ ์ง์ง '์๋ฒ ๊ฐ๋ฐ์'๊ฐ ๋ ๊ธฐ๋ถ์ด ๋ค๋๋ผ. ์ค๋์ ์ ๋ํ ์๋์ง๋ฅผ ๋ง์ด ์ผ๋ค. ํด๊ทผ๊ธธ์ ๋งํธ์ ๋ค๋ฌ์ ๋ง์๋ ๊ณ ๊ธฐ ์ฌ๋ค๊ฐ ๊ตฌ์ ๋จน์ด์ผ์ง! ๋ด์ผ์ ๋ '์ฐ์ํ' ์๋ํฌ์ธํธ๋ฅผ ์ค๊ณํ๋ ๊ฐ๋ฐ์๊ฐ ๋์ด์ผ๊ฒ ์ด. ๐ฃ