서버리스 함수 활용
Route Handler·서버 액션·Proxy와 Edge 런타임을 실행 위치와 제한에 맞게 선택합니다.
Next.js는 React 프레임워크를 넘어 SSR, SSG, API 처리까지 아우르는 풀스택 프레임워크로 발전했습니다. 특히 서버리스 함수(serverless functions)를 쓰면 서버를 직접 운영하지 않고도 API 엔드포인트와 백엔드 로직을 구현할 수 있습니다. 이런 함수는 Vercel 같은 플랫폼에서 자동으로 배포되고 관리되므로, 인프라의 부담을 줄이면서 기능을 빠르게 넓힐 수 있습니다. 이 글에서는 Route Handler, 서버 액션, Proxy, 그리고 선택적으로 쓰는 Edge 런타임이 각각 어디에서 실행되는지 구분합니다.
서버리스 함수란 무엇이며 왜 중요한가요?
서버리스 함수는 AWS Lambda, Google Cloud Functions, Azure Functions 같은 클라우드 공급자가 서버 인프라의 관리를 모두 맡고, 개발자는 코드만 작성해 배포하는 컴퓨팅 모델입니다. 코드는 HTTP 요청이나 데이터베이스 변경 같은 이벤트가 일어날 때만 실행되며, 쓴 만큼 비용이 청구됩니다.
서버리스 함수를 쓰면 서버를 준비하고 패치하고 늘리는 일을 직접 하지 않아도 되며, 클라우드 공급자가 인프라를 관리합니다. 플랫폼이 트래픽에 맞춰 인스턴스를 조정하지만, 동시 실행 한도와 연결하는 데이터베이스의 용량도 함께 고려해야 합니다. 요금은 호출 수, CPU, 메모리 같은 플랫폼의 과금 단위를 확인해야 하며, Vercel의 Fluid compute는 I/O를 기다리는 동안 CPU 과금은 멈추지만 처리 중인 요청에 할당된 메모리는 계속 계산합니다. 코드 변경을 빠르게 배포할 수 있고, 백엔드 인프라를 신경 쓰지 않고 업무 로직에 집중할 수 있어 생산성도 높아집니다. Next.js는 이런 이점을 애플리케이션에 쉽게 통합하도록 지원합니다.
Next.js에서 서버리스 함수 구현하기
App Router에서 서버 코드가 실행되는 주요 단위는 Route Handler, 서버 액션, Proxy입니다. 이들은 Next.js의 기능이며, 실제로 서버리스 함수로 배포할지 직접 운영하는 Node.js 서버에서 실행할지는 호스팅 방식에 따라 달라집니다.
Route Handler
Route Handler는 App Router에서 HTTP 엔드포인트를 만드는 기본 방식입니다. app의 라우트 세그먼트에 둔 route.ts가 HTTP 메서드 이름으로 처리 함수를 내보내며, 다음처럼 src/app/api 아래에 둘 수 있지만 api 디렉터리에만 둘 수 있는 것은 아닙니다.
// GET 요청을 처리하는 Route Handler
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const name = searchParams.get('name') || 'World';
// JSON 응답 반환
return NextResponse.json({ message: `Hello, ${name}!` });
}
// POST, PUT, DELETE 등 다른 HTTP 메서드도 동일한 방식으로 export 가능
export async function POST(request: Request) {
const data = await request.json();
return NextResponse.json({ received: data, status: 'success' }, { status: 200 });
}이 POST 예제는 JSON 입력을 그대로 돌려주는 최소한의 응답이며, 잘못된 JSON, 인증, 업무 입력 검증은 아직 처리하지 않습니다.
Route Handler로 GET, POST 같은 HTTP 요청에 응답하는 RESTful API 엔드포인트를 쉽게 만들 수 있고, 클라이언트에서는 fetch로 이 엔드포인트를 호출합니다. 요청할 때 처리하는 Route Handler는 Vercel Functions로 실행할 수 있고, 빌드할 때 확정할 수 있는 정적 GET 결과는 정적 산출물이 될 수도 있습니다. 데이터베이스 접근, 외부 API 호출, 인증 처리 같은 여러 백엔드 로직을 구현할 수 있습니다.
Server Actions (App Router Only)
서버 액션은 App Router에서 클라이언트의 폼 제출이나 이벤트를 서버 함수와 연결하는 기능입니다. 데이터 변경과 폼 처리를 구현할 때는 입력 검증, 권한 확인, 캐시 재검증을 함께 둡니다. 다음은 할 일을 추가하는 페이지입니다.
import { addTodo } from './actions'; // Server Action 임포트
export default async function AddTodoPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return (
<form action={addTodo} style={{ margin: '50px', padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
<input type="hidden" name="locale" value={locale} />
<label htmlFor="todo" style={{ marginRight: '8px' }}>새 할 일</label>
<input id="todo" type="text" name="todo" maxLength={100} required style={{ padding: '10px', marginRight: '10px' }} />
<button type="submit" style={{ padding: '10px 15px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px' }}>
할 일 추가
</button>
</form>
);
}페이지는 서버 컴포넌트이지만 <form action={addTodo}>처럼 서버 액션을 그대로 넘길 수 있고, 제출하면 폼의 입력값이 이 함수로 전달됩니다. URL에서 읽은 locale은 숨은 입력으로 함께 보내, 서버 액션이 어느 언어의 경로를 재검증할지 알게 합니다. 서버 액션은 별도의 actions.ts에 정의합니다. 파일 맨 위의 'use server' 때문에 이 파일에서 내보낸 함수는 모두 서버 액션이 되고, 폼의 action에 연결한 함수는 FormData를 첫 인자로 받습니다.
'use server'; // 이 파일의 모든 함수가 서버에서 실행됨을 명시
import { revalidatePath } from 'next/cache'; // 데이터 갱신을 위해 Next.js 캐시 유틸리티 임포트
export async function addTodo(formData: FormData): Promise<void> {
const value = formData.get('todo');
const locale = formData.get('locale');
if (
typeof value !== 'string' ||
typeof locale !== 'string' ||
!['ko', 'en', 'ja'].includes(locale)
) return;
const todo = value.trim();
if (!todo || todo.length > 100) return;
// 실제 데이터베이스에 할 일을 추가하는 로직 (예시)
console.log(`서버에서 할 일 추가됨: ${todo}`);
// await db.todos.create({ text: todo });
// 특정 경로의 캐시를 무효화하여 최신 데이터를 가져오도록 강제
revalidatePath(`/${locale}/add-todo`); // 현재 locale 페이지의 데이터와 UI 갱신
}예제의 데이터베이스 저장 코드는 주석이며, 실제로는 입력을 검사하고 로그를 남긴 뒤 경로를 재검증합니다. revalidatePath가 데이터 저장을 대신하지는 않습니다. 브라우저의 required와 maxLength는 편의를 위한 1차 검증일 뿐 우회할 수 있으므로, 서버 액션에서도 타입, 공백을 지운 뒤의 길이, 업무 규칙을 다시 확인해야 합니다.
서버 액션은 클라이언트와 서버 사이의 데이터 직렬화와 통신을 Next.js가 자동으로 처리해 개발을 단순하게 하며, 폼 제출이나 버튼 클릭처럼 UI 상호작용에 바로 반응하는 서버 로직을 쓰기에 알맞습니다. Next.js의 캐시 API와도 통합되어, 경로는 revalidatePath, 태그의 stale-while-revalidate 갱신은 revalidateTag(tag, 'max'), 서버 액션 안의 즉시 만료는 updateTag로 처리합니다. Vercel에 배포하면 서버리스 함수로 실행됩니다.
Proxy와 Edge Runtime
Next.js 16은 요청 앞단의 파일 규칙을 middleware.ts에서 proxy.ts로 바꿨습니다. Proxy는 Node.js 런타임에서 실행되므로 데이터베이스 드라이버와 Node.js API를 쓸 수 있습니다. 다만 모든 요청의 첫머리에서 실행될 수 있으므로 복잡한 조회나 오래 걸리는 작업을 넣지 않습니다. 다음 Proxy는 대시보드 경로에서 세션 쿠키가 없으면 로그인 페이지로 보냅니다.
import { NextResponse, type NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const sessionCookie = request.cookies.get('session');
const [, locale] = request.nextUrl.pathname.split('/');
if (!sessionCookie) {
return NextResponse.redirect(new URL(`/${locale}/login`, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/:locale(ko|en|ja)/dashboard/:path*'],
};Proxy의 진입 파일은 하나뿐이므로, 16장 1절의 locale Proxy와 함께 쓰려면 두 처리를 하나의 proxy 함수와 matcher로 합쳐야 합니다. 이 Proxy는 session 쿠키가 있는지만 확인하므로 빈 값이나 위조된 값도 걸러 내지 못합니다. 실제 인증과 권한 검사는 Route Handler와 서버 액션 같은 데이터 처리 지점에서도 해야 합니다.
Edge 런타임이 필요한 Route Handler는 파일 위쪽에 runtime = 'edge'를 명시할 수 있습니다.
import { NextResponse } from 'next/server';
export const runtime = 'edge';
export async function GET() {
return NextResponse.json({ message: 'Hello from the Edge!' });
}runtime은 라우트 세그먼트 설정이라서 이 파일의 핸들러에만 적용되고, 같은 프로젝트의 다른 Route Handler는 기본값인 Node.js 런타임으로 실행됩니다. 이 예제는 고정된 JSON만 돌려주고 Node.js 전용 API를 쓰지 않으므로 Edge에서도 그대로 동작합니다.
Edge Runtime 선택 기준 (Node.js 런타임과 비교)
Edge 런타임과 Node.js 런타임은 빠르냐 느리냐만으로 고르는 선택지가 아니며, 의존성, 데이터 접근, 실행 제한을 먼저 비교해야 합니다.
Next · Runtime dependency boundaries
Node.js·Edge 런타임 비교
사용할 API와 DB 드라이버·SDK 호환성을 먼저 확인합니다. 실행 지역·시간·메모리와 데이터 저장소 거리는 플랫폼별로 대조하고 성능은 실제 요청으로 측정합니다.
| 기준 | Node.js | Edge |
|---|---|---|
| 사용 가능한 API | Node.js API와 호환 패키지 | Web API 중심 · Node.js API 일부 미지원 |
| 데이터 접근 | 일반 Node.js DB 드라이버·SDK 검토 | Edge 호환 드라이버·HTTP API 필요 |
| 실행 위치와 한도 | 플랫폼의 지역·시간·메모리 설정 | 플랫폼의 지역·시간·메모리 설정 |
- 사용 가능한 API
- Node.js
- Node.js API와 호환 패키지
- Edge
- Web API 중심 · Node.js API 일부 미지원
- 데이터 접근
- Node.js
- 일반 Node.js DB 드라이버·SDK 검토
- Edge
- Edge 호환 드라이버·HTTP API 필요
- 실행 위치와 한도
- Node.js
- 플랫폼의 지역·시간·메모리 설정
- Edge
- 플랫폼의 지역·시간·메모리 설정
Next.js 16 Proxy는 Node.js를 사용하며 Proxy 파일에는 runtime 설정을 지정할 수 없습니다. Edge Route Handler 선택과 구분합니다. 사용자와 가까워도 저장소가 멀면 전체 요청이 빨라진다고 보장하지 않습니다.
위 표는 Node.js와 Edge 런타임을 세 기준으로 비교합니다. 쓸 수 있는 API에서 Node.js는 Node.js API와 호환 패키지를 쓰고, Edge는 Web API 중심이라 일부 Node.js API를 지원하지 않습니다. 데이터 접근에서는 Node.js가 일반 DB 드라이버와 SDK를 검토할 수 있지만, Edge에는 Edge와 호환되는 드라이버나 HTTP API가 필요합니다. 실행 위치와 한도는 플랫폼의 지역, 시간, 메모리 설정을 따로 확인합니다. 표의 설명처럼 Next.js 16의 Proxy는 Node.js를 쓰며 Proxy 파일에는 runtime 설정을 지정할 수 없으므로, Edge Route Handler를 고르는 일과 구분합니다. 또한 사용자와 가까워도 저장소가 멀면 전체 요청이 빨라진다고 보장하지 않습니다.
Node 전용 파일 시스템이나 DB 드라이버가 필요한 함수를 Edge로 옮길지 판단할 때는 먼저 그 의존성을 확인합니다. 응답이 짧거나 사용자와 가까운 곳에서 실행하고 싶다는 이유만으로 지원되지 않는 API를 쓸 수 있는 것은 아닙니다.
런타임은 다음 순서로 고르면 좋습니다. 먼저 로직이 fs, 네이티브 모듈, 특정 Node 전용 SDK에 의존하는지 확인하고, 의존한다면 Node 런타임을 고릅니다. 의존성이 없고 Route Handler를 사용자 가까이에서 실행해야 할 이유가 분명하다면 Edge 런타임을 검토하고, 트래픽이 몰리는 구간에서 p95, p99 지연 시간을 비교해 측정한 뒤 최종 결정합니다. 고른 런타임에서 환경 변수와 비밀 키를 읽는 방식, 오류 응답과 로그도 확인합니다.
서버리스 함수 활용 전략 및 고려사항
런타임은 알맞게 고릅니다. Route Handler, 서버 액션, Proxy는 기본 Node.js를 기준으로 의존성을 검토하며, 서버 액션은 그 액션을 쓰는 페이지나 레이아웃의 런타임을 따릅니다. Edge Route Handler는 여러 지역에 나누어 실행하는 이점이 측정되었고 Node.js 전용 의존성이 없는 응답에만 씁니다. API 키나 DB 연결 문자열 같은 민감한 정보는 환경 변수로 관리하고, Vercel 대시보드나 .env 파일로 안전하게 주입합니다.
서버리스 함수는 한동안 쓰이지 않으면 콜드(cold) 상태가 되어, 첫 요청에서 컨테이너가 시작되며 지연이 생길 수 있습니다. 중요한 API는 실제 배포 환경에서 시작 시간을 측정합니다. 함수 인스턴스가 다시 쓰이면 메모리의 값이 남을 수 있지만, 다음 요청이 같은 인스턴스에 간다고 보장할 수 없으므로 상태를 저장하지 않는(stateless) 방식으로 설계하고, 계속 유지되어야 하는 세션과 데이터는 외부 저장소에 둡니다.
Vercel 대시보드에서 서버리스 함수의 실행 로그와 성능 지표를 모니터링해 문제가 생겼을 때 디버깅에 활용하고, 함수 안에서 생기는 오류를 알맞게 처리하고 기록해 안정성을 확보합니다. Route Handler와 서버 액션의 입력에는 인증, 인가, 유효성 검사를 반드시 적용합니다.
더 깊이: 응답을 보낸 뒤의 작업을 맡기는 after
서버리스 함수는 응답을 닫으면 곧바로 계산을 멈추므로, 로그 전송이나 분석 기록처럼 사용자가 기다릴 필요가 없는 작업을 응답 뒤로 미루기가 까다롭습니다. Next.js는 이를 위해 next/server의 after를 제공하며, 15.1에서 안정 기능이 되었습니다. after에 넘긴 콜백은 응답(또는 사전 렌더링)이 끝난 뒤에 실행되고, 서버 컴포넌트와 generateMetadata, 서버 함수, Route Handler, Proxy에서 쓸 수 있습니다. 응답이 정상적으로 끝나지 않은 경우, 곧 오류가 던져졌거나 notFound, redirect가 불린 경우에도 콜백은 실행됩니다(Next.js 문서의 after).
after는 요청 시점 API가 아니라서 부른다고 라우트가 동적으로 바뀌지 않으며, 정적 페이지에서 쓰면 콜백은 빌드할 때나 페이지가 재검증될 때 실행됩니다. Route Handler와 서버 함수에서는 콜백 안에서 cookies()와 headers()를 부를 수 있지만, 서버 컴포넌트에서는 콜백 안에서 부르면 실행 중 오류가 나므로 렌더링 중에 미리 읽은 값을 넘겨야 합니다. 실행 시간은 플랫폼의 기본값이나 maxDuration 설정을 넘지 못하며, 서버리스 환경에서는 응답 뒤에도 호출의 수명을 늘리는 waitUntil(promise) 기능으로 구현됩니다. next start로 직접 운영한다면 SIGINT나 SIGTERM으로 서버를 멈출 때 진행 중인 요청과 남은 after 콜백을 끝낸 뒤 종료하므로, 플랫폼에 10초에서 30초 정도의 종료 대기 시간을 두라고 권장됩니다.
정리
- App Router의 서버 실행 단위는 Route Handler, 서버 액션, Proxy이며, 서버리스 함수로 배포될지 Node.js 서버에서 실행될지는 호스팅 방식에 따라 달라집니다.
- Route Handler는
route.ts에서 메서드 이름의 함수를 내보내 HTTP 엔드포인트를 만들고, 서버 액션은 폼이나 이벤트를 서버 함수에 연결합니다. - 서버 액션은 브라우저 검증과 별개로 서버에서 입력을 다시 검사하며,
revalidatePath는 저장이 아니라 저장한 뒤의 화면 갱신입니다. - Next.js 16의 Proxy는 Node.js 런타임에서 실행되고 진입 파일은 하나이며, 쿠키가 있는지 확인하는 것만으로 인증을 대신하지 않습니다.
- Edge 런타임은 Node.js 전용 의존성이 없고 측정한 이점이 있을 때만 Route Handler에 쓰며, 서버리스 함수는 상태를 저장하지 않도록 설계합니다.
연습 문제
1. 저장과 재검증을 따로 추적하기
addTodo에 세 요청을 보냅니다. todo가 공백 세 칸이고 locale이 ko인 요청, todo가 앞뒤 공백을 포함한 책 읽기 이고 locale이 en인 요청, 정상 문자열이지만 locale이 fr인 요청입니다. 어느 요청이 로그와 revalidatePath까지 이를까요? DB에는 몇 개의 할 일이 저장될까요?
해설 보기
공백뿐인 요청은 trim 뒤에 빈 값이 되어 돌아가고, fr 요청은 허용 locale 검사에서 돌아갑니다. 두 번째 요청만 책 읽기를 로그에 남기고 /en/add-todo를 재검증합니다. DB 저장 호출은 주석이므로 저장되는 건수는 0입니다. 재검증은 이미 저장된 데이터를 다시 보여 주는 단계일 뿐 저장 자체를 하지 않습니다.
Proxy에 session 쿠키가 있다는 사실도 그 쿠키의 서명, 만료, 업무 권한이 유효하다는 뜻은 아닙니다. 실제 저장을 연결할 때는 데이터를 처리하는 곳에서 로그인한 사용자와 권한을 확인하고, 저장이 성공한 뒤에 재검증하도록 순서를 정합니다.
2. Edge로 옮기려는 Route Handler
Prisma 같은 Node.js용 ORM으로 데이터베이스를 조회하는 Route Handler에 응답을 빠르게 하려고 export const runtime = 'edge'를 추가했습니다. 어떤 문제가 생길 수 있을까요?
해설 보기
Edge 런타임은 Web API 중심이라 일부 Node.js API를 지원하지 않으므로, Node.js 전용 드라이버나 기능에 기대는 ORM 설정은 빌드나 실행 중에 실패할 수 있습니다. Edge와 호환되는 드라이버나 HTTP 기반 데이터 API가 따로 필요합니다. 또 Edge 함수가 사용자 가까이에서 실행되더라도 데이터베이스가 먼 지역에 있으면 요청마다 먼 거리를 왕복하므로 전체 응답이 오히려 느려질 수 있습니다. 의존성과 데이터 위치를 먼저 확인하고, 실제 요청으로 지연 시간을 측정한 뒤 고릅니다.
3. 메모리에 저장한 방문 수
Route Handler에서 모듈 수준의 변수 let visits = 0을 두고 요청마다 1씩 늘려 돌려주도록 만들었습니다. Vercel에 배포하면 이 값을 믿을 수 있을까요?
해설 보기
믿을 수 없습니다. 서버리스 함수는 트래픽에 따라 인스턴스가 여러 개로 늘어나거나 사라지며, 요청이 어느 인스턴스로 갈지 보장되지 않습니다. 그래서 인스턴스마다 다른 visits 값이 생기고, 인스턴스가 다시 시작되면 0으로 돌아갑니다. 정확한 방문 수를 유지하려면 데이터베이스나 키-값 저장소 같은 외부 저장소에 값을 두고, 동시 요청에서도 맞게 늘어나도록 저장소의 증가 연산을 씁니다.
Route Handler와 런타임 설정은 Next.js 공식 문서의 route.js 레퍼런스와 Route Segment Config 레퍼런스에서 확인할 수 있습니다.
다음 글 WebSocket 통합에서는 서버와 클라이언트가 실시간으로 데이터를 주고받는 WebSocket을 Next.js 애플리케이션에 연결하는 방법을 알아봅니다.