Next.js 라우트 핸들러를 사용하여 강력한 API 엔드포인트를 만드는 방법을 알아보세요. 이 가이드는 기본 설정부터 고급 기술까지 실제 예제와 모범 사례를 포함하여 모든 것을 다룹니다.
Next.js 라우트 핸들러: API 엔드포인트 생성을 위한 종합 가이드
Next.js는 서버 사이드 렌더링, 정적 사이트 생성, 그리고 이제 라우트 핸들러와 같은 강력한 기능으로 웹 애플리케이션 구축 방식에 혁신을 가져왔습니다. 라우트 핸들러는 Next.js 애플리케이션 내에서 직접 API 엔드포인트를 생성하는 유연하고 효율적인 방법을 제공합니다. 이 가이드에서는 라우트 핸들러의 개념, 이점, 그리고 이를 효과적으로 사용하여 견고한 API를 구축하는 방법을 탐색합니다.
Next.js 라우트 핸들러란 무엇인가요?
라우트 핸들러는 Next.js 프로젝트의 app
디렉토리 내에 정의되어 들어오는 HTTP 요청을 처리하는 함수입니다. 기존의 pages/api
접근 방식(API 라우트 사용)과 달리, 라우트 핸들러는 React 컴포넌트와 함께 API 엔드포인트를 정의하는 더 간소화되고 유연한 방법을 제공합니다. 이것은 본질적으로 엣지(edge) 또는 선택한 서버 환경에서 실행되는 서버리스 함수입니다.
라우트 핸들러를 요청 처리, 데이터베이스와의 상호 작용, 응답 반환을 담당하는 Next.js 애플리케이션의 백엔드 로직으로 생각할 수 있습니다.
라우트 핸들러 사용의 이점
- 코드 배치(Colocation): 라우트 핸들러는
app
디렉토리 내의 React 컴포넌트 바로 옆에 위치하여 더 나은 코드 구성과 유지보수성을 촉진합니다. - TypeScript 지원: 내장된 TypeScript 지원은 타입 안정성과 향상된 개발자 경험을 보장합니다.
- 미들웨어 통합: 인증, 인가, 요청 유효성 검사와 같은 작업을 위해 미들웨어를 쉽게 통합할 수 있습니다.
- 스트리밍 지원: 라우트 핸들러는 데이터를 스트리밍할 수 있어 응답을 점진적으로 보낼 수 있으며, 이는 대용량 데이터셋이나 장기 실행 프로세스에 유용합니다.
- 엣지 함수(Edge Functions): 글로벌 CDN을 활용하여 사용자에게 더 가까운 곳에서 낮은 지연 시간의 응답을 위해 라우트 핸들러를 엣지 함수로 배포할 수 있습니다.
- 간소화된 API 디자인: 라우트 핸들러는 요청과 응답을 처리하기 위한 깔끔하고 직관적인 API를 제공합니다.
- 서버 액션 통합: 서버 액션과의 긴밀한 통합을 통해 클라이언트 사이드 컴포넌트와 서버 사이드 로직 간의 원활한 통신이 가능합니다.
Next.js 프로젝트 설정하기
라우트 핸들러를 시작하기 전에, app
디렉토리가 포함된 Next.js 프로젝트가 설정되어 있는지 확인하세요. 새 프로젝트를 시작하는 경우 다음 명령을 사용하세요:
npx create-next-app@latest my-nextjs-app
설정 과정에서 app
디렉토리를 선택하여 새로운 라우팅 시스템을 활성화하세요.
첫 번째 라우트 핸들러 생성하기
JSON 응답을 반환하는 간단한 API 엔드포인트를 만들어 보겠습니다. app
디렉토리 내에 예를 들어 /app/api/hello
와 같은 새 디렉토리를 만드세요. 이 디렉토리 안에 route.ts
(TypeScript를 사용하지 않는 경우 route.js
)라는 이름의 파일을 만드세요.
첫 번째 라우트 핸들러의 코드는 다음과 같습니다:
// app/api/hello/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
return NextResponse.json({ message: 'Hello from Next.js Route Handlers!' });
}
설명:
import { NextResponse } from 'next/server';
: API 응답을 구성하는 데 사용되는NextResponse
객체를 가져옵니다.export async function GET(request: Request) { ... }
:/api/hello
엔드포인트로의 GET 요청을 처리하는 비동기 함수를 정의합니다.request
매개변수는 들어오는 요청 객체에 대한 접근을 제공합니다.return NextResponse.json({ message: 'Hello from Next.js Route Handlers!' });
: 메시지가 포함된 JSON 응답을 생성하고NextResponse.json()
을 사용하여 반환합니다.
이제 브라우저에서 /api/hello
로 이동하거나 curl
또는 Postman
과 같은 도구를 사용하여 이 엔드포인트에 접근할 수 있습니다.
다양한 HTTP 메서드 처리하기
라우트 핸들러는 GET, POST, PUT, DELETE, PATCH, OPTIONS와 같은 다양한 HTTP 메서드를 지원합니다. 동일한 route.ts
파일 내에서 각 메서드에 대한 별도의 함수를 정의할 수 있습니다.
// app/api/users/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
// 데이터베이스에서 모든 사용자를 가져오는 로직
const users = [{ id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' }]; // 예제 데이터
return NextResponse.json(users);
}
export async function POST(request: Request) {
const data = await request.json(); // 요청 본문을 JSON으로 파싱
// 'data'를 사용하여 데이터베이스에 새 사용자를 생성하는 로직
const newUser = { id: 3, name: data.name, email: data.email }; // 예제
return NextResponse.json(newUser, { status: 201 }); // 201 Created 상태 코드와 함께 새 사용자를 반환
}
설명:
GET
함수는 사용자 목록을 검색하고(여기서는 시뮬레이션됨) JSON 응답으로 반환합니다.POST
함수는 요청 본문을 JSON으로 파싱하고, 새 사용자를 생성하며(시뮬레이션됨), 201 Created 상태 코드와 함께 새 사용자를 반환합니다.
요청 데이터 접근하기
request
객체는 헤더, 쿼리 매개변수, 요청 본문을 포함한 들어오는 요청에 대한 다양한 정보에 대한 접근을 제공합니다.
헤더
request.headers
속성을 사용하여 요청 헤더에 접근할 수 있습니다:
export async function GET(request: Request) {
const userAgent = request.headers.get('user-agent');
console.log('User Agent:', userAgent);
return NextResponse.json({ userAgent });
}
쿼리 매개변수
쿼리 매개변수에 접근하려면 URL
생성자를 사용할 수 있습니다:
export async function GET(request: Request) {
const url = new URL(request.url);
const searchParams = new URLSearchParams(url.search);
const id = searchParams.get('id');
console.log('ID:', id);
return NextResponse.json({ id });
}
요청 본문
POST, PUT, PATCH 요청의 경우, 콘텐츠 유형에 따라 request.json()
또는 request.text()
메서드를 사용하여 요청 본문에 접근할 수 있습니다.
export async function POST(request: Request) {
const data = await request.json();
console.log('Data:', data);
return NextResponse.json({ receivedData: data });
}
응답 반환하기
NextResponse
객체는 API 응답을 구성하는 데 사용됩니다. 헤더, 상태 코드, 응답 본문을 설정하기 위한 여러 메서드를 제공합니다.
JSON 응답
NextResponse.json()
메서드를 사용하여 JSON 응답을 반환하세요:
return NextResponse.json({ message: 'Success!', data: { name: 'John Doe' } }, { status: 200 });
텍스트 응답
new Response()
생성자를 사용하여 일반 텍스트 응답을 반환하세요:
return new Response('Hello, world!', { status: 200, headers: { 'Content-Type': 'text/plain' } });
리디렉션
NextResponse.redirect()
를 사용하여 사용자를 다른 URL로 리디렉션하세요:
import { redirect } from 'next/navigation';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
return NextResponse.redirect(new URL('/new-location', request.url));
}
헤더 설정
NextResponse.json()
또는 new Response()
의 headers
옵션을 사용하여 사용자 정의 헤더를 설정할 수 있습니다:
return NextResponse.json({ message: 'Success!' }, { status: 200, headers: { 'Cache-Control': 'no-cache' } });
미들웨어 통합
미들웨어를 사용하면 요청이 라우트 핸들러에 의해 처리되기 전에 코드를 실행할 수 있습니다. 이는 인증, 인가, 로깅 및 기타 공통 관심사에 유용합니다.
미들웨어를 생성하려면 app
디렉토리 또는 하위 디렉토리에 middleware.ts
(또는 middleware.js
)라는 이름의 파일을 만드세요. 미들웨어는 해당 디렉토리와 그 하위 디렉토리의 모든 라우트에 적용됩니다.
// app/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/protected/:path*'], // 이 미들웨어를 /protected/로 시작하는 경로에 적용
};
설명:
middleware
함수는 요청 쿠키에서 인증 토큰을 확인합니다.- 토큰이 없으면 사용자를 로그인 페이지로 리디렉션합니다.
- 그렇지 않으면 요청이 라우트 핸들러로 진행되도록 허용합니다.
config
객체는 이 미들웨어가/protected/
로 시작하는 라우트에만 적용되어야 함을 지정합니다.
오류 처리
견고한 API를 구축하기 위해서는 적절한 오류 처리가 매우 중요합니다. try...catch
블록을 사용하여 예외를 처리하고 적절한 오류 응답을 반환할 수 있습니다.
export async function GET(request: Request) {
try {
// 오류 시뮬레이션
throw new Error('Something went wrong!');
} catch (error: any) {
console.error('Error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
설명:
try...catch
블록은 라우트 핸들러 내에서 발생하는 모든 예외를 잡습니다.catch
블록에서는 오류가 기록되고 500 내부 서버 오류 상태 코드와 함께 오류 응답이 반환됩니다.
스트리밍 응답
라우트 핸들러는 스트리밍 응답을 지원하여 클라이언트에 데이터를 점진적으로 보낼 수 있습니다. 이는 특히 대용량 데이터셋이나 장기 실행 프로세스에 유용합니다.
import { Readable } from 'stream';
import { NextResponse } from 'next/server';
async function* generateData() {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 500)); // 지연 시뮬레이션
yield `Data chunk ${i}\n`;
}
}
export async function GET(request: Request) {
const readableStream = Readable.from(generateData());
return new Response(readableStream, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
설명:
generateData
함수는 지연과 함께 데이터 청크를 생성하는 비동기 생성기입니다.Readable.from()
메서드는 생성기에서 읽기 가능한 스트림을 생성합니다.Response
객체는 읽기 가능한 스트림을 본문으로 하여 생성되며,Content-Type
헤더는text/plain
으로 설정됩니다.
인증 및 인가
API 엔드포인트를 보호하는 것은 매우 중요합니다. 미들웨어를 사용하거나 라우트 핸들러 내에서 직접 인증 및 인가를 구현할 수 있습니다.
인증
인증은 요청을 보내는 사용자의 신원을 확인합니다. 일반적인 인증 방법은 다음과 같습니다:
- JWT (JSON 웹 토큰): 성공적인 로그인 시 토큰을 생성하고 후속 요청에서 이를 확인합니다.
- 세션 기반 인증: 쿠키를 사용하여 세션 식별자를 저장하고 각 요청에서 이를 확인합니다.
- OAuth: Google이나 Facebook과 같은 제3자 제공업체에 인증을 위임합니다.
다음은 미들웨어를 사용한 JWT 인증의 예입니다:
// app/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import jwt from 'jsonwebtoken';
const secret = process.env.JWT_SECRET || 'your-secret-key'; // 강력하고 무작위로 생성된 비밀 키로 교체하세요
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value;
if (!token) {
return NextResponse.json({ message: 'Authentication required' }, { status: 401 });
}
try {
jwt.verify(token, secret);
return NextResponse.next();
} catch (error) {
return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
}
}
export const config = {
matcher: ['/api/protected/:path*'],
};
인가
인가는 사용자가 접근할 수 있는 리소스를 결정합니다. 이는 일반적으로 역할이나 권한을 기반으로 합니다.
라우트 핸들러 내에서 사용자의 역할이나 권한을 확인하고 접근 권한이 없는 경우 오류를 반환하여 인가를 구현할 수 있습니다.
// app/api/admin/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
// 토큰이나 세션에서 사용자 역할을 가져오는 함수가 있다고 가정
const userRole = await getUserRole(request);
if (userRole !== 'admin') {
return NextResponse.json({ message: 'Unauthorized' }, { status: 403 });
}
// 관리자 데이터를 가져오는 로직
const adminData = { message: 'Admin data' };
return NextResponse.json(adminData);
}
async function getUserRole(request: Request): Promise {
// 실제 로직으로 교체하여 요청에서 사용자 역할을 추출
// JWT 토큰을 확인하거나 세션을 확인하는 작업이 포함될 수 있음
return 'admin'; // 예제: 시연을 위한 하드코딩된 역할
}
라우트 핸들러 배포하기
라우트 핸들러는 선택한 호스팅 제공업체에서 서버리스 함수로 배포됩니다. Next.js는 Vercel, Netlify, AWS 등 다양한 배포 플랫폼을 지원합니다.
Vercel의 경우, Git 저장소를 Vercel에 연결하고 코드를 푸시하는 것만으로 간단하게 배포할 수 있습니다. Vercel은 자동으로 Next.js 프로젝트를 감지하고 라우트 핸들러를 서버리스 함수로 배포합니다.
고급 기술
엣지 함수 (Edge Functions)
라우트 핸들러는 CDN의 엣지, 즉 사용자에게 더 가까운 곳에서 실행되는 엣지 함수로 배포될 수 있습니다. 이는 지연 시간을 크게 줄이고 성능을 향상시킬 수 있습니다.
라우트 핸들러를 엣지 함수로 배포하려면 route.ts
파일에 edge
런타임을 추가하세요:
export const runtime = 'edge';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
return NextResponse.json({ message: 'Hello from the Edge!' });
}
서버 액션 (Server Actions)
서버 액션을 사용하면 React 컴포넌트에서 직접 서버 사이드 코드를 실행할 수 있습니다. 라우트 핸들러와 서버 액션은 원활하게 함께 작동하여 복잡한 애플리케이션을 쉽게 구축할 수 있습니다.
다음은 서버 액션을 사용하여 라우트 핸들러를 호출하는 예입니다:
// app/components/MyComponent.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
async function handleSubmit(data: FormData) {
'use server';
const name = data.get('name');
const email = data.get('email');
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name, email }),
});
if (response.ok) {
router.refresh(); // 변경 사항을 반영하기 위해 페이지 새로고침
}
}
export default function MyComponent() {
const router = useRouter();
return (
);
}
캐싱
캐싱은 API 엔드포인트의 성능을 크게 향상시킬 수 있습니다. Cache-Control
헤더를 사용하여 브라우저와 CDN이 응답을 캐시하는 방식을 제어할 수 있습니다.
return NextResponse.json({ message: 'Success!' }, { status: 200, headers: { 'Cache-Control': 'public, max-age=3600' } });
이 예제는 Cache-Control
헤더를 public, max-age=3600
으로 설정하여 브라우저와 CDN이 응답을 한 시간 동안 캐시하도록 지시합니다.
모범 사례
- TypeScript 사용: TypeScript의 타입 안정성을 활용하여 코드 품질을 개선하고 오류를 방지하세요.
- 요청 유효성 검사: 들어오는 요청을 검증하여 데이터 무결성을 보장하고 악의적인 입력을 방지하세요.
- 우아한 오류 처리: 적절한 오류 처리를 구현하여 클라이언트에게 유용한 오류 메시지를 제공하세요.
- 엔드포인트 보안: 인증 및 인가를 구현하여 API 엔드포인트를 보호하세요.
- 미들웨어 사용: 인증, 로깅, 요청 유효성 검사와 같은 공통 관심사에는 미들웨어를 사용하세요.
- 응답 캐싱: 캐싱을 사용하여 API 엔드포인트의 성능을 향상시키세요.
- API 모니터링: API를 모니터링하여 문제를 신속하게 식별하고 해결하세요.
- API 문서화: 다른 개발자들이 쉽게 사용할 수 있도록 API를 문서화하세요. API 문서화를 위해 Swagger/OpenAPI와 같은 도구를 사용하는 것을 고려해 보세요.
실제 사용 예제
다음은 라우트 핸들러가 사용될 수 있는 몇 가지 실제 예제입니다:
- 전자상거래 API: 제품, 주문, 사용자를 관리하기 위한 API 엔드포인트를 생성합니다.
- 소셜 미디어 API: 트윗 게시, 사용자 팔로우, 타임라인 검색을 위한 API 엔드포인트를 생성합니다.
- 콘텐츠 관리 시스템(CMS) API: 콘텐츠, 사용자, 설정을 관리하기 위한 API 엔드포인트를 생성합니다.
- 데이터 분석 API: 데이터를 수집하고 분석하기 위한 API 엔드포인트를 생성합니다. 예를 들어, 라우트 핸들러는 여러 웹사이트의 추적 픽셀로부터 데이터를 수신하고 보고를 위해 정보를 집계할 수 있습니다.
국제 전자상거래 예제: 사용자의 국가에 따라 제품 가격을 가져오는 데 사용되는 라우트 핸들러입니다. 엔드포인트는 요청의 지리적 위치(IP 주소에서 파생)를 사용하여 사용자의 위치를 파악하고 적절한 통화로 가격을 반환할 수 있습니다. 이는 현지화된 쇼핑 경험에 기여합니다.
글로벌 인증 예제: 전 세계 사용자를 위한 다단계 인증(MFA)을 구현하는 라우트 핸들러입니다. 이는 각 지역의 개인정보 보호 규정 및 통신 인프라를 존중하면서 SMS 코드 전송이나 인증 앱 사용을 포함할 수 있습니다.
다국어 콘텐츠 전달: 사용자가 선호하는 언어로 콘텐츠를 전달하는 라우트 핸들러입니다. 이는 요청의 `Accept-Language` 헤더에서 결정될 수 있습니다. 이 예제는 적절한 경우 UTF-8 인코딩 및 오른쪽에서 왼쪽으로 쓰는 언어 지원의 필요성을 강조합니다.
결론
Next.js 라우트 핸들러는 Next.js 애플리케이션 내에서 직접 API 엔드포인트를 생성하는 강력하고 유연한 방법을 제공합니다. 라우트 핸들러를 활용함으로써 견고한 API를 쉽게 구축하고, 백엔드 로직을 React 컴포넌트와 함께 배치하며, 미들웨어, 스트리밍, 엣지 함수와 같은 기능을 활용할 수 있습니다.
이 종합 가이드는 기본 설정부터 고급 기술까지 모든 것을 다루었습니다. 이 가이드에 요약된 모범 사례를 따르면 안전하고 성능이 뛰어나며 유지보수가 용이한 고품질 API를 구축할 수 있습니다.