Next.js 15 (App Router 기반)에서는 서버 기능을 처리하는 두 가지 방식이 존재한다.
이 둘은 모두 서버에서 실행되지만 목적과 사용 방식이 다르다.
pages/api 대신 app/api/**/route.ts에서 작성GET, POST 등의 요청 처리 가능/app
/api
/hello
route.ts
// app/api/hello/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ message: 'Hello from Route Handler!' });
}
GET, POST, PUT, DELETE, PATCH 등 HTTP 메서드 대응fetch('/api/hello') 방식으로 클라이언트에서 호출NextRequest, NextResponse로 요청/응답 처리
use client지시어가 없는 Server Component도 기본적으로 서버에서 실행된다.
하지만 Route Handler는 React와 완전히 격리된 순수 서버 함수로, 다음과 같은 차별점이 있다:
| 항목 | Server Component | Route Handler |
|---|---|---|
| 위치 | React 렌더링 트리 내부 | React 바깥 (app/api) |
| 클라이언트 번들 포함 여부 | ❌ (Hydration 없음) | ✅ 아예 번들 대상 아님 |
| 민감 정보 안전성 | 비교적 안전 | 구조적으로 완전 차단 |
| 인증 처리 | 쿠키 접근 시 제약 있음 | NextRequest에서 완전한 접근 가능 |
| API 설계 | 어려움 (fetch나 mutate로 연결 필요) | 직관적 REST API 가능 |
즉, Route Handler는 API 서버처럼 동작하는 "완전한 서버 전용 코드"로 보안, 역할 분리, 유지보수 면에서 더 명확한 이점이 있다.
fetch() 호출할 때use server로 선언된 서버에서 실행되는 함수/app/login/page.tsx
/app/login/actions.ts
서버 함수:
// app/login/actions.ts
'use server';
export async function handleLogin(formData: FormData) {
const email = formData.get('email');
const password = formData.get('password');
return { errors: ['Invalid email or password'] };
}
클라이언트에서 호출:
// app/login/page.tsx
'use client';
import { handleLogin } from './actions';
export default function Login() {
return (
<form action={handleLogin}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">로그인</button>
</form>
);
}
form action={서버함수} 패턴으로 간단하게 제출 처리useFormState와 함께 상태 기반 UI도 구현 가능// next.config.ts
experimental: {
serverActions: {
bodySizeLimit: '20mb',
},
}
서버 액션은 POST 요청처럼 동작하고, 전송되는 데이터(body) 크기가 기본 1MB 제한됨
→ 이미지/FormData 업로드 시 반드시 이 설정 필요
| 항목 | Route Handler | Server Action |
|---|---|---|
| 위치 | app/api/**/route.ts | 컴포넌트 내부 or lib/actions/** |
| 호출 방식 | fetch() | form action={함수} 또는 직접 호출 |
| 주 사용 목적 | API 라우트, 외부 요청 | 내부 서버 처리, 폼 제출 |
| 브라우저 번들 | ❌ 클라이언트 포함 안 됨 | ❌ 클라이언트 포함 안 됨 |
| 외부 백엔드 연동 | 적합 | 가능하나 덜 직관적 |
| 보안 민감 작업 | 안전 | 더 직관적, 쿠키 접근 등 쉬움 |
대부분 안 쓴다 또는 최소한만 쓴다
// 굳이 서버 액션으로 외부 백엔드 호출할 필요는 없다
'use server';
export async function loginAction(formData: FormData) {
const name = formData.get('name');
const password = formData.get('password');
const res = await fetch('http://localhost:8080/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, password }),
});
return res.json();
}
🧾 이런 경우엔 그냥 useMutation(fetch)로 처리하자.
| 상황 | 추천 방식 |
|---|---|
| 외부 API 요청, DB 저장 등 REST API 필요 | Route Handler |
| 클라이언트에서 fetch로 직접 호출 | Route Handler |
| 간단한 폼 제출 처리 | Server Action |
| 쿠키, 세션 조작 등 보안 민감 작업 | Server Action |
| 백엔드 없이 Next.js만 사용하는 경우 | Server Action |
Route Handler: 전통적인 API 서버 느낌
→ fetch()로 요청하고, Next.js가 API 서버 역할을 함
→ React와 완전히 분리된 독립 서버 코드
Server Action: 서버 코드가 프론트 코드 안에 있는 느낌
→ 코드가 간결해지고, 폼과 직결되어 사용하기 편리함
→ 간단한 내부 처리나 Form과의 통합에 강점