Next.js
강의

#2 - BFF를 이해하면 절대 포기할 수 없는 Next.js Server Actions

중년개발자
중년개발자

@loxo

29일 전

35

BFF를 이해하면 절대 포기할 수 없는 Next.js Server Actions

프론트엔드가 단순히 “API를 호출하는 화면”이던 시절은 이미 지났습니다.
오늘날의 웹 애플리케이션은 인증, 권한, 캐시, 상태 동기화까지 사실상 하나의 서버 역할을 요구받고 있습니다.

이 흐름 속에서 등장한 개념이 바로 BFF(Backend For Frontend) 입니다.
프론트엔드에 최적화된 전용 백엔드를 두고, UI와 비즈니스 로직 사이의 간극을 최소화하는 패턴이죠.

Next.js Server Actions는 이 BFF 패턴을 가장 단순하고 강력하게 구현한 기능입니다.
별도의 API 레이어를 만들지 않아도,

  • 서버 로직을 안전하게 실행하고
  • 쿠키와 세션을 다루며
  • 캐시를 무효화하고
  • 클라이언트에서는 함수처럼 호출할 수 있습니다.
  • 좋아요, 북마크, 신고하기 등 사용자와 인터랙션이 바로 발생하는 부분에서 빛을 발합니다.

이 글을 끝까지 읽고 나면,
**“이걸 알고도 API Routes부터 만드는 건 낭비다”**라는 생각이 들게 될 겁니다.


Server Actions vs API Routes 완벽 가이드

📌 Server Actions란?

Server Actions는 Next.js 13+에서 도입된 서버에서 실행되는 비동기 함수로, 클라이언트에서 직접 호출할 수 있습니다.

핵심 특징

tsx
// actions/user.action.ts 'use server'; // ⭐ 이 디렉티브가 핵심! import { cookies, headers } from 'next/headers'; export async function updateUserProfile(formData: FormData) { // ✅ 쿠키 접근 가능 const cookieStore = await cookies(); const token = cookieStore.get('auth-token'); // ✅ 헤더 접근 가능 const headersList = await headers(); const userAgent = headersList.get('user-agent'); // ✅ DB 접근 가능 await db.update('users', formData); // ✅ 캐시 무효화 가능 revalidatePath('/profile'); return { success: true }; }

작동 원리:

plaintext
[클라이언트] ↓ updateUserProfile(formData) 호출 [Next.js 자동 처리] - POST /api/__next_action 요청 생성 - FormData를 body에 포함 [서버] - Server Action 함수 실행 - DB 업데이트 - 결과 반환 [클라이언트] - 결과 수신

🆚 Server Actions vs API Routes 비교

기본 구조 비교

Server Actions
tsx
// actions/posts.action.ts 'use server'; import { cookies } from 'next/headers'; import { revalidatePath } from 'next/cache'; export async function createPost(formData: FormData) { const cookieStore = await cookies(); const userId = cookieStore.get('user-id')?.value; await db.insert('posts', { title: formData.get('title'), userId, }); revalidatePath('/posts'); return { success: true }; }

사용:

tsx
'use client'; export default function CreatePostForm() { const handleSubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const result = await createPost(formData); // ✅ 함수처럼 호출 }; return <form onSubmit={handleSubmit}>...</form>; }
API Routes
tsx
// app/api/posts/route.ts import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { const cookies = request.cookies; const userId = cookies.get('user-id')?.value; const body = await request.json(); await db.insert('posts', { title: body.title, userId, }); return NextResponse.json({ success: true }); }

사용:

tsx
'use client'; export default function CreatePostForm() { const handleSubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); // ✅ HTTP 요청으로 호출 const response = await fetch('/api/posts', { method: 'POST', body: JSON.stringify({ title: formData.get('title') }), }); const result = await response.json(); }; return <form onSubmit={handleSubmit}>...</form>; }

🎯 언제 무엇을 사용할까?

✅ Server Actions를 사용해야 할 때

1. 폼 제출 및 데이터 변경 (Mutations)
tsx
// ✅ 권장: Server Actions 'use server'; export async function submitContactForm(formData: FormData) { const name = formData.get('name'); const email = formData.get('email'); await db.insert('contacts', { name, email }); revalidatePath('/contacts'); return { success: true }; }

이유:

  • 폼 데이터 자동 처리
  • revalidatePath 자동 캐시 무효화
  • 타입 안전성 (TypeScript)
  • 보일러플레이트 코드 최소화
2. 사용자 인터랙션 (좋아요, 북마크 등)
tsx
// ✅ 권장: Server Actions 'use server'; export async function toggleLike(postId: string) { const userId = await getAuthUserId(); await db.toggleLike(userId, postId); revalidatePath('/posts'); return { success: true }; }

이유:

  • 간단한 mutations
  • 빠른 구현
  • 캐시 무효화 간편
3. 인증이 필요한 Private Actions
tsx
// ✅ 권장: Server Actions 'use server'; import { cookies } from 'next/headers'; export async function deletePost(postId: string) { const cookieStore = await cookies(); const session = cookieStore.get('session')?.value; if (!session) { return { error: 'Unauthorized' }; } await db.delete('posts', postId); revalidatePath('/posts'); return { success: true }; }

이유:

  • 쿠키/헤더 접근 가능
  • 타입 안전한 에러 처리
  • 클라이언트에서 간편하게 호출
4. 서버 상태 변경 후 UI 자동 업데이트
tsx
// ✅ 권장: Server Actions 'use server'; export async function updateSettings(settings: Settings) { await db.update('settings', settings); // ✅ Next.js 캐시 자동 무효화 revalidatePath('/settings'); revalidateTag('user-settings'); return { success: true }; }

이유:

  • revalidatePath, revalidateTag 사용 가능
  • Server Component 자동 재렌더링

✅ API Routes를 사용해야 할 때

1. GET 요청 (데이터 조회)
tsx
// ✅ 권장: API Routes // app/api/posts/route.ts export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const category = searchParams.get('category'); const limit = parseInt(searchParams.get('limit') || '10'); const posts = await PostService.getPosts({ category, limit }); return NextResponse.json(posts); }

이유:

  • Server Actions는 POST만 지원 (GET 불가)
  • URL로 데이터 전달 가능
  • 브라우저에서 직접 접근 가능 (디버깅 편함)
  • React Query와 자연스러운 통합
2. Webhook 및 외부 시스템 통합
tsx
// ✅ 권장: API Routes // app/api/webhooks/stripe/route.ts export async function POST(request: NextRequest) { const signature = request.headers.get('stripe-signature'); const body = await request.text(); // Stripe webhook 검증 const event = stripe.webhooks.constructEvent(body, signature, secret); // 비즈니스 로직 처리 await handleStripeEvent(event); return NextResponse.json({ received: true }); }

이유:

  • 외부 시스템이 직접 호출
  • 커스텀 헤더 검증 필요
  • Raw body 접근 필요
3. Public API Endpoints
tsx
// ✅ 권장: API Routes // app/api/public/stats/route.ts export async function GET() { const stats = await db.getPublicStats(); return NextResponse.json(stats, { headers: { 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'public, max-age=3600', }, }); }

이유:

  • CORS 설정 필요
  • 커스텀 헤더 설정
  • 퍼블릭 접근
4. 파일 업로드/다운로드
tsx
// ✅ 권장: API Routes // app/api/upload/route.ts export async function POST(request: NextRequest) { const formData = await request.formData(); const file = formData.get('file') as File; const buffer = Buffer.from(await file.arrayBuffer()); await uploadToS3(buffer, file.name); return NextResponse.json({ url: s3Url }); }

이유:

  • 파일 처리에 최적화
  • Progress 이벤트 처리 가능
  • 스트리밍 지원
5. 복잡한 HTTP 응답 제어
tsx
// ✅ 권장: API Routes // app/api/download/route.ts export async function GET() { const csvData = await generateCSV(); return new NextResponse(csvData, { status: 200, headers: { 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename=export.csv', }, }); }

이유:

  • 커스텀 응답 타입
  • 파일 다운로드
  • 스트리밍 응답

📊 의사결정 플로우차트

plaintext
데이터 페칭 시작 [질문 1] 읽기(GET) vs 쓰기(POST/PUT/DELETE)? 읽기 → API Routes ✅ 쓰기 [질문 2] 외부 시스템에서 호출? (Webhook 등) Yes → API Routes ✅ No [질문 3] 파일 업로드/다운로드? Yes → API Routes ✅ No [질문 4] Public API (CORS 필요)? Yes → API Routes ✅ No [질문 5] 커스텀 HTTP 헤더/응답 타입 필요? Yes → API Routes ✅ No Server Actions ✅ (가장 간단!)

🔑 Cookie 및 Headers 접근

🚨 중요: 둘 다 쿠키/헤더 접근 가능!

Server Actions에서 쿠키 접근
tsx
// ✅ Server Actions도 쿠키 접근 가능! 'use server'; import { cookies, headers } from 'next/headers'; export async function getAuthUser() { const cookieStore = await cookies(); const session = cookieStore.get('session')?.value; const headersList = await headers(); const userAgent = headersList.get('user-agent'); return { session, userAgent }; }
API Routes에서 쿠키 접근
tsx
// ✅ API Routes도 쿠키 접근 가능! export async function GET(request: NextRequest) { const session = request.cookies.get('session')?.value; const userAgent = request.headers.get('user-agent'); return NextResponse.json({ session, userAgent }); }

결론: 쿠키/헤더 접근은 둘 다 가능하므로 선택 기준이 아닙니다!


📋 실전 사용 예시

postgresql.co.kr 프로젝트 패턴

✅ Server Actions 사용
tsx
// actions/interaction/like.action.ts 'use server'; export async function toggleLikeAction(postId: string) { const userId = await getAuthUserId(); // 쿠키에서 인증 정보 await LikeService.toggleLike(userId, postId); revalidatePath('/posts'); return { success: true }; }

이유:

  • 단순한 mutation
  • 폼 데이터 없음
  • 캐시 무효화 필요
✅ API Routes 사용
tsx
// app/api/studio/[category]/route.ts export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const category = searchParams.get('category'); const filter = searchParams.get('filter'); const posts = await PostService.getPosts({ category, filter }); return NextResponse.json(posts); }

이유:

  • GET 요청
  • React Query로 데이터 페칭
  • 무한 스크롤 구현

🎯 최종 결정 가이드

상황권장 방법이유
폼 제출Server Actions간단, 타입 안전
좋아요, 북마크Server ActionsMutation + 캐시 무효화
데이터 조회 (GET)API RoutesServer Actions는 POST만 지원
무한 스크롤API RoutesReact Query 통합
WebhookAPI Routes외부 시스템 호출
파일 업로드API Routes파일 처리 최적화
Public APIAPI RoutesCORS, 커스텀 헤더
Private MutationsServer Actions간단하고 안전

✨ 권장 사항 요약

Server Actions를 기본으로 사용하세요

tsx
// ✅ 간단하고 직관적 'use server'; export async function updateUser(data: FormData) { await db.update('users', data); revalidatePath('/profile'); return { success: true }; }

다음 경우에만 API Routes 사용

  1. GET 요청이 필요할 때
  2. 외부 시스템 통합 (Webhook 등)
  3. 파일 업로드/다운로드
  4. Public API 제공
  5. 커스텀 HTTP 응답 제어

🚀 마이그레이션 가이드

API Routes → Server Actions

Before (API Routes):

tsx
// app/api/posts/like/route.ts export async function POST(request: NextRequest) { const { postId } = await request.json(); const userId = await getAuthUserId(); await db.toggleLike(userId, postId); return NextResponse.json({ success: true }); } // Client await fetch('/api/posts/like', { method: 'POST', body: JSON.stringify({ postId }), });

After (Server Actions):

tsx
// actions/like.action.ts 'use server'; export async function toggleLike(postId: string) { const userId = await getAuthUserId(); await db.toggleLike(userId, postId); return { success: true }; } // Client await toggleLike(postId); // ✅ 훨씬 간단!

이점:

  • 50% 코드 감소
  • 타입 안전성 향상
  • 자동 직렬화/역직렬화
  • 보일러플레이트 제거

📚 참고 자료

목차

#Next.js#Server Actions#BFF#Frontend Development#Web Architecture

댓글 1

Ctrl + Enter를 눌러 등록할 수 있습니다
※ AI 다듬기는 내용을 정제하는 보조 기능이며, 최종 내용은 사용자가 확인해야 합니다.