Next.js를 Frontend의 Backend로 사용하는 방법
Next.js는 "Backend for Frontend" 패턴을 지원합니다. 이를 통해 HTTP 요청을 처리하고 HTML뿐만 아니라 모든 콘텐츠 타입을 반환할 수 있는 공개 엔드포인트를 만들 수 있습니다. 또한 데이터 소스에 액세스하고 원격 데이터 업데이트와 같은 부수 효과를 수행할 수도 있습니다.
새 프로젝트를 시작하는 경우, --api 플래그와 함께 create-next-app을 사용하면 새 프로젝트의 app/ 폴더에 API 엔드포인트 생성 방법을 보여주는 예제 route.ts가 자동으로 포함됩니다.
npx create-next-app@latest --api
- 공개적으로 접근 가능
- 모든 HTTP 요청 처리
- 모든 콘텐츠 타입 반환 가능
이 패턴을 구현하려면 다음을 사용하세요:
- Route Handlers
- middleware
- Pages Router에서는 API Routes
공개 엔드포인트 (Public Endpoints)
Route Handlers는 공개 HTTP 엔드포인트입니다. 모든 클라이언트가 액세스할 수 있습니다.
route.ts 또는 route.js 파일 규칙을 사용하여 Route Handler를 생성합니다:
// /app/api/route.ts
export function GET(request: Request) {}
이는 /api로 전송된 GET 요청을 처리합니다.
예외를 던질 수 있는 작업에는 try/catch 블록을 사용하세요:
// /app/api/route.ts
import { submit } from '@/lib/submit'
export async function POST(request: Request) {
try {
await submit(request)
return new Response(null, { status: 204 })
} catch (reason) {
const message =
reason instanceof Error ? reason.message : 'Unexpected error'
return new Response(message, { status: 500 })
}
}
콘텐츠 타입 (Content Types)
Route Handlers를 사용하면 JSON, XML, 이미지, 파일, 일반 텍스트 등 UI가 아닌 응답을 제공할 수 있습니다.
Next.js는 일반적인 엔드포인트에 대해 파일 규칙을 사용합니다:
- sitemap.xml
- opengraph-image.jpg, twitter-image
- favicon, app icon, apple-icon
- manifest.json
- robots.txt
다음과 같은 커스텀 엔드포인트도 정의할 수 있습니다:
- llms.txt
- rss.xml
- .well-known
예를 들어, app/rss.xml/route.ts는 rss.xml에 대한 Route Handler를 생성합니다.
// /app/rss.xml/route.ts
export async function GET(request: Request) {
const rssResponse = await fetch(/* rss endpoint */)
const rssData = await rssResponse.json()
const rssFeed = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>${rssData.title}</title>
<description>${rssData.description}</description>
<link>${rssData.link}</link>
<copyright>${rssData.copyright}</copyright>
${rssData.items.map((item) => {
return `<item>
<title>${item.title}</title>
<description>${item.description}</description>
<link>${item.link}</link>
<pubDate>${item.publishDate}</pubDate>
<guid isPermaLink="false">${item.guid}</guid>
</item>`
})}
</channel>
</rss>`
const headers = new Headers({ 'content-type': 'application/xml' })
return new Response(rssFeed, { headers })
}
요청 페이로드 소비
요청 본문에 액세스하려면 .json(), .formData() 또는 .text()와 같은 Request 인스턴스 메서드를 사용하세요.
GET 및 HEAD 요청은 본문을 포함하지 않습니다.
// /app/api/echo-body/route.ts
export async function POST(request: Request) {
const res = await request.json()
return Response.json({ res })
}
// /app/api/send-email/route.ts
import { sendMail, validateInputs } from '@/lib/email-transporter'
export async function POST(request: Request) {
const formData = await request.formData()
const email = formData.get('email')
const contents = formData.get('contents')
try {
await validateInputs({ email, contents })
const info = await sendMail({ email, contents })
return Response.json({ messageId: info.messageId })
} catch (reason) {
const message =
reason instanceof Error ? reason.message : 'Unexpected exception'
return new Response(message, { status: 500 })
}
}
요청 본문은 한 번만 읽을 수 있습니다. 다시 읽어야 하는 경우 요청을 복제하세요:
// /app/api/clone/route.ts
export async function POST(request: Request) {
try {
const clonedRequest = request.clone()
await request.body()
await clonedRequest.body()
await request.body() // 에러 발생
return new Response(null, { status: 204 })
} catch {
return new Response(null, { status: 500 })
}
}
데이터 조작
Route Handlers는 하나 이상의 소스에서 데이터를 변환, 필터링 및 집계할 수 있습니다. 이를 통해 로직을 프론트엔드에서 분리하고 내부 시스템 노출을 방지할 수 있습니다.
또한 무거운 계산을 서버로 오프로드하여 클라이언트 배터리 및 데이터 사용량을 줄일 수 있습니다.
// /app/api/weather/route.ts
import { parseWeatherData } from '@/lib/weather'
export async function POST(request: Request) {
const body = await request.json()
const searchParams = new URLSearchParams({ lat: body.lat, lng: body.lng })
try {
const weatherResponse = await fetch(`${weatherEndpoint}?${searchParams}`)
if (!weatherResponse.ok) {
/* handle error */
}
const weatherData = await weatherResponse.text()
const payload = parseWeatherData.asJSON(weatherData)
return new Response(payload, { status: 200 })
} catch (reason) {
const message =
reason instanceof Error ? reason.message : 'Unexpected exception'
return new Response(message, { status: 500 })
}
}
백엔드로 프록시하기
Route Handler를 다른 백엔드의 프록시로 사용할 수 있습니다. 요청을 전달하기 전에 검증 로직을 추가하세요.
// /app/api/[...slug]/route.ts
import { isValidRequest } from '@/lib/utils'
export async function POST(request: Request, { params }) {
const clonedRequest = request.clone()
const isValid = await isValidRequest(clonedRequest)
if (!isValid) {
return new Response(null, { status: 400, statusText: 'Bad Request' })
}
const { slug } = await params
const pathname = slug.join('/')
const proxyURL = new URL(pathname, 'https://nextjs.org')
const proxyRequest = new Request(proxyURL, request)
try {
return fetch(proxyRequest)
} catch (reason) {
const message =
reason instanceof Error ? reason.message : 'Unexpected exception'
return new Response(message, { status: 500 })
}
}
또는 다음을 사용할 수도 있습니다:
- middleware rewrites
- next.config.js의 rewrites
NextRequest와 NextResponse
Next.js는 일반적인 작업을 단순화하는 메서드로 Request와 Response Web API를 확장합니다. 이러한 확장은 Route Handlers와 Middleware 모두에서 사용할 수 있습니다.
둘 다 쿠키를 읽고 조작하는 메서드를 제공합니다.
NextRequest는 들어오는 요청의 파싱된 값을 노출하는 nextUrl 속성을 포함합니다. 예를 들어, 요청 경로와 검색 매개변수에 더 쉽게 액세스할 수 있습니다.
NextResponse는 next(), json(), redirect(), rewrite()와 같은 헬퍼를 제공합니다.
// /app/echo-pathname/route.ts
import { type NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const nextUrl = request.nextUrl
if (nextUrl.searchParams.get('redirect')) {
return NextResponse.redirect(new URL('/', request.url))
}
if (nextUrl.searchParams.get('rewrite')) {
return NextResponse.rewrite(new URL('/', request.url))
}
return NextResponse.json({ pathname: nextUrl.pathname })
}
웹훅과 콜백 URL
Route Handlers를 사용하여 타사 애플리케이션으로부터 이벤트 알림을 받을 수 있습니다.
예를 들어, CMS에서 콘텐츠가 변경될 때 경로를 재검증합니다. 변경 시 특정 엔드포인트를 호출하도록 CMS를 구성하세요.
// /app/webhook/route.ts
import { type NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get('token')
if (token !== process.env.REVALIDATE_SECRET_TOKEN) {
return NextResponse.json({ success: false }, { status: 401 })
}
const tag = request.nextUrl.searchParams.get('tag')
if (!tag) {
return NextResponse.json({ success: false }, { status: 400 })
}
revalidateTag(tag)
return NextResponse.json({ success: true })
}
콜백 URL은 또 다른 사용 사례입니다. 사용자가 타사 플로우를 완료하면 타사가 콜백 URL로 사용자를 보냅니다. Route Handler를 사용하여 응답을 확인하고 사용자를 리디렉션할 위치를 결정하세요.
// /app/auth/callback/route.ts
import { type NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get('session_token')
const redirectUrl = request.nextUrl.searchParams.get('redirect_url')
const response = NextResponse.redirect(new URL(redirectUrl, request.url))
response.cookies.set({
value: token,
name: '_token',
path: '/',
secure: true,
httpOnly: true,
expires: undefined, // session cookie
})
return response
}
웹훅과 콜백 URL의 실제 사용 사례:
- Stripe 결제 완료 알림 - 결제 상태 업데이트
- GitHub Actions 배포 완료 - 캐시 무효화
- Contentful CMS 콘텐츠 업데이트 - 페이지 재검증
- OAuth 인증 완료 - 사용자 세션 설정
리디렉션
// app/api/route.ts
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
redirect('https://nextjs.org/')
}
Middleware
프로젝트당 하나의 middleware 파일만 허용됩니다. config.matcher를 사용하여 특정 경로를 대상으로 하세요.
middleware를 사용하여 요청이 경로 경로에 도달하기 전에 응답을 생성하세요.
// middleware.ts
import { isAuthenticated } from '@lib/auth'
export const config = {
matcher: '/api/:function*',
}
export function middleware(request: Request) {
if (!isAuthenticated(request)) {
return Response.json(
{ success: false, message: 'authentication failed' },
{ status: 401 }
)
}
}
middleware를 사용하여 요청을 프록시할 수도 있습니다:
// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request: Request) {
if (request.nextUrl.pathname === '/proxy-this-path') {
const rewriteUrl = new URL('https://nextjs.org')
return NextResponse.rewrite(rewriteUrl)
}
}
middleware가 생성할 수 있는 또 다른 유형의 응답은 리디렉션입니다:
// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request: Request) {
if (request.nextUrl.pathname === '/v1/docs') {
request.nextUrl.pathname = '/v2/docs'
return NextResponse.redirect(request.nextUrl)
}
}
보안 (Security)
Rate Limiting
Next.js 백엔드에서 Rate Limiting을 구현할 수 있습니다. 코드 기반 검사 외에도 호스트에서 제공하는 Rate Limiting 기능을 활성화하세요.
// /app/resource/route.ts
import { NextResponse } from 'next/server'
import { checkRateLimit } from '@/lib/rate-limit'
export async function POST(request: Request) {
const { rateLimited } = await checkRateLimit(request)
if (rateLimited) {
return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 })
}
return new Response(null, { status: 204 })
}
페이로드 검증
- 신뢰하지 말기: 들어오는 요청 데이터를 절대 신뢰하지 마세요
- 콘텐츠 타입과 크기 검증: 사용하기 전에 XSS 방지를 위해 위생 처리
- 타임아웃 사용: 남용을 방지하고 서버 리소스 보호
- 전용 서비스 사용: 사용자 생성 정적 자산을 전용 서비스에 저장
- 자격 증명 확인: 액세스 권한을 부여하기 전에 항상 자격 증명 확인
- 민감한 데이터 제거: 응답과 백엔드 로그에서 민감하거나 불필요한 데이터 제거
- 정기적 순환: 자격 증명과 API 키를 정기적으로 순환
Preflight 요청
Preflight 요청은 OPTIONS 메서드를 사용하여 원본, 메서드 및 헤더를 기반으로 요청이 허용되는지 서버에 묻습니다.
OPTIONS가 정의되지 않은 경우, Next.js가 자동으로 추가하고 다른 정의된 메서드를 기반으로 Allow 헤더를 설정합니다.
라이브러리 패턴
커뮤니티 라이브러리는 종종 Route Handlers에 대해 팩토리 패턴을 사용합니다.
// /app/api/[...path]/route.ts
import { createHandler } from 'third-party-library'
const handler = createHandler({
/* library-specific options */
})
export const GET = handler
// 또는
export { handler as POST }
이는 GET 및 POST 요청에 대한 공유 핸들러를 생성합니다. 라이브러리는 요청의 method와 pathname을 기반으로 동작을 커스터마이즈합니다.
라이브러리는 middleware 팩토리도 제공할 수 있습니다.
// middleware.ts
import { createMiddleware } from 'third-party-library'
export default createMiddleware()
제한사항 (Caveats)
Server Components
Server Components에서는 Route Handlers를 통하지 않고 소스에서 직접 데이터를 가져오세요.
빌드 시 사전 렌더링되는 Server Components의 경우, Route Handlers를 사용하면 빌드 단계가 실패합니다. 빌드하는 동안 이러한 요청을 수신하는 서버가 없기 때문입니다.
온디맨드로 렌더링되는 Server Components의 경우, Route Handlers에서 가져오는 것은 핸들러와 렌더링 프로세스 간의 추가 HTTP 왕복으로 인해 더 느립니다.
서버 사이드 fetch 요청은 절대 URL을 사용합니다. 이는 외부 서버로의 HTTP 왕복을 의미합니다.
- 개발 중: 개발 서버가 외부 서버 역할
- 빌드 시: 서버가 없음
- 런타임: 공개 도메인을 통해 서버 사용 가능
Export 모드
export 모드는 런타임 서버 없이 정적 사이트를 출력합니다. Next.js 런타임이 필요한 기능은 지원되지 않습니다.
export 모드에서는 dynamic 경로 세그먼트 구성이 'force-static'으로 설정된 GET Route Handlers만 지원됩니다.
// app/hello-world/route.ts
export const dynamic = 'force-static'
export function GET() {
return new Response('Hello World', { status: 200 })
}
배포 환경
일부 호스트는 Route Handlers를 람다 함수로 배포합니다. 이는 다음을 의미합니다:
- Route Handlers는 요청 간에 데이터를 공유할 수 없음
- 환경이 파일 시스템 쓰기를 지원하지 않을 수 있음
- 오래 실행되는 핸들러는 타임아웃으로 인해 종료될 수 있음
- WebSocket은 타임아웃이나 응답 생성 후 연결이 닫히므로 작동하지 않음
프로덕션 배포 시 고려사항:
- Vercel: Edge Runtime 사용으로 더 빠른 응답
- AWS Lambda: 콜드 스타트 최소화를 위한 keep-warm 전략
- Docker: 컨테이너 최적화로 시작 시간 단축
- CloudFlare Workers: 글로벌 엣지 배포로 지연 시간 최소화
'(준)공식 문서 > Next.js' 카테고리의 다른 글
| [ Next.js 15 공식 문서 ] Caching (캐싱) (3) | 2025.07.27 |
|---|---|
| [ Nexts.js 15 공식 문서 ] Authentication (인증) (2) | 2025.07.27 |
| [ Next.js 15 공식 문서 ] Analytics (분석 하기) (3) | 2025.07.27 |
| [ Next.js 15 공식 문서 ] Route Handlers and Middleware (라우트 핸들러와 미들웨어) (0) | 2025.07.20 |
| [ Next.js 15 공식 문서 ] Metadata and OG images (메타 데이터와 OG 이미지) (0) | 2025.07.20 |