Next.js Route Handlers vs Server Actions: 폼·웹훅·업로드별 선택 기준
호출 주체, HTTP 메서드, 순차 실행, 본문 크기 제한으로 나누는 판단표
핵심 요약
- Server Actions는 같은 앱 UI의 데이터 변경에, Route Handlers는 외부 호출·GET·스트리밍·웹훅에 맞는다.
- Server Action은 클라이언트당 순차 실행되므로 Promise.all로 병렬화하지 않는다.
- Server Action 본문은 기본 1MB로 제한되며 웹훅 서명 검증은 Route Handler의 request.text()가 적합하다.
- 두 방식 모두 공개 엔드포인트로 보고 인증·권한·입력 검증을 매번 수행한다.
Next.js App Router에서 서버 쪽 로직을 실행하는 길은 크게 두 가지다. React 액션 방식으로 호출되는 Server Actions와, 웹 표준 Request/Response로 HTTP 요청을 직접 처리하는 Route Handlers다. 둘 다 서버에서 실행되지만 호출 방식과 제약이 달라 잘못 고르면 동시 요청이 직렬로 밀리거나 웹훅 서명 검증이 실패한다. 이 글은 Server Actions 가이드와 route.js 문서(16.3 기준)를 바탕으로 선택 기준을 정리한다. 서버 액션 안의 오류 처리는 서버 액션 에러 핸들링 글에서 다뤘다.
두 방식의 차이 한눈에 보기
| 항목 | Server Actions | Route Handlers |
|---|---|---|
| 정의 | 'use server' 함수, 폼 action·formAction·트랜지션으로 호출 | app/**/route.ts에서 GET·POST 등 메서드 export |
| HTTP | 항상 호출한 페이지로의 POST | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS |
| 호출 주체 | 같은 앱의 React UI | 브라우저, 외부 서비스, 모바일 앱, 다른 서버 |
| 동시성 | 클라이언트당 한 번에 하나씩 순차 실행 | 일반 HTTP 요청처럼 병렬 처리 |
| 본문 크기 | 기본 1MB 제한(serverActions.bodySizeLimit로 조정) | 런타임·플랫폼 제한을 따름 |
| 응답 | 반환값과 다시 렌더링된 화면(RSC Payload)을 한 응답으로 | 직접 만든 Response(JSON, 스트림, XML 등) |
| CSRF 검사 | 프레임워크가 Origin과 Host를 비교 | 직접 구현 |
Server Actions가 맞는 경우
문서는 Server Action이 데이터 변경(뮤테이션)과 잘 맞는다고 설명한다. 액션이 updateTag, revalidatePath, refresh를 호출하거나 쿠키를 바꾸면, Next.js는 한 번의 요청 안에서 변경·캐시 무효화·현재 페이지 재렌더링을 모두 처리해 응답에 담는다. 화면을 갱신하기 위한 추가 요청이 필요 없다.
- 같은 앱의 폼 제출(글 작성, 설정 변경, 댓글 등록)
- 변경 직후 현재 화면이 바로 바뀌어야 하는 작업
useActionState로 검증 오류를 폼에 표시해야 하는 작업
Route Handlers가 맞는 경우
- 외부 서비스의 웹훅: 결제·Git 서비스 같은 외부 시스템이 호출한다. 문서의 웹훅 예시처럼
request.text()로 원본 본문을 읽을 수 있어 서명 검증에 적합하다. - GET으로 읽는 공개 API: 모바일 앱이나 다른 서버가 호출하는 조회 API, RSS·사이트맵 같은 비UI 응답.
- 병렬로 많이 호출되는 조회: 문서는 Server Action이 클라이언트당 하나씩 순차 실행되므로
Promise.all로 병렬화하려 하지 말고, 병렬 조회가 필요하면 Server Component나 Route Handler를 쓰라고 안내한다. - 스트리밍 응답: LLM 응답처럼
ReadableStream을 직접 반환해야 하는 경우. - 1MB를 넘는 업로드: 설정으로 액션 제한을 늘릴 수 있지만, 큰 파일은 Route Handler나 스토리지 서비스의 직접 업로드 URL로 처리하는 구조가 다루기 쉽다.
웹훅을 Route Handler로 받는 기본 형태
// app/api/webhooks/billing/route.ts
export async function POST(request: Request) {
const rawBody = await request.text() // 서명 검증용 원본 본문
const signature = request.headers.get('x-signature')
if (!verifySignature(rawBody, signature)) {
return new Response('invalid signature', { status: 400 })
}
const event = JSON.parse(rawBody)
await enqueue(event) // 무거운 처리는 큐로 넘김
return new Response('ok', { status: 200 })
}Pages Router의 API Routes와 달리 bodyParser 설정이 필요 없다는 점도 문서에 나와 있다. 결제 웹훅의 서명 검증과 중복 처리는 비동기 API 설계 글의 큐 구조와 함께 보면 이해가 쉽다.
두 방식 모두에 적용되는 보안 원칙
Server Action은 화면에 보이는 함수처럼 생겼지만, 문서는 같은 POST를 보낼 수 있는 누구나 접근 가능한 엔드포인트이므로 모든 액션을 신뢰할 수 없는 진입점으로 다루라고 한다. 로그인한 사람에게만 폼을 보여 주는 것은 보안 경계가 아니다.
- 액션·핸들러 시작 부분에서 인증과 권한을 확인한다.
FormData, 쿼리 매개변수, 헤더를 모두 신뢰할 수 없는 입력으로 보고 검증한다.- 클라이언트에서는 대상 ID와 변경 내용만 받고, 소유권은 세션 기준으로 DB에서 다시 확인한다.
- 반환값은 화면에 필요한 필드로만 구성한다. 액션 반환값은 클라이언트로 직렬화된다.
- 삭제처럼 파괴적인 작업은 재인증 같은 강한 확인을 검토한다.
배포 시 Server Actions에서만 생기는 문제
Server Action은 빌드 산출물에 포함된 액션 ID로 식별된다. 새 배포는 보통 새 ID를 만들기 때문에, 이전 빌드를 띄워 둔 사용자가 액션을 호출하면 "Failed to find Server Action" 오류가 날 수 있다. 문서는 점진 배포를 선호하고, 여러 인스턴스에서 NEXT_SERVER_ACTIONS_ENCRYPTION_KEY를 같은 값으로 유지하며, 이 오류를 UI에서 새로고침 안내로 처리하라고 권한다. 외부 클라이언트가 장기간 호출해야 하는 API라면 이 점에서도 Route Handler가 안정적이다.
선택 흐름 정리표
| 질문 | 예 | 아니오 |
|---|---|---|
| 같은 앱의 React UI만 호출하는가? | 다음 질문으로 | Route Handler |
| 데이터를 변경하는가? | 다음 질문으로 | Server Component 조회 또는 Route Handler(GET) |
| 병렬 호출·스트리밍·1MB 초과 본문이 필요한가? | Route Handler | Server Action |
초보자가 자주 실수하는 포인트
- 외부 서비스 웹훅을 Server Action으로 받으려 하는 경우
- 여러 Server Action을 Promise.all로 동시에 호출해 빨라질 것으로 기대하는 경우
- 폼이 로그인 화면에만 있다는 이유로 액션 안의 권한 확인을 생략하는 경우
- 웹훅 본문을 JSON으로 먼저 파싱해 서명 검증이 실패하는 경우
체크리스트
- 외부 시스템이 호출하는 엔드포인트가 Route Handler로 구현됐는가
- 모든 액션·핸들러에서 인증과 권한을 확인하는가
- 큰 파일 업로드가 액션 본문 제한에 걸리지 않는 구조인가
- 액션 반환값에 화면에 필요한 필드만 있는가
- 배포 후 액션 ID 불일치 오류에 대한 UI 처리가 있는가
자주 묻는 질문
Server Action에서 GET 요청을 받을 수 있나요?
아니다. Server Action은 호출한 페이지로의 POST로 실행된다. 조회용 공개 API는 Route Handler의 GET으로 만든다.
Route Handler의 GET 응답은 기본으로 캐시되나요?
v15부터 GET 핸들러의 기본 캐시가 정적에서 동적으로 바뀌었다. Cache Components를 켜면 페이지와 같은 프리렌더링 모델을 따른다.
Server Action의 1MB 제한은 늘릴 수 있나요?
next.config의 serverActions.bodySizeLimit으로 조정할 수 있다. 큰 파일은 스토리지 직접 업로드 방식이 더 다루기 쉽다.
참고 자료 · 검증 기준
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.