Server Actions는 함수 호출이 아니라 POST 요청 계약이다
<form action={serverAction}>을 함수 호출처럼 믿으면 어떤 권한·데이터·캐시 버그를 놓치게 될까?
Server Actions는 함수 호출이 아니라 POST 요청 계약이다
- 카테고리: web_app_dev
- 예상 읽기 시간: 10분
- 오늘의 질문:
<form action={serverAction}>을 함수 호출처럼 믿으면 어떤 권한·데이터·캐시 버그를 놓치게 될까? - 핵심 출처:
- React 19.2.7 release - 2026-06-01, 확인일 2026-06-24
- Next.js v16.2.7 release - 2026-06-01, 확인일 2026-06-24
- Next.js Forms with Server Actions - n.d., 확인일 2026-06-24
- Next.js Mutating Data - n.d., 확인일 2026-06-24
- Next.js v16.2.6 security release - 2026-05-07, 확인일 2026-06-24
1. 왜 지금 봐야 하나
React와 Next.js의 최근 안정화 릴리스를 보면 Server Actions의 본질이 드러난다. React 19.2.7은 Server Actions에서 FormData 항목이 누락되던 회귀를 수정했다. Next.js v16.2.7도 16.2 계열에 FormData 항목을 떨어뜨리지 않는 수정과, middleware rewrite 상황에서 server action forwarding loop가 생기는 문제 수정을 백포트했다.
이런 항목은 화려한 신기능처럼 보이지 않는다. 하지만 웹앱 개발자에게는 꽤 중요한 신호다. Server Action은 “서버에 있는 함수를 클라이언트에서 바로 호출하는 문법 설탕”처럼 보이지만, 실제로는 브라우저의 폼 제출, 네트워크 POST, 프레임워크 라우팅, 인증 상태, 캐시 무효화가 한 번에 얽힌 요청 계약이다.
즉 버그가 생기는 지점도 함수 본문 하나가 아니라 다음 경계들이다.
- 브라우저가 어떤 필드를
FormData로 보냈는가 - 프레임워크가 그
FormData를 서버 함수까지 손실 없이 전달했는가 - middleware, rewrite, basePath, 캐시가 액션 요청의 목적지를 바꾸지 않았는가
- 액션 내부에서 인증·인가·소유권을 다시 확인했는가
- mutation 이후 사용자가 보는 데이터가 적절히 갱신되는가
오늘의 핵심 문장: Server Actions는 함수 호출처럼 생겼지만, 운영에서는 직접 호출 가능한 POST 엔드포인트처럼 검증해야 한다.
2. 핵심 개념
Next.js 문서는 React Server Actions를 “서버에서 실행되는 Server Functions”로 설명한다. 폼의 action 속성에 서버 함수를 연결하면, 그 함수는 자동으로 native FormData 객체를 받는다. 그래서 코드는 이렇게 단순해진다.
export async function createPost(formData: FormData) {
'use server'
const title = formData.get('title')
const content = formData.get('content')
// mutate data
// revalidate cache
}
하지만 이 단순함 때문에 오해가 생긴다. 이 코드는 로컬 함수 호출이 아니다. Next.js의 Mutating Data 문서는 Server Functions가 클라이언트에서 네트워크 요청으로 호출될 수 있기 때문에 async여야 하고, Server Actions는 POST 요청으로 호출된다고 설명한다. 또한 UI를 통해서만 접근된다고 가정하지 말고, 모든 Server Function 내부에서 인증과 인가를 확인하라고 경고한다.
이 관점으로 보면 Server Action 설계에는 최소 네 가지 계약이 있다.
- 입력 계약:
FormData에 어떤 name이 들어오는가. 여러 필드를Object.fromEntries(formData)로 바꿀 때$ACTION_접두사의 내부 필드가 섞일 수 있다는 점까지 고려해야 한다. - 권한 계약: 페이지가 로그인 사용자에게만 보였더라도 action 내부에서
auth()와 리소스 소유권을 다시 확인해야 한다. - 라우팅 계약: middleware, rewrite, basePath, adapter가 action 요청을 다른 목적지로 보내거나 반복 forwarding하지 않아야 한다.
- 신선도 계약: mutation 이후
revalidatePath,revalidateTag, redirect, optimistic UI 중 무엇으로 사용자 화면을 갱신할지 정해야 한다.
3. 최신 이슈와 연결
React 19.2.7의 릴리스 노트는 한 줄짜리 수정이다. “19.2.6에서 회귀한 Server Actions의 missing FormData entries를 수정했다.” Next.js v16.2.7도 “FormData entries를 떨어뜨리지 않도록” 수정했다. 둘 다 같은 방향을 가리킨다. Server Action의 입력은 단순 객체가 아니라 브라우저 폼 데이터라는 전송 형식에 묶여 있고, 프레임워크 레이어가 그 형식을 정확히 보존해야 한다.
Next.js v16.2.7의 다른 수정도 흥미롭다. middleware rewrite와 함께 server action forwarding loop가 생기는 문제, basePath와 rewrite 조합에서 catch-all router.query가 깨지는 문제, cache tag의 비ASCII 문자 인코딩 문제 등이 백포트됐다. 이는 action을 안전하게 쓰려면 “함수 본문 타입이 맞는가”만 보면 부족하다는 뜻이다. 액션 요청은 앱 라우터, 미들웨어, 캐시 태그, 어댑터와 함께 움직인다.
바로 직전 v16.2.6 보안 릴리스에는 App Router middleware/proxy bypass, React Server Component 응답 cache poisoning, Cache Components 관련 DoS 같은 항목이 포함됐다. 이 글이 보안 권고 하나를 자세히 분석하려는 것은 아니다. 다만 App Router 시대의 서버 기능은 “컴포넌트 코드”와 “HTTP 경계”가 붙어 있기 때문에, 권한과 캐시를 컴포넌트 편의성 뒤에 숨기면 안 된다는 교훈을 준다.
4. 개발자 관점 해석
작은 팀에서 Server Actions를 도입할 때 흔한 패턴은 “API route를 만들지 않아도 되니 빠르다”이다. 맞다. 폼, validation, pending UI, mutation, cache revalidation을 한 흐름으로 묶을 수 있다. 하지만 API route를 없앴다고 해서 API 설계가 사라지는 것은 아니다.
예를 들어 게시글 삭제 action을 생각해보자.
'use server'
export async function deletePost(formData: FormData) {
const id = formData.get('id')
await db.post.delete({ where: { id: String(id) } })
}
이 코드는 화면에서는 잘 동작할 수 있다. 하지만 요청 계약으로 보면 위험하다.
id가 문자열 하나로 들어온다는 검증이 없다.- 현재 사용자가 로그인했는지 확인하지 않는다.
- 그 사용자가 해당 게시글 소유자인지 확인하지 않는다.
- 삭제 후 목록·상세 캐시를 어떻게 갱신할지 정하지 않았다.
- hidden input에 넣은 값은 HTML에 그대로 노출될 수 있다.
조금 더 안전한 관점은 이렇다.
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { auth } from '@/lib/auth'
const schema = z.object({
id: z.string().uuid(),
})
export async function deletePost(formData: FormData) {
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
const parsed = schema.safeParse({
id: formData.get('id'),
})
if (!parsed.success) {
return { ok: false, message: '잘못된 요청입니다.' }
}
const post = await db.post.findUnique({
where: { id: parsed.data.id },
select: { id: true, ownerId: true },
})
if (!post || post.ownerId !== session.user.id) {
throw new Error('Forbidden')
}
await db.post.delete({ where: { id: post.id } })
revalidatePath('/posts')
return { ok: true }
}
핵심은 코드가 길어졌다는 것이 아니다. Server Action을 “직접 호출 가능한 mutation endpoint”로 다뤘다는 점이다.
5. 내 프로젝트에 적용할 체크포인트
Server Actions를 쓰고 있다면 오늘 10분 동안 아래를 점검해보자.
입력
- 모든 action에서
FormData.get()결과를 바로 DB에 넣지 않는가? zod,valibot같은 스키마로 타입·필수값·범위를 검증하는가?Object.fromEntries(formData)를 쓴다면$ACTION_내부 필드를 필터링하는가?- checkbox, multi-select, file input처럼
get()하나로 부족한 필드는getAll()또는 별도 처리를 하는가?
권한
- 페이지 접근 제어와 별개로 action 내부에서
auth()를 다시 호출하는가? - update/delete 계열 mutation에서 리소스 소유권 또는 역할을 확인하는가?
- 관리자 action은 service role, admin client, RLS bypass 범위를 분리하는가?
- hidden input에 권한 판단용 값을 넣고 믿지는 않는가?
라우팅·미들웨어
- middleware rewrite가 action POST 요청에도 의도대로 동작하는가?
- basePath, locale, proxy, adapter 환경에서 action URL이 바뀌지 않는가?
- 인증 middleware가 GET 페이지와 POST action을 동일한 가정으로 처리하지 않는가?
캐시·UX
- mutation 후
revalidatePath또는revalidateTag범위가 너무 넓거나 좁지 않은가? - optimistic UI가 실패했을 때 되돌아가는 흐름이 있는가?
- pending state와 validation error가 스크린리더에 전달되도록
aria-live같은 접근성 처리를 했는가?
6. 오늘 10분 액션
- 프로젝트에서 가장 위험한 Server Action 하나를 고른다. 보통 delete, role 변경, 결제/구독 변경, 관리자 기능이 좋다.
- 그 action 위에 주석으로 “이 action은 어떤 POST 요청 계약인가?”를 적는다.
- 입력 필드
- 인증 조건
- 인가 조건
- mutation 대상
- 캐시 갱신 대상
FormData.get()결과를 스키마 검증으로 감싼다.- action 내부에 인증·소유권 확인이 없다면 추가한다.
- 브라우저 UI가 아니라 직접 POST 가능하다는 가정으로 테스트 케이스를 하나 만든다.
작은 예시는 이런 식이다.
// 계약:
// - 입력: id(uuid)
// - 인증: 로그인 사용자
// - 인가: post.ownerId === session.user.id
// - 부작용: post 삭제
// - 갱신: /posts 목록 재검증
이 주석 하나만으로도 코드 리뷰의 질문이 바뀐다. “잘 돌아가나요?”가 아니라 “이 요청 계약이 깨지는 입력과 권한 경계는 무엇인가요?”가 된다.
7. 더 볼 자료
- React 19.2.7 릴리스: Server Actions의
FormDataentries 회귀 수정 확인 - Next.js v16.2.7 릴리스:
FormDataentries, server action forwarding loop, rewrite/basePath 관련 백포트 확인 - Next.js Forms 가이드:
FormData,bind,useActionState, 서버 검증, 접근성 메시지 패턴 확인 - Next.js Mutating Data 문서: Server Functions가 네트워크 POST로 호출되며 action 내부 인증·인가가 필요하다는 경고 확인
- Next.js v16.2.6 릴리스: App Router, middleware/proxy, RSC cache 관련 보안 경계 확인
중복 회피 메모
로컬 content/generated 경로와 Supabase 최근 web_app_dev 글을 확인했다. 최근 글은 Supabase OAuth/RLS 위임 권한, npm trusted publishing, View Transition 상태 변화 계약, Supabase 브랜칭/pg-delta, agent-friendly content negotiation, React Compiler, Next.js Cache Components, Hono 보안 릴리스 등을 다뤘다. 이번 글은 Next.js와 React를 다시 언급하지만 캐시 컴포넌트, 렌더링 최적화, 화면 전환, AI 디버깅을 반복하지 않고, React/Next.js의 2026년 6월 Server Actions FormData 및 forwarding 관련 수정에서 출발해 Server Actions를 직접 호출 가능한 POST 요청 계약으로 설계·검증하는 방법에 집중했다.
핵심 출처
로그인하면 이 글을 북마크하고, 나만 보는 한 줄 메모를 남길 수 있어요.
댓글 0
최신순 ▾혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)