서버리스 함수 활용
Route Handler·서버 액션·Proxy와 Edge 런타임을 실행 위치와 제한에 맞게 선택합니다.
Next.js는 React 프레임워크를 넘어 SSR, SSG, API 처리까지 아우르는 풀스택 프레임워크로 발전했습니다.
특히 서버리스 함수(Serverless Functions)를 통해 별도 서버를 직접 운영하지 않고도 API 엔드포인트와 백엔드 로직을 구현할 수 있습니다.
이 함수들은 Vercel 같은 플랫폼에서 자동 배포/관리되므로 인프라 부담을 줄이면서 빠르게 기능을 확장할 수 있습니다.
이 절에서는 Route Handler, Server Actions, Proxy와 선택적 Edge 런타임의 실행 위치를 구분합니다.
먼저 서버 로직이 Route Handler, Server Action, Proxy 중 어디에 배치되는지 큰 그림을 잡습니다.
Next.js의 API Route, Server Action, Edge Function은 실행 위치와 제약이 다르므로 요청 성격에 맞춰 골라야 한다.
- RequestHTTP / Form / Proxy
짧은 이벤트가 진입
- Function짧은 로직
DB/API/cache 호출
- ResultResponse / cache sync
Response.json 또는 redirect를 반환하고 변경 뒤에는 재검증 함수를 명시적으로 호출
- Limitcold start/time limit
긴 작업은 분리
서버리스 함수란 무엇이며 왜 중요한가요?
서버리스 함수 (Serverless Functions)는 클라우드 공급자(AWS Lambda, Google Cloud Functions, Azure Functions 등)가 서버 인프라 관리를 전적으로 담당하고, 개발자는 코드만 작성해 배포하는 컴퓨팅 모델입니다.
코드는 이벤트(예: HTTP 요청, 데이터베이스 변경)에 의해 트리거될 때만 실행되며, 사용량에 따라 비용이 청구됩니다.
서버리스 함수의 주요 이점- 인프라 관리 부담 감소: 서버를 프로비저닝, 패치, 스케일링할 필요가 없습니다. 클라우드 공급자가 모든 인프라를 관리합니다.
- 자동 스케일링: 트래픽 증가에 따라 자동으로 함수 인스턴스가 확장(스케일 아웃)되어 대규모 트래픽도 안정적으로 처리할 수 있습니다.
- 비용 효율성: 코드가 실행될 때만 비용이 발생합니다(Pay-per-execution 모델). 유휴 시간에는 비용이 발생하지 않아 효율적입니다.
- 빠른 배포: 코드 변경 사항을 빠르게 배포하고 적용할 수 있습니다.
- 개발 생산성 향상: 백엔드 인프라 걱정 없이 비즈니스 로직 구현에 집중할 수 있습니다.
Next.js는 이러한 서버리스 함수의 이점을 애플리케이션에 쉽게 통합할 수 있도록 지원합니다.
Next.js에서 서버리스 함수 구현하기
App Router의 주요 서버 실행 단위는 Route Handler, Server Actions와 Proxy입니다.
Route Handler
Route Handler는 App Router에서 HTTP 엔드포인트를 만드는 기본 방식입니다.
src/app/api 아래의 route.ts 파일이 경로에 대응하며 HTTP 메서드 이름으로 처리 함수를 내보냅니다.
// 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 });
}- HTTP 요청(GET, POST 등)에 응답하는 RESTful API 엔드포인트를 쉽게 구축할 수 있습니다.
- 클라이언트에서는
fetch로 이 엔드포인트를 호출할 수 있습니다. - Vercel에 배포 시 자동으로 서버리스 함수로 변환됩니다.
- 데이터베이스 접근, 외부 API 호출, 인증 처리 등 다양한 백엔드 로직을 구현할 수 있습니다.
Server Actions (App Router Only)
Server Actions는 App Router에서 클라이언트의 폼 제출이나 이벤트를 서버 함수와 연결하는 기능입니다.
데이터 변경과 폼 처리를 구현할 때 입력 검증, 권한 확인, 캐시 재검증 경계를 함께 둡니다.
Server Actions 예시 (src/app/[locale]/add-todo/page.tsx와 actions.ts)
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>
);
}'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 갱신
}브라우저의 required와 maxLength는 편의를 위한 1차 검증일 뿐 우회할 수 있습니다.
따라서 Server Action에서도 타입, 공백 제거 뒤 길이, 업무 규칙을 다시 확인해야 합니다.
특징- 클라이언트와 서버 간의 데이터 직렬화 및 통신을 Next.js가 자동으로 처리하여 개발 복잡성을 줄입니다.
- 폼 제출, 버튼 클릭 등 UI 상호작용에 직접적으로 반응하는 서버 로직을 작성하기에 이상적입니다.
- Next.js 캐시 API와 통합됩니다. 경로는
revalidatePath, 태그의 SWR 갱신은revalidateTag(tag, 'max'), Server Action의 즉시 만료는updateTag로 처리합니다. - Vercel에 배포 시 서버리스 함수로 변환됩니다.
Proxy와 Edge Runtime
Next.js 16은 요청 앞단의 파일 규칙을 middleware.ts에서 proxy.ts로 바꿨습니다.
Proxy는 Node.js 런타임에서 실행되므로 데이터베이스 드라이버와 Node.js API를 사용할 수 있습니다.
다만 모든 요청의 초입에서 실행될 수 있으므로 복잡한 조회와 긴 작업을 넣지 않습니다.
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 이름 변경은 단순한 파일명 교체가 아니라 요청 경계를 명시하는 변화입니다.
인증과 권한을 다룰 때는 직접 쿠키 값만 보는 예제보다 Auth.js 같은 인증 계층의 검증 결과를 사용합니다.
Edge Runtime이 필요한 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!' });
}Edge Runtime 선택 기준 (Node.js 런타임과 비교)
Edge Runtime과 Node.js Runtime은 빠르냐 느리냐만으로 고르는 선택지가 아닙니다.
의존성, 데이터 접근, 실행 제한을 먼저 비교해야 합니다.
런타임 선택은 성능 취향이 아니라 의존성, 데이터 접근, 실행 제한을 먼저 확인하는 결정이다.
- Edge Runtime
적합: 인증 기준, 리다이렉트, 지역 기반 분기, A/B 테스트 부적합: fs, 네이티브 모듈, 무거운 DB SDK
- Node.js Runtime
적합: DB 접근, 파일 처리, 복잡한 비즈니스 로직 주의: 리전 위치와 콜드 스타트 지연
| 질문 | Edge로 가는 경우 | Node로 가는 경우 |
|---|---|---|
| Node 전용 API가 필요한가 | 아니오, Web API만 사용 | 예, fs/crypto/SDK 필요 |
| 응답 전에 초고속 분기가 필요한가 | 예, 요청 초입에서 판단 | 아니오, 데이터 처리 중심 |
| 비밀키와 DB 연결이 무거운가 | 가벼운 토큰 검증 | 풀 연결, 트랜잭션 필요 |
| 항목 | Edge Runtime | Node.js Runtime |
|---|---|---|
| 최적 용도 | 가벼운 지역 분산 응답 | Proxy, DB 접근, 파일 처리, 업무 로직 |
| 지연 시간 | 매우 낮음 (엣지 실행) | 상대적으로 높을 수 있음 (리전 중심) |
| 사용 가능한 API | Web Standard 중심, Node 내장 모듈 제약 | Node 생태계 전반 사용 가능 |
| 실행 시간/메모리 | 더 엄격한 제한 환경 | 상대적으로 여유로운 환경 |
- 로직이
fs, 네이티브 모듈, 특정 Node 전용 SDK에 의존하는지 먼저 확인합니다. - 의존성이 있다면 Node 런타임을 선택합니다.
- 의존성이 없고 Route Handler를 사용자 가까이 배치해야 할 근거가 있으면 Edge Runtime을 검토합니다.
- 트래픽 급증 구간에서 p95/p99 지연 시간을 비교 측정한 뒤 최종 확정합니다.
Edge Runtime Pitfall 체크리스트
- Edge에서 동작하지 않는 라이브러리(Node 전용 모듈 의존)를 사전에 점검했는가?
- Proxy와 Edge Route Handler의 런타임 차이를 확인했는가?
- Edge/Node 런타임 혼용 시 환경 변수 및 비밀키 접근 정책을 분리했는가?
- 실패 시 Node.js Route Handler로 우회할 fallback 경로를 준비했는가?
Edge를 선택할 때는 사용할 수 없는 Node API와 fallback 경로를 먼저 점검해야 합니다.
Edge Runtime은 전 세계 가까운 위치에서 실행되는 대신 사용할 수 있는 API와 실행 시간이 더 엄격하다.
- dependencyNode 전용 모듈
fs, net, 일부 DB 드라이버, 네이티브 의존 패키지는 Edge에서 실패할 수 있다.
- latency과한 미들웨어
모든 요청 초입에서 무거운 연산을 하면 TTFB가 나빠진다.
- secret환경 변수 정책
Edge와 Node의 비밀키 접근 범위와 노출 가능성을 분리해 본다.
- fallbackNode 우회 경로
Edge에서 처리할 수 없는 경우 Node API Route로 넘기는 기준을 둔다.
서버리스 함수 활용 전략 및 고려사항
-
적절한 런타임 선택
- Route Handler·Server Actions·Proxy: 기본 Node.js 런타임에서 데이터베이스 접근과 업무 로직을 처리합니다.
- Edge Route Handler: 지역 분산 실행의 이점이 측정되고 Node.js 전용 의존성이 없는 응답에 한정합니다.
- 환경 변수 관리: 민감한 정보(API 키, DB 연결 문자열)는 환경 변수로 관리하고, Vercel 대시보드나
.env파일을 통해 안전하게 주입합니다. - 콜드 스타트(Cold Start) 이해: 서버리스 함수는 일정 시간 사용되지 않으면 콜드 상태가 됩니다. 첫 요청 시 컨테이너가 시작되어 지연이 생길 수 있으므로 중요한 API는 실제 배포 환경의 시작 시간을 측정합니다.
- 상태 비저장(Stateless) 설계: 서버리스 함수는 상태를 유지하지 않는(stateless) 방식으로 설계해야 합니다. 사용자 세션 정보나 데이터는 데이터베이스, 캐시(Redis), 또는 외부 스토리지에 저장해야 합니다.
- 로그 및 모니터링: Vercel 대시보드에서 서버리스 함수의 실행 로그와 성능 지표를 모니터링할 수 있습니다. 문제 발생 시 디버깅에 활용합니다.
- 오류 처리: 서버리스 함수 내에서 발생하는 오류를 적절히 처리하고 로깅하여 안정성을 확보합니다.
- 보안: Route Handler와 Server Actions의 입력에 인증, 인가와 유효성 검사를 적용해야 합니다.
위 전략을 배포 전 점검 기준으로 정리하면, 런타임 선택과 상태 저장 위치를 먼저 분리해서 볼 수 있습니다.
함수 자체는 짧게 실행되고 사라진다. 상태, 비밀키, 로그, 실패 처리는 외부 기준으로 설계해야 한다.
- 입력 검증
요청 body와 권한을 함수 시작 지점에서 확인한다.
- 짧은 실행
긴 작업은 queue나 background job으로 분리한다.
- 명시적 실패
timeout, retry, fallback 응답을 API 계약에 포함한다.
| 점검 | 좋은 기준 | 나쁜 신호 | 확인 위치 |
|---|---|---|---|
| Runtime | Edge/Node 선택 이유 명확 | Node SDK를 Edge에 배치 | route config |
| State | DB/Redis/Storage에 보관 | 메모리 전역 변수 의존 | data layer |
| Secrets | 서버 전용 env로 제한 | 클라이언트 번들 노출 | env prefix |
| Observability | 로그, latency, error rate 추적 | 실패 재현 불가 | Vercel logs |
다음으로는 요청 성격에 따라 어떤 실행 위치가 맞는지 비교합니다.
서버리스 선택은 “서버에서 돈다”가 아니라 요청의 성격을 보고 위치를 고르는 일이다.
| 요청 성격 | 추천 위치 | 이유 | 주의점 |
|---|---|---|---|
| 로그인 전 redirect | Proxy | 가장 앞에서 빠르게 분기 | 무거운 DB 조회 금지 |
| 폼 제출과 데이터 변경 | Server Action | 경로는 revalidatePath, 즉시 tag는 updateTag, SWR tag는 revalidateTag(tag, 'max') | 권한, CSRF, 입력 검증 |
| 외부 API 프록시 | API Route | 비밀키를 서버에 숨김 | rate limit, timeout |
| 무거운 DB 트랜잭션 | Node Runtime | Node SDK와 연결 관리 | 콜드 스타트와 리전 |
Next.js의 서버리스 함수 기능은 화면 코드와 서버 로직을 같은 프로젝트 안에서 관리하게 해 줍니다.
Route Handler, Server Actions, Proxy와 선택적 Edge Route Handler는 호출 방식, 데이터 접근과 배포 제약이 다르므로 요청 성격에 맞게 선택해야 합니다.
세 방식은 모두 서버에서 실행되지만 호출 방식, 데이터 경계, 배포 제약이 다르다.
- API Routes
좋음: 외부 클라이언트도 호출하는 HTTP API 주의: 인증과 rate limit이 빠지기 쉬움
- Server Actions
좋음: 폼 제출, 데이터 변경, revalidatePath/updateTag 호출 주의: SWR tag는 revalidateTag(tag, 'max'), 클라이언트 입력은 검증
- Edge Functions
좋음: 인증 초입, redirect, geo 분기 주의: Node API와 무거운 SDK 제약
| 질문 | 선택 | 확인할 것 |
|---|---|---|
| 브라우저 밖에서도 호출해야 하나 | API Route | HTTP method, auth, schema |
| 폼 액션과 UI 갱신이 붙어 있나 | Server Action | 권한, revalidatePath/updateTag, stale 허용 시 revalidateTag(tag, 'max') |
| 요청 초입에서 빠르게 분기하나 | Edge Function | 지원 API, TTFB, fallback |
마지막 비교표에서는 Route Handler, Server Actions, Proxy와 Edge Runtime의 선택 기준을 호출 방식과 실패 처리까지 함께 놓고 봅니다.
기능 이름보다 요청이 실제로 무엇을 해야 하는지가 런타임 선택을 결정한다.
| 선택지 | 호출 방식 | 데이터 접근 | 실패 처리 |
|---|---|---|---|
| API Route | fetch/외부 HTTP | 서버 비밀키, DB 가능 | status code와 JSON 계약 |
| Server Action | form/action 호출 | DB 변경 + revalidatePath/updateTag, SWR은 revalidateTag(tag, 'max') | 폼 오류, optimistic UI 조정 |
| Edge Function | 요청 초입 자동 실행 | 가벼운 토큰/헤더 중심 | Node route로 fallback |
| Node Runtime | API/Action 내부 런타임 | Node SDK와 트랜잭션 | timeout, retry, log |
운영 단계에서는 런타임 경계, 캐시 전략, 통신 실패 대응을 분리해 관리해야 합니다.
함수는 짧게 실행되지만 사용자는 느린 응답, 오래된 데이터, 실패 메시지를 경험한다.
- boundary런타임 경계
boundary Edge와 Node의 의존성, 비밀키, 실행 시간을 분리한다.
- cache캐시 전략
cache GET 응답, revalidate, stale 데이터 표시를 요청별로 정한다.
- failure실패 응답
failure timeout, 권한 오류, 외부 API 실패를 같은 JSON 계약으로 돌려준다.
| 운영 질문 | 좋은 답 | 나쁜 신호 |
|---|---|---|
| 어디서 실행되는가 | route별 runtime 명시 | Edge에서 Node SDK 사용 |
| 얼마나 오래 저장되는가 | cache/revalidate 정책 문서화 | 사용자별 응답 cache-first |
| 실패를 어떻게 알리는가 | 표준 오류 코드와 retry 안내 | 무한 로딩 또는 빈 응답 |