API 라우트 생성하기
웹 애플리케이션은 사용자 인터페이스(클라이언트)와 데이터를 주고받기 위해 서버 측 API가 필요합니다. Next.js는 React 컴포넌트를 기반으로 프론트엔드를 구축할 뿐만 아니라, 서버리스 함수(Serverless Functions)로 작동하는 강력한 API 라우트(API Routes) 기능을 내장하고 있습니다. 이를 통해 백엔드 서버를 별도로 구축할 필요 없이 Next.js 프로젝트 내에서 직접 API 엔드포인트를 생성하고 관리할 수 있습니다.
이 절에서는 Next.js App Router 환경에서 API 라우트가 무엇인지, 왜 필요한지, 그리고 GET, POST 요청을 처리하는 API 라우트를 어떻게 생성하는지 상세히 알아보겠습니다.
API 라우트란 무엇인가요?
Next.js의 API 라우트는 pages/api
디렉토리(Page Router) 또는 app/api
디렉토리(App Router) 내에 생성되는 파일 기반 API 엔드포인트입니다. 각 파일은 서버리스 함수로 취급되며, 해당 파일 경로가 API 엔드포인트의 URL이 됩니다.
주요 특징
- 파일 시스템 기반 라우팅: 파일 이름과 디렉토리 구조가 API 엔드포인트의 URL 경로에 직접 매핑됩니다. (예:
app/api/users/route.ts
는/api/users
경로로 접근 가능) - 서버리스 함수: 각 API 라우트는 독립적인 서버리스 함수로 배포됩니다. 이는 필요한 경우에만 실행되어 리소스를 효율적으로 사용하고 확장성이 뛰어납니다.
- Node.js 환경: API 라우트 코드는 Node.js 런타임에서 실행됩니다. 따라서 파일 시스템 접근, 데이터베이스 연결, 외부 API 호출 등 서버 측 작업을 수행할 수 있습니다.
- HTTP 메서드 지원:
GET
,POST
,PUT
,DELETE
등 다양한 HTTP 메서드에 대한 핸들러를 정의할 수 있습니다. - Request 및 Response 객체: Express.js와 유사하게 요청(
NextRequest
) 객체와 응답(NextResponse
) 객체를 통해 HTTP 요청 및 응답을 처리합니다.
왜 API 라우트가 필요한가요? (이점)
Next.js 애플리케이션에서 API 라우트를 사용하는 것은 여러 이점을 제공합니다.
- 풀스택 개발 용이성: 프론트엔드와 백엔드 로직을 하나의 Next.js 프로젝트 내에서 관리할 수 있어 개발 과정을 간소화하고 생산성을 높입니다.
- 빠른 프로토타이핑: 별도의 백엔드 서버 설정 없이 빠르게 API를 만들고 테스트할 수 있습니다.
- SSR/SSG과의 시너지: 서버 컴포넌트에서 API 라우트를 직접 호출하여 데이터를 가져올 수 있으며, 이는 서버 사이드 렌더링(SSR) 및 정적 사이트 생성(SSG) 시 데이터를 미리 가져오는 데 유용합니다.
- 인증 및 보안: NextAuth.js와 같은 인증 라이브러리와 연동하여 보호된 API 엔드포인트를 쉽게 구축할 수 있습니다.
- 쉬운 배포: Next.js 프로젝트와 함께 API 라우트도 자동으로 서버리스 플랫폼(Vercel, AWS Lambda 등)에 배포됩니다.
API 라우트 생성하기 (App Router)
Next.js App Router에서 API 라우트는 app
디렉토리 내에 api
폴더를 생성하고, 그 안에 라우트 핸들러 파일을 정의하는 방식으로 생성됩니다.
기본 구조
app/
├── api/
│ ├── users/
│ │ └── route.ts # GET, POST 등 메서드 핸들러 정의
│ └── posts/
│ └── [id]/
│ └── route.ts # 동적 라우트 예시 (e.g., /api/posts/123)
├── dashboard/
│ └── page.tsx
└── page.tsx
각 route.ts
(또는 .js
, .jsx
, .tsx
) 파일은 해당 경로의 API 요청을 처리하는 역할을 합니다. 파일 내에서는 GET
, POST
, PUT
, DELETE
, PATCH
, HEAD
, OPTIONS
와 같은 HTTP 메서드 이름의 함수를 export
하여 해당 메서드 요청을 처리합니다.
GET 요청 처리 API 라우트 생성
GET 요청은 주로 데이터를 조회할 때 사용됩니다.
실습: 사용자 목록을 반환하는 GET API 라우트 생성
-
src/app/api/users/route.ts
파일 생성src/app/api/users/route.ts // src/app/api/users/route.ts import { NextRequest, NextResponse } from 'next/server'; // 가상의 사용자 데이터 (실제 프로젝트에서는 데이터베이스에서 가져옵니다) const users = [ { id: 1, name: '김철수', email: 'chulsoo@example.com' }, { id: 2, name: '이영희', email: 'younghee@example.com' }, { id: 3, name: '박민수', email: 'minsu@example.com' }, ]; /** * GET 요청을 처리하는 핸들러 함수 * @param request NextRequest 객체 (선택 사항, 쿼리 파라미터 등 접근 시 사용) */ export async function GET(request: NextRequest) { // 쿼리 파라미터 예시: /api/users?search=김 const searchParam = request.nextUrl.searchParams.get('search'); let filteredUsers = users; if (searchParam) { filteredUsers = users.filter(user => user.name.includes(searchParam)); } // JSON 형식으로 사용자 목록 반환 return NextResponse.json(filteredUsers, { status: 200 }); }
-
API 테스트: 개발 서버(
npm run dev
)를 실행한 후, 웹 브라우저나 Postman/Insomnia 같은 API 클라이언트에서http://localhost:3000/api/users
경로로 접속합니다.http://localhost:3000/api/users
로 접속하면 전체 사용자 목록이 JSON 형태로 반환됩니다.http://localhost:3000/api/users?search=김
으로 접속하면 '김'이 포함된 사용자만 필터링되어 반환됩니다.
POST 요청 처리 API 라우트 생성
POST 요청은 주로 새로운 데이터를 생성하거나 서버로 데이터를 전송할 때 사용됩니다.
실습: 새로운 사용자 정보를 추가하는 POST API 라우트 생성
-
src/app/api/users/route.ts
파일에 POST 핸들러 추가: (GET
핸들러와 같은 파일에 추가합니다. 하나의route.ts
파일은 여러 HTTP 메서드 핸들러를 포함할 수 있습니다.)src/app/api/users/route.ts // src/app/api/users/route.ts (GET 핸들러 아래에 추가) import { NextRequest, NextResponse } from 'next/server'; // (GET 핸들러 위에 있던 users 배열은 예시를 위해 전역으로 유지) // 실제로는 데이터베이스에 저장될 것이므로, 여기서는 단순히 배열에 추가하는 것으로 대체 let users = [ { id: 1, name: '김철수', email: 'chulsoo@example.com' }, { id: 2, name: '이영희', 'email': 'younghee@example.com' }, { id: 3, name: '박민수', email: 'minsu@example.com' }, ]; let nextId = 4; // 다음 사용자 ID /** * POST 요청을 처리하는 핸들러 함수 * @param request NextRequest 객체 (요청 본문(body) 접근 시 사용) */ export async function POST(request: NextRequest) { try { // 요청 본문(body)에서 JSON 데이터 파싱 const body = await request.json(); const { name, email } = body; // 필수 필드 검증 if (!name || !email) { return NextResponse.json( { message: '이름과 이메일은 필수입니다.' }, { status: 400 } // Bad Request ); } // 새로운 사용자 객체 생성 const newUser = { id: nextId++, name, email }; // 가상 데이터 배열에 추가 (실제로는 데이터베이스에 저장) users.push(newUser); // 생성된 사용자 정보와 함께 성공 응답 반환 return NextResponse.json(newUser, { status: 201 }); // Created } catch (error) { console.error('Error creating user:', error); return NextResponse.json( { message: '사용자 생성에 실패했습니다.', error: (error as Error).message }, { status: 500 } // Internal Server Error ); } } // (위 GET 핸들러는 그대로 유지) export async function GET(request: NextRequest) { // ... (기존 GET 핸들러 코드) ... const searchParam = request.nextUrl.searchParams.get('search'); let filteredUsers = users; if (searchParam) { filteredUsers = users.filter(user => user.name.includes(searchParam)); } return NextResponse.json(filteredUsers, { status: 200 }); }
-
API 테스트: Postman, Insomnia 또는
curl
명령어를 사용하여 POST 요청을 보냅니다.curl
예시curl -X POST \ http://localhost:3000/api/users \ -H 'Content-Type: application/json' \ -d '{ "name": "새로운 사용자", "email": "newuser@example.com" }'
요청이 성공하면, 새로 생성된 사용자 정보와 함께 HTTP 상태 코드 201(Created)이 반환됩니다. 이후
http://localhost:3000/api/users
로 GET 요청을 다시 보내면, 새로 추가된 사용자가 목록에 포함된 것을 확인할 수 있습니다.
동적 API 라우트 생성
특정 리소스에 대한 작업을 수행해야 할 때 (예: 특정 ID의 사용자 조회, 업데이트, 삭제), 동적 API 라우트를 사용할 수 있습니다.
파일 또는 폴더 이름을 대괄호([]
)로 감싸서 동적 파라미터를 정의합니다. ([slug]
또는 [id]
)
실습: 특정 사용자 조회, 업데이트, 삭제 API 라우트 생성
-
src/app/api/users/[id]/route.ts
파일 생성src/app/api/users/[id]/route.ts // src/app/api/users/[id]/route.ts import { NextRequest, NextResponse } from 'next/server'; // 가상의 사용자 데이터 (POST 예시의 users 배열과 동일하게 유지) // 실제로는 데이터베이스에서 가져와야 함. let users = [ { id: 1, name: '김철수', email: 'chulsoo@example.com' }, { id: 2, name: '이영희', email: 'younghee@example.com' }, { id: 3, name: '박민수', email: 'minsu@example.com' }, ]; // 라우트 파라미터 타입 정의 interface Context { params: { id: string }; } /** * 특정 사용자 조회 (GET /api/users/[id]) * @param request NextRequest 객체 * @param context 동적 라우트 파라미터 (params.id) */ export async function GET(request: NextRequest, context: Context) { const id = parseInt(context.params.id); // ID를 정수로 변환 const user = users.find(u => u.id === id); if (!user) { return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 }); } return NextResponse.json(user, { status: 200 }); } /** * 특정 사용자 업데이트 (PUT /api/users/[id]) * @param request NextRequest 객체 * @param context 동적 라우트 파라미터 (params.id) */ export async function PUT(request: NextRequest, context: Context) { const id = parseInt(context.params.id); const body = await request.json(); const { name, email } = body; const userIndex = users.findIndex(u => u.id === id); if (userIndex === -1) { return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 }); } // 사용자 정보 업데이트 (불변성을 위해 새로운 배열 생성) users = users.map(user => user.id === id ? { ...user, name: name || user.name, email: email || user.email } : user ); return NextResponse.json(users[userIndex], { status: 200 }); } /** * 특정 사용자 삭제 (DELETE /api/users/[id]) * @param request NextRequest 객체 * @param context 동적 라우트 파라미터 (params.id) */ export async function DELETE(request: NextRequest, context: Context) { const id = parseInt(context.params.id); const initialLength = users.length; users = users.filter(u => u.id !== id); // 사용자 삭제 if (users.length === initialLength) { return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 }); } return NextResponse.json({ message: '사용자가 성공적으로 삭제되었습니다.' }, { status: 200 }); }
-
API 테스트
- GET:
http://localhost:3000/api/users/1
로 접속하여 ID가 1인 사용자 정보를 조회합니다. 존재하지 않는 ID(예:/api/users/99
)로 접속하면 404 응답을 받습니다. - PUT: Postman 등으로
http://localhost:3000/api/users/1
에 PUT 요청을 보내고 본문(Body)에{ "name": "김철수(수정됨)" }
와 같은 JSON 데이터를 포함하여 사용자 정보를 업데이트합니다. - DELETE: Postman 등으로
http://localhost:3000/api/users/2
에 DELETE 요청을 보내 ID가 2인 사용자를 삭제합니다.
- GET:
API 라우트 사용 시 고려사항
- 데이터베이스 연동: 위 예시는 간단한 배열을 사용하여 데이터를 관리했지만, 실제 애플리케이션에서는 MongoDB, PostgreSQL, MySQL 등 데이터베이스와 연동하여 데이터를 영구적으로 저장하고 관리해야 합니다. Prisma, Drizzle ORM 등을 사용하여 데이터베이스 작업을 추상화할 수 있습니다.
- 오류 처리: API 라우트에서는 적절한 HTTP 상태 코드(예: 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Internal Server Error)와 함께 의미 있는 오류 메시지를 반환하는 것이 중요합니다.
try...catch
블록을 사용하여 예상치 못한 오류를 처리해야 합니다. - 데이터 유효성 검사: 클라이언트로부터 받은 데이터는 항상 유효성 검사를 수행해야 합니다. Zod, Joi 같은 라이브러리를 사용하여 스키마 기반 유효성 검사를 적용할 수 있습니다.
- 인증 및 권한 부여: 보호된 리소스에 접근하는 API 라우트의 경우, NextAuth.js의
getServerSession
을 사용하여 사용자의 로그인 상태 및 역할을 확인해야 합니다. - 파일 크기: 서버리스 함수는 일반적으로 실행 시간과 번들 크기에 제한이 있습니다. 불필요한 의존성을 줄여 최적화하는 것이 좋습니다.
- Streamable Response: Next.js 13 이상에서는
NextResponse.json()
외에도Response
객체를 직접 반환하여 스트리밍 응답을 구현할 수도 있습니다. - CORS (Cross-Origin Resource Sharing): 다른 도메인의 클라이언트에서 API 라우트에 접근해야 하는 경우, CORS 헤더를 적절히 설정해야 합니다.
Next.js의 API 라우트는 프론트엔드 개발자가 풀스택 애플리케이션을 빠르고 효율적으로 구축할 수 있도록 돕는 강력한 기능입니다. 이 기능을 통해 백엔드 로직을 직접 구현하고 프론트엔드와의 원활한 데이터 통신을 구축할 수 있습니다.