Route Handlers

김동현·2026년 3월 4일

next.js 공식문서 번역

목록 보기
15/79

라우트 핸들러 (Route Handlers)

라우트 핸들러를 사용하면 Web RequestResponse API를 사용해서 특정 라우트(경로)에 대한 여러분만의 맞춤형 요청 핸들러를 만들 수 있어요.

Route.js Special File

💡 강사님의 팁 & 부연설명:
쉽게 말해서 프론트엔드 프로젝트 안에서 우리가 직접 백엔드 API(REST API)를 만들 수 있는 기능이에요! 별도의 Node.js(Express) 서버를 구축하지 않고도 데이터베이스에서 데이터를 가져오거나 수정하는 등의 서버 사이드 로직을 처리할 수 있게 해주는 아주 강력한 도구랍니다.

알아두면 좋아요: 라우트 핸들러는 app 디렉토리 내부에서만 사용할 수 있습니다. pages 디렉토리에서 사용하던 API Routes와 똑같은 역할을 하죠. 즉, API Routes와 라우트 핸들러를 함께 사용할 필요는 없습니다. (App Router 체제에서는 라우트 핸들러만 쓰시면 됩니다!)


규칙 (Convention)

라우트 핸들러는 app 디렉토리 내부의 route.js 또는 route.ts 파일에 정의합니다.

// filename="app/api/route.ts" switcher
export async function GET(request: Request) {}
// filename="app/api/route.js" switcher
export async function GET(request) {}

라우트 핸들러는 page.jslayout.js처럼 app 디렉토리 내의 어디에나 중첩해서 만들 수 있어요. 하지만 page.js가 있는 동일한 라우트 세그먼트(경로 레벨)에는 route.js 파일을 만들 수 없습니다.

💡 강사님의 팁 & 부연설명:
이게 무슨 뜻이냐면, app/about/page.jsapp/about/route.js를 같은 폴더 안에 두면 안 된다는 거예요. 왜 그럴까요? 사용자가 /about 이라는 주소로 접근했을 때, Next.js는 화면(page.js)을 보여줘야 할지, 아니면 데이터(route.js)를 보내줘야 할지 헷갈리기 때문이에요. 그래서 API용 라우트는 보통 app/api/... 처럼 별도의 폴더를 파서 분리하는 것이 실무적인 관례(Best Practice)입니다.


지원되는 HTTP 메서드 (Supported HTTP Methods)

라우트 핸들러는 다음과 같은 HTTP 메서드들을 지원합니다: GET, POST, PUT, PATCH, DELETE, HEAD, 그리고 OPTIONS 입니다. 만약 지원하지 않는 메서드가 호출되면, Next.js는 405 Method Not Allowed 응답을 자동으로 반환해 줍니다.


확장된 NextRequestNextResponse API

Next.js는 기본 RequestResponse API를 지원할 뿐만 아니라, 고급 사용 사례를 위해 편리한 헬퍼 기능들을 제공하도록 NextRequestNextResponse로 이를 확장했어요.

💡 강사님의 팁 & 부연설명:
실무에서 NextRequest는 정말 유용합니다! 기존 기본 Request 객체만으로는 쿠키를 읽거나, URL의 쿼리 파라미터(예: ?search=apple)를 파싱하는 게 꽤 번거로운데, NextRequestrequest.cookiesrequest.nextUrl.searchParams처럼 훨씬 직관적이고 쉬운 메서드를 제공해주거든요. API를 작성하실 때 타입은 꼭 NextRequest로 지정해서 사용하시는 걸 추천합니다.


캐싱 (Caching)

라우트 핸들러는 기본적으로 캐싱되지 않습니다. 하지만 GET 메서드에 한해서는 캐싱을 적용하도록 선택(opt-in)할 수 있어요. 다른 지원되는 HTTP 메서드(POST, PUT 등)는 절대 캐시되지 않습니다. GET 메서드를 캐시하려면 라우트 핸들러 파일에서 export const dynamic = 'force-static'과 같은 라우트 설정 옵션을 사용하시면 됩니다.

// filename="app/items/route.ts" switcher
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 })
}
// filename="app/items/route.js" switcher
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 메서드와 같은 파일에 나란히 배치되어 있더라도, 다른 지원되는 HTTP 메서드들은 캐시되지 않습니다.

💡 강사님의 팁 & 부연설명: POST, PUT, DELETE 등은 데이터베이스의 값을 변경하는 요청이잖아요? 이런 요청이 캐시되어버리면 실제 데이터가 수정되지 않는 치명적인 버그가 발생하겠죠. 그래서 Next.js는 똑똑하게 GET 요청만 캐싱을 허용하는 겁니다.


Cache Components와 함께 사용하기 (With Cache Components)

Cache Components 기능이 활성화되면, GET 라우트 핸들러는 애플리케이션의 일반적인 UI 라우트와 동일한 모델을 따르게 됩니다. 기본적으로 요청이 올 때마다(request time) 실행되지만, 동적(dynamic) 데이터나 런타임 데이터에 접근하지 않는다면 빌드 시점에 미리 렌더링(prerendered)될 수 있어요. 또한, 정적인 응답 안에 동적 데이터를 포함시키고 싶다면 use cache를 사용할 수도 있습니다.

아래 여러 가지 예시를 통해 확실히 이해해 볼까요?

1. 정적(Static) 예시 - 동적 데이터나 런타임 데이터에 접근하지 않으므로 빌드 시점에 미리 렌더링(prerendering) 됩니다:

// filename="app/api/project-info/route.ts"
export async function GET() {
  return Response.json({
    projectName: 'Next.js',
  })
}

2. 동적(Dynamic) 예시 - 비결정론적(non-deterministic) 연산에 접근합니다. 빌드 중에 Math.random()이 호출되면 사전 렌더링이 멈추고, 요청이 들어올 때마다 렌더링하도록 지연시킵니다:

// filename="app/api/random-number/route.ts"
export async function GET() {
  return Response.json({
    randomNumber: Math.random(),
  })
}

3. 런타임 데이터 예시 - 특정 요청에 종속적인 데이터에 접근합니다. headers()와 같은 런타임 API가 호출되면 사전 렌더링이 즉시 종료됩니다:

// filename="app/api/user-agent/route.ts"
import { headers } from 'next/headers'

export async function GET() {
  const headersList = await headers()
  const userAgent = headersList.get('user-agent')

  return Response.json({ userAgent })
}

알아두면 좋아요: GET 핸들러가 네트워크 요청, 데이터베이스 쿼리, 비동기 파일 시스템 작업, 요청 객체의 속성(예: req.url, request.headers, request.cookies, request.body), 런타임 API(cookies(), headers(), connection()) 또는 비결정론적 작업(Math.random 등)에 접근하면 사전 렌더링(Prerendering)이 중단됩니다. 즉, 자동으로 동적 라우트로 전환됩니다.

4. 캐시된(Cached) 예시 - 동적 데이터(데이터베이스 쿼리)에 접근하지만 use cache를 사용하여 캐시 처리합니다. 이렇게 하면 사전 렌더링된 응답에 이 결과를 포함시킬 수 있습니다:

// filename="app/api/products/route.ts"
import { cacheLife } from 'next/cache'

export async function GET() {
  const products = await getProducts()
  return Response.json(products)
}

async function getProducts() {
  'use cache'
  cacheLife('hours')

  return await db.query('SELECT * FROM products')
}

알아두면 좋아요: 'use cache' 지시어는 라우트 핸들러 함수 본문(body) 안에서 직접 사용할 수 없습니다. 반드시 위 예시처럼 헬퍼 함수(getProducts)로 분리해서 추출해야 합니다. 캐시된 응답은 새로운 요청이 도착했을 때 cacheLife 설정에 따라 재검증(revalidate) 됩니다.


특수 라우트 핸들러 (Special Route Handlers)

sitemap.ts, opengraph-image.tsx, icon.tsx와 같은 특수 라우트 핸들러 및 기타 메타데이터 파일들은 동적 API나 동적 설정 옵션을 사용하지 않는 한 기본적으로 정적(static)인 상태를 유지합니다.

💡 강사님의 팁 & 부연설명:
SEO(검색 엔진 최적화)를 위해 사이트맵이나 메타 이미지를 동적으로 생성해야 할 때가 많죠? Next.js는 이런 특별한 파일들도 내부적으로는 '라우트 핸들러'처럼 처리합니다. 기본은 빌드할 때 한번 만들어지지만, 안에 DB 조회 로직 같은 걸 넣으면 요청 때마다 새로 만들어주기도 한답니다.


라우트 결정 기준 (Route Resolution)

route를 가장 낮은 수준의 라우팅 구성 요소(primitive)로 생각하시면 됩니다.

  • 라우트 핸들러는 page 처럼 레이아웃(layouts)이나 클라이언트 측 내비게이션에 관여하지 않습니다.
  • page.js와 동일한 경로에 route.js 파일이 있을 수 없습니다.
페이지 (Page)라우트 (Route)결과 (Result)
app/page.jsapp/route.js충돌 (Conflict)
app/page.jsapp/api/route.js유효함 (Valid)
app/[user]/page.jsapp/api/route.js유효함 (Valid)

route.js 또는 page.js 파일은 해당 라우트에 대한 모든 HTTP 메서드를 담당하게 됩니다.

// filename="app/page.ts" switcher
export default function Page() {
  return <h1>Hello, Next.js!</h1>
}

// Conflict (충돌 발생!)
// `app/route.ts`
export async function POST(request: Request) {}
// filename="app/page.js" switcher
export default function Page() {
  return <h1>Hello, Next.js!</h1>
}

// Conflict (충돌 발생!)
// `app/route.js`
export async function POST(request) {}

라우트 핸들러가 여러분의 프론트엔드 애플리케이션을 어떻게 보완하는지 더 자세히 읽어보거나, 라우트 핸들러 API 참조(API Reference)를 탐색해 보세요.


라우트 컨텍스트 헬퍼 (Route Context Helper)

TypeScript 환경에서는, 전역으로 사용 가능한 RouteContext 헬퍼를 사용해서 라우트 핸들러의 context 파라미터 타입을 쉽게 지정할 수 있어요:

// filename="app/users/[id]/route.ts" switcher
import type { NextRequest } from 'next/server'

export async function GET(_req: NextRequest, ctx: RouteContext<'/users/[id]'>) {
  const { id } = await ctx.params
  return Response.json({ id })
}

알아두면 좋아요

  • 위와 같은 타입들은 next dev, next build, 또는 next typegen 명령어를 실행하는 동안 자동으로 생성됩니다.

💡 강사님의 팁 & 부연설명: [id] 처럼 동적 라우팅을 사용할 때 URL에서 값을 뽑아오기 위해 context.params를 쓰게 되는데요. 예전에는 타입을 직접 { params: { id: string } } 이렇게 타이핑해야 해서 귀찮았는데, 이제 RouteContext<경로> 하나면 완벽하게 타입 추론이 되니 TypeScript 유저라면 꼭 도입해보세요!


API 참조 (API Reference)

라우트 핸들러에 대해 더 깊이 알아보세요.


모든 문서에 대한 의미론적 개요를 보시려면 /docs/sitemap.md를 참조하세요.

사용 가능한 모든 문서의 색인(Index)을 보시려면 /docs/llms.txt를 참조하세요.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글