Route Handlers
๋ผ์ฐํธ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ๋ฉด, ์น ์์ฒญ ๋ฐ ์๋ต API๋ฅผ ์ฌ์ฉํ์ฌ ์ง์ ๋ ๊ฒฝ๋ก์ ๋ํ ์ฌ์ฉ์ ์ง์ ์์ฒญ ํธ๋ค๋ฌ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.

์์๋๋ฉด ์ข์ต๋๋ค:
- Route ํธ๋ค๋ฌ๋ app ๋๋ ํ ๋ฆฌ ๋ด์์๋ง ์ฌ์ฉํ ์ ์์ต๋๋ค.
- Route ํธ๋ค๋ฌ๋ page ๋๋ ํฐ๋ฆฌ ๋ด์ API ๊ฒฝ๋ก์ ๋์ผํ๋ฏ๋ก, API ๊ฒฝ๋ก์ Route ํธ๋ค๋ฌ๋ฅผ ํจ๊ป ์ฌ์ฉํ ํ์๊ฐ ์์ต๋๋ค.
1. Convention(๊ท์น)
๋ผ์ฐํธ ํธ๋ค๋ฌ๋ app ๋๋ ํฐ๋ฆฌ ๋ด์ route.js|ts ํ์ผ์ ์ ์๋ฉ๋๋ค:
1export const dynamic = 'force-dynamic' // defaults to auto2export async function GET(request: Request) {}
๋ผ์ฐํธ ํธ๋ค๋ฌ๋ page.js ๋ฐ layout.js์ ์ ์ฌํ๊ฒ app ๋๋ ํ ๋ฆฌ ๋ด์ ์ค์ฒฉ๋ ์ ์์ต๋๋ค.
๊ทธ๋ฌ๋ page.js์ ๋์ผํ ๊ฒฝ๋ก ์ธ๊ทธ๋จผํธ ์์ค์๋ route.js ํ์ผ์ด ์์ ์ ์์ต๋๋ค.
1.1 ์ง์๋๋ HTTP ๋ฉ์๋
- ์ง์๋๋ HTTP ๋ฉ์๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
GET,POST,PUT,PATCH,DELETE,HEAD๋ฐOPTIONS. - ์ง์๋์ง ์๋ ๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด Next.js๋
405 ๋ฉ์๋ ํ์ฉ๋์ง ์์ ์๋ต์ ๋ฐํํฉ๋๋ค.
1.2 ํ์ฅ๋ NextRequest ๋ฐ NextResponse API
๊ธฐ๋ณธ Request ๋ฐ Response์ ์ง์ํ ๋ฟ๋ง ์๋๋ผ, Next.js๋ ๊ณ ๊ธ ์ฌ์ฉ ์ฌ๋ก๋ฅผ ์ํ ํธ๋ฆฌํ ๋์ฐ๋ฏธ๋ฅผ ์ ๊ณตํ๊ธฐ ์ํด NextRequest ๋ฐ NextResponse๋ก ์ด๋ฅผ ํ์ฅํฉ๋๋ค.
2. Behavior(ํ๋)
2.1 Caching
์๋ต ๊ฐ์ฒด์ ํจ๊ป GET ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ๋, ๊ธฐ๋ณธ์ ์ผ๋ก ๋ผ์ฐํธ ํธ๋ค๋ฌ๊ฐ ์บ์๋ฉ๋๋ค.
1export async function GET() {2const res = await fetch('https://data.mongodb-api.com/...', {3headers: {4'Content-Type': 'application/json',5'API-Key': process.env.DATA_API_KEY,6},7})8const data = await res.json()910return Response.json({ data })11}
TypeScript ๊ฒฝ๊ณ :
Response.json()์ TypeScript 5.2๋ถํฐ ์ ํจํฉ๋๋ค.- ํ์ ๋ฒ์ ์ TypeScript๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ์ ๋ ฅ๋ ์๋ต์
NextResponse.json()์ ๋์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
2.2 Opting out of caching
์บ์ฑ์ ์ฌ์ฉํ์ง ์๋๋ก ์ค์ ํ ์ ์๋ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
GET๋ฉ์๋์ ํจ๊ป ์์ฒญ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํฉ๋๋ค.- ๋ค๋ฅธ HTTP ๋ฉ์๋ ์ฌ์ฉ.
์ฟ ํค๋ฐํค๋์ ๊ฐ์ ๋์ ํจ์ ์ฌ์ฉ.- ์ธ๊ทธ๋จผํธ ๊ตฌ์ฑ ์ต์ ์์ ๋์ ๋ชจ๋๋ฅผ ์๋์ผ๋ก ์ง์ ํฉ๋๋ค.
์๋ฅผ ๋ค์ด:
1export async function GET(request: Request) {2const { searchParams } = new URL(request.url)3const id = searchParams.get('id')4const res = await fetch(`https://data.mongodb-api.com/product/${id}`, {5headers: {6'Content-Type': 'application/json',7'API-Key': process.env.DATA_API_KEY!,8},9})10const product = await res.json()1112return Response.json({ product })13}
๋ง์ฐฌ๊ฐ์ง๋ก, POST ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ์ฐํธ ํธ๋ค๋ฌ๊ฐ ๋์ ์ผ๋ก ํ๊ฐ๋ฉ๋๋ค.
1export async function POST() {2const res = await fetch('https://data.mongodb-api.com/...', {3method: 'POST',4headers: {5'Content-Type': 'application/json',6'API-Key': process.env.DATA_API_KEY!,7},8body: JSON.stringify({ time: new Date().toISOString() }),9})1011const data = await res.json()1213return Response.json(data)14}
์์๋๋ฉด ์ข์ต๋๋ค:
- API ๊ฒฝ๋ก์ ๋ง์ฐฌ๊ฐ์ง๋ก, ๊ฒฝ๋ก ํธ๋ค๋ฌ๋ ์์ ์ ์ถ ์ฒ๋ฆฌ์ ๊ฐ์ ๊ฒฝ์ฐ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- React์ ๊ธด๋ฐํ๊ฒ ํตํฉ๋๋ ์์ ๋ฐ ๋ณํ์ ์ฒ๋ฆฌํ๊ธฐ ์ํ ์๋ก์ด ์ถ์ํ๊ฐ ์์ ์ค์ ๋๋ค.
2.3 Route Resolution(ํด๊ฒฐ)
route๋ฅผ ๊ฐ์ฅ ๋ฎ์ ์์ค์ routing primitive.๋ก ๊ฐ์ฃผํ ์ ์์ต๋๋ค.
page์ ๊ฐ์ ๋ ์ด์์์ด๋ ํด๋ผ์ด์ธํธ ์ธก ํ์์ ์ฐธ์ฌํ์ง ์์ต๋๋ค.page.js์ ๊ฐ์ ๊ฒฝ๋ก์ route.js ํ์ผ์ด ์์ ์ ์์ต๋๋ค.
You can consider a route the lowest level routing primitive.
- They do not participate in layouts or client-side navigations like
page. - There cannot be a
route.jsfile at the same route aspage.js.
| Page | Route | Result |
|---|---|---|
app/page.js | app/route.js | โ Conflict |
app/page.js | app/api/route.js | โ Valid |
app/[user]/page.js | app/api/route.js | โ Valid |
๊ฐ route.js ๋๋ page.js ํ์ผ์ ํด๋น ๊ฒฝ๋ก์ ๋ชจ๋ HTTP ๋์ฌ๋ฅผ ๋์ ํฉ๋๋ค.
1export default function Page() {2return <h1>Hello, Next.js!</h1>3}45// โ Conflict6// `app/route.js`7export async function POST(request) {}
3. ์์
๋ค์ ์์ ์์๋ ๋ผ์ฐํธ ํธ๋ค๋ฌ๋ฅผ ๋ค๋ฅธ Next.js API ๋ฐ ๊ธฐ๋ฅ๊ณผ ๊ฒฐํฉํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค.
3.1 ์บ์๋ ๋ฐ์ดํฐ ์ฌ๊ฒ์ฆ
next.revalidate ์ต์ ์ ์ฌ์ฉํ์ฌ ์บ์๋ ๋ฐ์ดํฐ์ ์ ํจ์ฑ์ ์ฌ๊ฒ์ฆํ ์ ์์ต๋๋ค:
1export async function GET() {2const res = await fetch('https://data.mongodb-api.com/...', {3next: { revalidate: 60 }, // Revalidate every 60 seconds4})5const data = await res.json()67return Response.json(data)8}
๋๋(Alternatively), ์ธ๊ทธ๋จผํธ ๊ตฌ์ฑ ์ฌ๊ฒ์ฆ(revalidate) ์ต์
์ ์ฌ์ฉํ ์๋ ์์ต๋๋ค:
1export const revalidate = 60
3.2 ๋์ ๊ธฐ๋ฅ
๋ผ์ฐํธ ํธ๋ค๋ฌ๋ ์ฟ ํค ๋ฐ ํค๋์ ๊ฐ์ Next.js์ ๋์ ํจ์์ ํจ๊ป ์ฌ์ฉํ ์ ์์ต๋๋ค.
3.2.1 Cookies
next/headers์ ์ฟ ํค๋ก ์ฟ ํค๋ฅผ ์ฝ๊ฑฐ๋ ์ค์ ํ ์ ์์ต๋๋ค.
์ด ์๋ฒ ํจ์๋ ๋ผ์ฐํธ ํธ๋ค๋ฌ์์ ์ง์ ํธ์ถํ๊ฑฐ๋ ๋ค๋ฅธ ํจ์ ์์ ์ค์ฒฉํ์ฌ ํธ์ถํ ์ ์์ต๋๋ค.
๋๋ Set-Cookie ํค๋๋ฅผ ์ฌ์ฉํ์ฌ ์ ์๋ต์ ๋ฐํํ ์๋ ์์ต๋๋ค.
1import { cookies } from 'next/headers'23export async function GET(request: Request) {4const cookieStore = cookies()5const token = cookieStore.get('token')67return new Response('Hello, Next.js!', {8status: 200,9headers: { 'Set-Cookie': `token=${token.value}` },10})11}
๊ธฐ๋ณธ ์น API๋ฅผ ์ฌ์ฉํ์ฌ ์์ฒญ์์ ์ฟ ํค๋ฅผ ์ฝ์ ์๋ ์์ต๋๋ค(NextRequest):
1import { type NextRequest } from 'next/server'23export async function GET(request: NextRequest) {4const token = request.cookies.get('token')5}
3.2.2 Headers
next/headers์์ ํค๋๋ก ํค๋๋ฅผ ์ฝ์ ์ ์์ต๋๋ค.
์ด ์๋ฒ ํจ์๋ ๋ผ์ฐํธ ํธ๋ค๋ฌ์์ ์ง์ ํธ์ถํ๊ฑฐ๋ ๋ค๋ฅธ ํจ์ ์์ ์ค์ฒฉํ์ฌ ํธ์ถํ ์ ์์ต๋๋ค.
์ด headers ์ธ์คํด์ค๋ ์ฝ๊ธฐ ์ ์ฉ์
๋๋ค. headers๋ฅผ ์ค์ ํ๋ ค๋ฉด, ์ ํค๋๊ฐ ํฌํจ๋ ์ ์๋ต์ ๋ฐํํด์ผ ํฉ๋๋ค.
1import { headers } from 'next/headers'23export async function GET(request: Request) {4const headersList = headers()5const referer = headersList.get('referer')67return new Response('Hello, Next.js!', {8status: 200,9headers: { referer: referer },10})11}
๊ธฐ๋ณธ ์น API๋ฅผ ์ฌ์ฉํ์ฌ ์์ฒญ์ ํค๋๋ฅผ ์ฝ์ ์๋ ์์ต๋๋ค(NextRequest):
1import { type NextRequest } from 'next/server'23export async function GET(request: NextRequest) {4const requestHeaders = new Headers(request.headers)5}
3.3 Redirects(๋ฆฌ๋๋ ์ )
1import { redirect } from 'next/navigation'23export async function GET(request: Request) {4redirect('https://nextjs.org/')5}
3.4 Dynamic Route Segments
๊ณ์ํ๊ธฐ ์ ์ ๊ฒฝ๋ก ์ ์ ํ์ด์ง๋ฅผ ์ฝ์ด๋ณด์๊ธฐ ๋ฐ๋๋๋ค.
๋ผ์ฐํธ ํธ๋ค๋ฌ๋ ๋์ ์ธ๊ทธ๋จผํธ๋ฅผ ์ฌ์ฉํ์ฌ ๋์ ๋ฐ์ดํฐ์์ ์์ฒญ ํธ๋ค๋ฌ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
1export async function GET(request: Request, { params }: { params: { slug: string } }) {2const slug = params.slug // 'a', 'b', or 'c'3}
| Route | Example URL | params |
|---|---|---|
app/items/[slug]/route.js | /items/a | { slug: 'a' } |
app/items/[slug]/route.js | /items/b | { slug: 'b' } |
app/items/[slug]/route.js | /items/c | { slug: 'c' } |
3.5 URL Query Parameters
๋ผ์ฐํธ ํธ๋ค๋ฌ์ ์ ๋ฌ๋๋ ์์ฒญ ๊ฐ์ฒด๋ ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์๋ฅผ ๋ณด๋ค ์ฝ๊ฒ ์ฒ๋ฆฌํ๋ ๋ฑ ๋ช ๊ฐ์ง ์ถ๊ฐ ํธ์ ๋ฉ์๋๊ฐ ์๋ NextRequest ์ธ์คํด์ค์
๋๋ค.
1import { type NextRequest } from 'next/server'23export function GET(request: NextRequest) {4const searchParams = request.nextUrl.searchParams5const query = searchParams.get('query')6// query is "hello" for /api/search?query=hello7}
3.6 Streaming
์คํธ๋ฆฌ๋ฐ์ ์ผ๋ฐ์ ์ผ๋ก AI๊ฐ ์์ฑํ ์ฝํ ์ธ ์ OpenAI์ ๊ฐ์ ๋๊ท๋ชจ ์ธ์ด ๋ชจ๋ธ(LLM)๊ณผ ํจ๊ป ์ฌ์ฉ๋ฉ๋๋ค. AI SDK์ ๋ํด ์์ธํ ์์๋ณด์ธ์.
1import OpenAI from 'openai'2import { OpenAIStream, StreamingTextResponse } from 'ai'34const openai = new OpenAI({5apiKey: process.env.OPENAI_API_KEY,6})78export const runtime = 'edge'910export async function POST(req: Request) {11const { messages } = await req.json()12const response = await openai.chat.completions.create({13model: 'gpt-3.5-turbo',14stream: true,15messages,16})1718const stream = OpenAIStream(response)1920return new StreamingTextResponse(stream)21}
์ด๋ฌํ ์ถ์ํ๋ ์น API๋ฅผ ์ฌ์ฉํ์ฌ ์คํธ๋ฆผ์ ์์ฑํฉ๋๋ค. ๊ธฐ๋ณธ ์น API๋ฅผ ์ง์ ์ฌ์ฉํ ์๋ ์์ต๋๋ค.
1// https://developer.mozilla.org/docs/Web/API/ReadableStream#convert_async_iterator_to_stream2function iteratorToStream(iterator: any) {3return new ReadableStream({4async pull(controller) {5const { value, done } = await iterator.next()67if (done) {8controller.close()9} else {10controller.enqueue(value)11}12},13})14}1516function sleep(time: number) {17return new Promise(resolve => {18setTimeout(resolve, time)19})20}2122const encoder = new TextEncoder()2324async function* makeIterator() {25yield encoder.encode('<p>One</p>')26await sleep(200)27yield encoder.encode('<p>Two</p>')28await sleep(200)29yield encoder.encode('<p>Three</p>')30}3132export async function GET() {33const iterator = makeIterator()34const stream = iteratorToStream(iterator)3536return new Response(stream)37}
3.7 Request Body
ํ์ค ์น API ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ, ์์ฒญ body์ ์ฝ์ ์ ์์ต๋๋ค:
1export async function POST(request: Request) {2const res = await request.json()3return Response.json({ res })4}
3.8 Request Body FormData
request.formData() ํจ์๋ฅผ ์ฌ์ฉํ์ฌ FormData๋ฅผ ์ฝ์ ์ ์์ต๋๋ค:
1export async function POST(request: Request) {2const formData = await request.formData()3const name = formData.get('name')4const email = formData.get('email')5return Response.json({ name, email })6}
formData ๋ฐ์ดํฐ๋ ๋ชจ๋ ๋ฌธ์์ด์ด๋ฏ๋ก,
zod-form-data๋ฅผ ์ฌ์ฉํ์ฌ ์์ฒญ์ ์ ํจ์ฑ์ ๊ฒ์ฌํ๊ณ ์ํ๋ ํ์(e.g. number)์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ ์ ์์ต๋๋ค.
3.9 CORS
ํ์ค ์น API ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ, ํน์ ๋ผ์ฐํธ ํธ๋ค๋ฌ์ ๋ํ CORS ํค๋๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค:
You can set CORS headers on a Response using the standard Web API methods:
1export const dynamic = 'force-dynamic' // defaults to auto23export async function GET(request: Request) {4return new Response('Hello, Next.js!', {5status: 200,6headers: {7'Access-Control-Allow-Origin': '*',8'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',9'Access-Control-Allow-Headers': 'Content-Type, Authorization',10},11})12}
์์๋๋ฉด ์ ์ฉํฉ๋๋ค:
- ์ฌ๋ฌ ๋ผ์ฐํธ ํธ๋ค๋ฌ์ CORS ํค๋๋ฅผ ์ถ๊ฐํ๋ ค๋ฉด, ๋ฏธ๋ค์จ์ด ๋๋
next.config.jsํ์ผ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.- ๋๋ CORS ์์ ํจํค์ง๋ฅผ ์ฐธ์กฐํ์ธ์.
3.10 Webhooks
๋ผ์ฐํธ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ์ฌ ํ์ฌ ์๋น์ค์์ ์นํ ์ ์์ ํ ์ ์์ต๋๋ค:
1export async function POST(request: Request) {2try {3const text = await request.text()4// Process the webhook payload5} catch (error) {6return new Response(`Webhook error: ${error.message}`, {7status: 400,8})9}1011return new Response('Success!', {12status: 200,13})14}
ํนํ Pages ๋ผ์ฐํฐ๋ฅผ ์ฌ์ฉํ๋ API ๋ผ์ฐํฐ์ ๋ฌ๋ฆฌ, ์ถ๊ฐ ๊ตฌ์ฑ์ ์ํด bodyParser๋ฅผ ์ฌ์ฉํ ํ์๊ฐ ์์ต๋๋ค.
3.11 Edge ๋ฐ Node.js ๋ฐํ์
๋ผ์ฐํธ ํธ๋ค๋ฌ์๋ ์คํธ๋ฆฌ๋ฐ ์ง์์ ํฌํจํ์ฌ Edge ๋ฐ Node.js ๋ฐํ์์ ์ํํ๊ฒ ์ง์ํ๋ ๋ํ ์น API๊ฐ ์์ต๋๋ค. ๋ผ์ฐํธ ํธ๋ค๋ฌ๋ ํ์ด์ง ๋ฐ ๋ ์ด์์๊ณผ ๋์ผํ ๋ผ์ฐํธ ์ธ๊ทธ๋จผํธ ๊ตฌ์ฑ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์, ๋ฒ์ฉ ์ ์ ์ผ๋ก ์ฌ์์ฑ๋๋ ๋ผ์ฐํธ ํธ๋ค๋ฌ์ ๊ฐ์ด ์ค๋ซ๋์ ๊ธฐ๋ค๋ ค์จ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
runtime ์ธ๊ทธ๋จผํธ ๊ตฌ์ฑ ์ต์
์ ์ฌ์ฉํ์ฌ ๋ฐํ์์ ์ง์ ํ ์ ์์ต๋๋ค:
1export const runtime = 'edge' // 'nodejs' is the default
3.12 Non-UI Responses
๋ผ์ฐํธ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ์ฌ non-UI ์ฝํ
์ธ ๋ฅผ ๋ฐํํ ์ ์์ต๋๋ค.
sitemap.xml robots.txt, app icons ๋ฐ open graph images๋ ๋ชจ๋ ๊ธฐ๋ณธ ์ง์๋ฉ๋๋ค.
1export const dynamic = 'force-dynamic' // defaults to auto23export async function GET() {4return new Response(5`<?xml version="1.0" encoding="UTF-8" ?>6<rss version="2.0">78<channel>9<title>Next.js Documentation</title>10<link>https://nextjs.org/docs</link>11<description>The React Framework for the Web</description>12</channel>1314</rss>`,15{16headers: {17'Content-Type': 'text/xml',18},19},20)21}
3.13 ์ธ๊ทธ๋จผํธ ๊ตฌ์ฑ ์ต์
๋ผ์ฐํธ ํธ๋ค๋ฌ๋ ํ์ด์ง ๋ฐ ๋ ์ด์์๊ณผ ๋์ผํ ๋ผ์ฐํธ ์ธ๊ทธ๋จผํธ ๊ตฌ์ฑ์ ์ฌ์ฉํฉ๋๋ค.
1export const dynamic = 'auto'2export const dynamicParams = true3export const revalidate = false4export const fetchCache = 'auto'5export const runtime = 'nodejs'6export const preferredRegion = 'auto'
์์ธํ ๋ด์ฉ์ API ์ฐธ์กฐ๋ฅผ ์ฐธ์กฐํ์ธ์.