THE DEVLOG

scribbly.

Next JS 치트 시트

2025.05.10 18:42:08

route.ts : Route Handler

Next.js의 Route Handler는 API 엔드포인트를 정의하기 위한 구조이다. app/ 디렉토리 내부에서 route.ts 또는 route.js 파일로 작성하며, 경로는 해당 파일이 위치한 디렉토리 구조에 따라 결정된다.

Page Route에서는 api 폴더 내에서 정의해야 했으나, Next.js 15의 App Router에서는 pages/api 대신 app/api/**/route.ts 파일을 사용한다. 해당 경로에 route.ts 파일이 있다면 해당 경로는 서버의 API 엔드 포인트로 작동한다. (단, route.ts와 page.ts가 같은 경로에 있을 수는 없다.)

Route Handler의 규칙 (Convention)

Next.js에서 Route Handler는 app/ 디렉토리 내부에 위치한 route.ts 또는 route.js 파일로 정의한다. 이 파일은 API 엔드포인트를 구성하며, 위치한 경로에 따라 URL이 결정된다.

예시:

app/api/route.ts → /api

Route Handler는 page.tsx, layout.tsx와 마찬가지로 app/ 내부의 어느 위치에나 중첩(nested)해서 작성할 수 있다. 단, 동일한 경로 레벨에 page.tsxroute.ts를 동시에 작성하는 것은 허용되지 않는다.

지원되는 HTTP 메서드

Route Handler에서는 다음과 같은 HTTP 메서드가 지원된다:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • HEAD
  • OPTIONS

각 메서드는 별도의 함수로 export하며, 요청에 따라 해당 함수가 실행된다. 예시:

// app/api/route.ts
export async function GET(request: Request) {
  return new Response("GET 요청 처리 결과");
}

export async function POST(request: Request) {
  const body = await request.json();
  return new Response(`POST로 받은 데이터: ${body.name}`);
}

지원되지 않는 HTTP 메서드로 요청할 경우, Next.js는 자동으로 405 Method Not Allowed 응답을 반환한다.

NextRequest와 NextResponse

기본적으로 Route Handler는 Web API의 표준 객체인 RequestResponse를 사용한다. 여기에 더해, Next.js는 이 객체들을 확장한 NextRequestNextResponse를 제공한다.

NextRequest

NextRequestRequest 객체에 추가적인 기능을 제공한다. 대표적으로 다음과 같은 속성이 있다:

  • request.cookies.get()
  • request.headers.get()
  • request.nextUrl → URL 객체 (pathname, searchParams 등 접근 가능)

NextResponse

NextResponseResponse 객체에 응답 관련 도우미 기능을 확장한 형태이다:

  • NextResponse.redirect(url)
  • NextResponse.json(data)
  • NextResponse.rewrite(url)
  • NextResponse.next() (middleware에서 사용)

이 확장 API를 통해 요청을 더 세밀하게 제어하거나, 미들웨어 및 캐싱과 함께 사용할 수 있다.

Route Handler의 작동 방식 (Behavior)

캐싱(Caching)

Route Handler는 기본적으로 캐시되지 않는다. 단, GET 메서드에 한해서 명시적으로 캐싱을 설정할 수 있다. 이를 위해 export const dynamic = 'force-static' 또는 export const revalidate = <초 단위> 와 같은 설정을 사용한다.

// app/items/route.ts

export const dynamic = 'force-static';

export async function GET() {
  const res = await fetch('https://data.mongodb-api.com/...', {
    headers: {
      'Content-Type': 'application/json',
      'API-Key': process.env.DATA_API_KEY,
    },
  });

  const data = await res.json();
  return Response.json({ data });
}
  • GET 외의 메서드 (POST, PUT, 등)는 어떤 설정을 하더라도 캐시되지 않는다.
  • 동일한 파일에 GET, POST가 함께 있어도 POST는 캐시 대상이 아니다.

특별한 Route Handler들

다음과 같은 메타데이터 목적의 Route Handler들은 기본적으로 정적(static)이다. 하지만 내부에서 동적 API 호출을 사용하거나 dynamic = 'force-dynamic' 등을 선언하면 동적으로 처리된다:

  • sitemap.ts (사이트맵 XML)
  • opengraph-image.tsx (OG 이미지)
  • icon.tsx (favicon 등)

이들은 페이지 라우팅과는 별도로 동작하며, 정적인 리소스를 생성하는 데 사용된다.


Route Resolution (라우트 해석 규칙)

  • Route Handler는 라우팅의 가장 하위 수준에서 동작하는 단위이며, layout.tsx, loading.tsx, error.tsx 등과는 연결되지 않는다.
  • 클라이언트 측 내비게이션에도 포함되지 않는다.
  • 동일 경로에 page.tsxroute.ts를 함께 둘 수 없다. 이 경우 충돌이 발생한다.
경로 구조설명
app/page.tsx + app/route.ts충돌 (Conflict)
app/page.tsx + app/api/route.ts유효 (Valid)
app/[user]/page.tsx + app/api/route.ts유효 (Valid)
  • route.ts 또는 page.tsx 파일은 해당 경로에서의 모든 HTTP 메서드를 처리하게 된다.

예시:

// app/page.tsx
export default function Page() {
  return <h1>Hello, Next.js!</h1>;
}

// 같은 경로에서 route.ts 작성 시 충돌 발생
export async function POST(request: Request) {}

예제

Revalidate: ISR을 통한 캐시 데이터 갱신

GET 요청에 대해 revalidate 값을 설정하면 해당 경로는 Incremental Static Regeneration을 사용하여 일정 시간마다 데이터를 갱신한다.

// app/posts/route.ts

export const revalidate = 60

export async function GET() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()

  return Response.json(posts)
}

Cookies: 쿠키 읽기 및 설정

next/headerscookies()를 통해 서버 컨텍스트에서 쿠키를 읽거나 설정할 수 있다.

// app/api/route.ts

import { cookies } from 'next/headers'

export async function GET(request: Request) {
  const cookieStore = cookies()
  const token = cookieStore.get('token')

  return new Response('Hello, Next.js!', {
    status: 200,
    headers: { 'Set-Cookie': `token=${token?.value}` },
  })
}

또는 NextRequest 객체를 통해 클라이언트 요청으로부터 직접 쿠키를 읽을 수도 있다.

// app/api/route.ts

import { type NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const token = request.cookies.get('token')
}

Headers: 요청 헤더 읽기

next/headersheaders()를 사용하면 서버 컨텍스트에서 요청 헤더를 읽을 수 있다.

// app/api/route.ts

import { headers } from 'next/headers'

export async function GET(request: Request) {
  const headersList = headers()
  const referer = headersList.get('referer')

  return new Response('Hello, Next.js!', {
    status: 200,
    headers: { referer: referer ?? '' },
  })
}

또는 NextRequestrequest.headers를 통해 직접 접근할 수 있다.

// app/api/route.ts

import { type NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const requestHeaders = new Headers(request.headers)
}

Redirects

next/navigation에서 제공하는 redirect() 함수를 사용하면 서버에서 요청을 다른 URL로 리디렉션할 수 있다. 이 함수는 Response 객체를 직접 반환하지 않고, 즉시 리디렉션 처리를 수행한다.

// app/api/route.ts

import { redirect } from 'next/navigation'

export async function GET(request: Request) {
  redirect('https://nextjs.org/')
}

Dynamic Route Segments

동적 세그먼트를 사용하면 URL 경로의 일부를 변수처럼 받아 처리할 수 있다. 이때 paramsPromise 형태로 전달되므로 await을 통해 구조 분해 할당해야 한다.

// app/items/[slug]/route.ts

export async function GET(
  request: Request,
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params
  // slug는 'a', 'b', 'c' 등 경로에 따라 동적으로 할당됨
}
경로 파일예시 URLparams 결과
app/items/[slug]/route.ts/items/a{ slug: 'a' }
app/items/[slug]/route.ts/items/b{ slug: 'b' }
app/items/[slug]/route.ts/items/c{ slug: 'c' }

URL Query Parameters

Route Handler의 request 객체는 NextRequest 타입이며, nextUrl.searchParams를 통해 쿼리 파라미터에 접근할 수 있다.

// app/api/search/route.ts

import { type NextRequest } from 'next/server'

export function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const query = searchParams.get('query')
  // /api/search?query=hello → query === 'hello'
}

Streaming

AI 콘텐츠 생성과 같은 상황에서는 스트리밍 응답이 필요하다. AI SDK 등의 고수준 추상화 도구를 사용할 수 있으며, Web API만으로도 직접 스트리밍을 구현할 수 있다.

AI SDK를 사용하는 예:

// app/api/chat/route.ts

import { openai } from '@ai-sdk/openai'
import { StreamingTextResponse, streamText } from 'ai'

export async function POST(req: Request) {
  const { messages } = await req.json()
  const result = await streamText({
    model: openai('gpt-4-turbo'),
    messages,
  })

  return new StreamingTextResponse(result.toAIStream())
}

Web API를 직접 사용하는 예:

// app/api/route.ts

function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

const encoder = new TextEncoder()

async function* makeIterator() {
  yield encoder.encode('<p>One</p>')
  await sleep(200)
  yield encoder.encode('<p>Two</p>')
  await sleep(200)
  yield encoder.encode('<p>Three</p>')
}

function iteratorToStream(iterator: AsyncGenerator<Uint8Array>) {
  return new ReadableStream({
    async pull(controller) {
      const { value, done } = await iterator.next()
      if (done) {
        controller.close()
      } else {
        controller.enqueue(value)
      }
    },
  })
}

export async function GET() {
  const iterator = makeIterator()
  const stream = iteratorToStream(iterator)
  return new Response(stream)
}

Request Body

Route Handler에서는 Web API의 표준 메서드를 사용하여 요청 본문을 읽을 수 있다.

// app/items/route.ts

export async function POST(request: Request) {
  const res = await request.json()
  return Response.json({ res })
}

Request Body: FormData

multipart/form-data 형식의 요청은 request.formData() 메서드를 통해 처리할 수 있다. 이 메서드는 파일 업로드나 일반 입력값 전송 시 주로 사용된다.

// app/items/route.ts

export async function POST(request: Request) {
  const formData = await request.formData()
  const name = formData.get('name')
  const email = formData.get('email')
  return Response.json({ name, email })
}

FormData는 기본적으로 문자열로 처리되므로, 필요 시 zod-form-data 등의 유효성 검증 도구를 사용할 수 있다.

CORS 설정

특정 Route Handler에서 CORS 헤더를 명시적으로 설정할 수 있다. 이는 외부 도메인의 요청을 허용하고자 할 때 사용된다.

// app/api/route.ts

export async function GET(request: Request) {
  return new Response('Hello, Next.js!', {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}

여러 라우트에 동일한 CORS 정책을 적용하려면 middleware.ts 또는 next.config.js에 공통 설정을 두는 것이 효율적이다.

Webhooks

Route Handler는 외부 서비스에서 전송하는 Webhook을 수신하는 데 사용할 수 있다. 일반적으로 request.text()를 사용하여 원본 문자열을 파싱한 뒤 처리한다.

// app/api/route.ts

export async function POST(request: Request) {
  try {
    const text = await request.text()
    // Webhook 데이터 처리
  } catch (error) {
    return new Response(`Webhook error: ${error.message}`, { status: 400 })
  }

  return new Response('Success!', { status: 200 })
}

Pages Router와는 달리 별도의 bodyParser 설정이 필요하지 않다.

Non-UI 응답

Route Handler는 UI가 아닌 콘텐츠를 응답하는 데 사용할 수 있다. 예를 들어 RSS, XML, 텍스트 등을 직접 반환할 수 있다.

// app/rss.xml/route.ts

export async function GET() {
  return new Response(
    `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
  <channel>
    <title>Next.js Documentation</title>
    <link>https://nextjs.org/docs</link>
    <description>The React Framework for the Web</description>
  </channel>
</rss>`,
    {
      headers: { 'Content-Type': 'text/xml' },
    }
  )
}

Segment Config Options

Route Handler는 페이지나 레이아웃과 동일한 세그먼트 구성 옵션(segment config options)을 사용할 수 있다.

// app/items/route.ts

export const dynamic = 'auto'
export const dynamicParams = true
export const revalidate = false
export const fetchCache = 'auto'
export const runtime = 'nodejs'
export const preferredRegion = 'auto'
  • dynamic: 'auto', 'force-static', 'force-dynamic' 중 선택
  • revalidate: false 또는 초 단위 숫자
  • runtime: 'edge' 또는 'nodejs'
  • preferredRegion: Vercel 환경에서 리전 최적화를 위한 설정