폼 제출, 댓글 저장, 설정 변경처럼 “서버에 한 번 반영하고 화면을 갱신”하는 작업은 서버 액션이 기본 후보다. 별도 공개 엔드포인트를 하나씩 파지 않아도, 서버 전용 함수에 서버 지시문만 붙이면 화면에서 그 함수를 호출할 수 있다.
다만 “편하다”와 “운영에 안전하다”는 다른 문제다. 입력이 그대로 오면 검증이 빠지고, 쿠키만 믿으면 권한 우회가 생기고, 캐시를 안 깨면 목록이 옛 데이터를 보여 준다. 서버 액션을 둘 위치, 일반 라우트와 갈라지는 기준, 폼·캐시 무효화·보안 점검을 한 흐름으로 잡는다. 넥스트 앱 라우터와 리액트 서버 컴포넌트를 이미 쓰는 팀을 기준으로 한다.
서버 액션이 하는 일 | 서버 함수를 화면에서 호출
서버 액션은 서버에서만 실행되는 비동기 함수다. 파일 상단 또는 함수 본문에 서버 전용 지시문을 선언하면, 넥스트가 그 함수를 안전하게 호출할 수 있는 통로로 연결한다. 클라이언트 컴포넌트의 이벤트 처리나 폼의 액션 속성에 그 함수를 넘기면 된다.
한 줄로 쓰면 이렇다. 브라우저가 보낸 입력을 받아 데이터베이스·파일 저장소·외부 서비스를 건드리고, 필요하면 캐시를 무효화한 뒤 결과를 돌려준다. 읽기만 있는 화면은 서버 컴포넌트에서 직접 조회하는 편이 맞고, “쓰기가 있는 상호작용”에 액션을 붙인다.
서버 액션이 모든 백엔드 진입점을 대체하는 만능 칼은 아니다. 같은 앱 화면 안의 폼·버튼이면 액션이 짧고, 외부 웹훅·공개 인터페이스·모바일 앱 공용이면 라우트 핸들러가 맞다.
상황
서버 액션
라우트 핸들러
같은 넥스트 앱 안의 폼 제출
우선 추천
가능하나 준비 코드가 늘어남
결제·슬랙·깃허브 웹훅
비권장
필수에 가깝다
외부 클라이언트가 쓰는 공개 제이슨 인터페이스
부적합
적합
캐시 태그 무효화와 화면 갱신을 한 묶음으로
유리
가능, 호출 주체를 직접 설계
파일 다운로드·긴 스트림 응답
제한적
핸들러가 편함
실무 규칙은 단순하다. 화면이 유일한 호출자면 서버 액션, 계약된 에이치티티피 표면이 필요하면 라우트 핸들러. 둘을 섞어도 되지만, 같은 변경 로직은 공유 서버 모듈로 빼고 진입점만 둘로 두는 편이 중복을 줄인다. 팀 온보딩 문서에 “관리자 콘솔 폼은 액션, 파트너 연동은 핸들러” 한 줄만 있어도 리뷰 논쟁이 줄어든다.
서버 액션은 같은 앱 화면의 쓰기 요청을 서버 함수로 연결한다
폼에 붙이는 기본 패턴 | 액션과 점진적 향상
가장 읽기 쉬운 형태는 서버 파일에 액션을 두고 폼의 액션 속성에 넘기는 것이다. 자바스크립트가 꺼져 있어도 폼은 동작하는 쪽이 기본값에 가깝다. 아래는 제목을 받아 저장한 뒤 목록 경로 캐시를 깨고 목록으로 보내는 최소 예시다.
서버 액션 — 글 생성 (발췌)
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = String(formData.get('title') || '').trim()
if (title.length < 2) {
return { ok: false, message: '제목을 2자 이상 입력하세요.' }
}
// await db.post.create({ data: { title } })
revalidatePath('/posts')
redirect('/posts')
}
파일 업로드는 폼에서 받은 값이 실제 파일 객체인지 확인하고, 크기·형식을 서버에서 다시 본다. 브라우저에서 막았더라도 프록시·봇 요청은 그대로 들어온다. 비밀키·데이터베이스 주소는 액션 파일에 직접 쓰지 말고 환경 변수만 읽는다. 시크릿 운영 점검은 시크릿·환경변수 프로덕션 체크리스트와 같은 목록을 팀에 두는 편이 안전하다.
반환 형태는 팀 규칙을 하나로 고정한다. 예: 성공 여부, 사용자 메시지, 필드별 오류. 어떤 액션은 다른 주소로 보내고, 어떤 액션은 객체만 돌려주면 화면 분기가 폭발한다. “항상 객체 반환, 이동이 필요하면 액션 이름에 이동 전용 접미사를 둔다”처럼 규칙을 짧게 적어 두면 신규 기여자가 덜 헤맨다.
숫자·날짜·체크박스는 문자로 들어오는 경우가 많다. 스키마에서 변환·기본값을 처리하고, 빈 문자열과 “값 없음”을 구분해 두면 할인율 0과 미입력이 섞이지 않는다. 국내 서비스라면 휴대폰 번호 하이픈, 사업자번호 자리수처럼 현지 형식을 서버 규칙에 포함하는 것이 맞다.
입력 검증은 화면 편의와 서버 방어를 분리해 설계한다
캐시 무효화 | 경로 갱신과 태그 갱신
쓰기에 성공했는데 목록이 안 바뀌면 거의 항상 캐시 문제다. 서버 컴포넌트 페이지·원격 요청 캐시·경로 단위 캐시가 남아 있으면 옛 화면이 나간다. 액션 끝에서 무효화를 명시한다.
태그는 읽기 쪽에서 미리 붙여 둬야 의미가 있다. 원격 요청 옵션에 같은 문자열 태그를 넣거나, 프레임워크가 제공하는 캐시 도우미에 동일 이름을 쓴다. 경로만 무효화하면 연관 카드·홈 구역이 남을 수 있어, 목록·상세·집계에 쓴 태그를 액션마다 표로 적어두면 사고가 줄어든다.
실무에서 자주 나는 실수는 “상세만 깨고 홈 최신 글 영역은 그대로”다. 글 수정 액션이 성공해도 메인에 옛 제목이 남으면 사용자 신뢰가 떨어진다. 쓰기 함수 옆에 “이 액션이 깨야 할 화면 목록”을 주석이나 작은 표로 고정해 두면 리뷰에서 바로 보인다.
배포·주소 정책 쪽은 끝 슬래시와 공유 이미지, 사이트맵 동기화는 빌드 전 단계에서 사이트맵·구독 피드 갱신과 맞춰 두면 “본문은 새 글인데 피드·공유 이미지는 옛것” 류의 운영 사고를 같이 줄일 수 있다. 캐시 무효화는 앱 안 데이터만이 아니라, 검색·구독 산출물 파이프라인과도 맞물린다.
인증·권한·보안 | 액션 안에서 다시 확인
서버 액션 주소가 난수처럼 보여도 비밀 문이 아니다. 호출 가능 여부와 권한은 매 요청마다 서버에서 검사한다. 페이지를 서버 컴포넌트로 막아 두었어도, 액션은 별도 진입점이다.
넥스트는 서버 액션에 대해 출처(오리진) 검사 등 기본 방어를 제공한다. 공식 문서의 보안 권고를 쓰는 버전 기준으로 읽고, 커스텀 도메인·프록시·서브도메인 구성에서는 허용 출처가 맞는지 배포 환경에서 확인한다. “프레임워크가 다 해준다”고 가정한 채 공개 폼에 위험한 부수효과를 넣지 않는다.
외부에 노출되면 안 되는 키는 액션 반환값에 넣지 않는다. 에러 메시지도 스택·질의문을 그대로 보내지 말고, 사용자용 짧은 문장과 서버 로그용 상세를 분리한다. 국내 서비스에서 본인 인증·결제·개인정보가 섞이면 액션 하나로 몰지 말고, 검증된 결제 모듈과 웹훅 검증 쪽 역할 분리를 먼저 그린다.
체크 한 줄: 페이지 가드와 액션 가드는 다르다. 로그인 필수 페이지에서만 버튼을 보여 줘도, 액션 함수 자체에 세션·소유권 검사가 없으면 직접 호출에 열린다.
에러·로딩 화면 | 실패를 사용자 문장으로
성공만 가정한 액션은 운영 첫 주에 깨진다. 네트워크 지연, 중복 키 제약 위반, 외부 결제 연동 시간 초과를 구분해 돌려준다.
검증 실패: 필드 단위 메시지, 구조화된 객체로 반환
권한 실패: 모호한 “오류”보다 “권한이 없습니다” (정보 노출 범위는 정책에 맞게)
일시 장애: 다시 시도 가능 여부 표시, 버튼 연타 방지
폼 제출 중 상태 값으로 버튼을 비활성화하면 중복 주문이 줄어든다. 낙관적 화면 갱신은 빨라 보이지만, 실패 시 되돌릴 경로를 같이 설계해야 한다. 서버 액션과 낙관적 화면을 같이 쓸 때는 “실패 시 목록을 다시 불러오기” 같은 안전한 기본값을 두는 편이 낫다.
로그에는 요청 식별자·사용자 식별자(해시 가능)·액션 이름을 남겨 장애 추적에 쓴다. 카드 결제처럼 규제가 걸리는 흐름은 액션 하나로 몰지 말고, 검증된 결제 모듈·웹훅 검증과 역할을 나눈다. 사용자에게 보이는 문장은 짧고 행동 가능하게, 운영자가 보는 로그는 원인 추적 가능하게 나누는 것이 핵심 습관이다.
한글 메시지는 존댓말 톤을 팀 가이드에 맞춰 통일한다. 어떤 화면은 반말, 어떤 화면은 과도한 사과 문구면 브랜드 신뢰가 흔들린다. “다시 시도해 주세요”, “입력값을 확인해 주세요”처럼 짧고 동일한 말투가 지원 문의도 줄인다.
진행 중 상태와 필드 오류를 구분해 보여 주면 재제출 비용이 줄어든다
쓰지 말아야 할 때 | 대안 세 가지
서버 액션을 모든 백엔드 입구로 밀면 관측·계약·버전 관리가 어려워진다. 아래면 다른 도구를 고른다.
외부 시스템이 호출한다 — 웹훅, 파트너 연동, 모바일 네이티브 클라이언트. 메서드·상태 코드·문서화된 계약이 있는 라우트 핸들러.
오래 걸리는 작업 — 영상 변환, 대량 메일. 액션은 “대기열에 넣기”만 하고 실제 처리는 워커.
세밀한 캐시·시디엔 제어가 핵심 — 공개 콘텐츠 인터페이스는 캐시 헤더와 경로 단위 설계가 더 명확할 수 있다.
같은 저장소 안에서도 “관리자 콘솔 폼은 서버 액션, 공개 파트너 연동은 라우트 핸들러”처럼 경계를 문서 한 줄로 박아 두면 온보딩이 빨라진다. 이미 페이지 라우터를 쓰는 옛 코드는 무리한 일괄 이관보다, 신규 쓰기 경로만 앱 라우터와 액션으로 여는 단계 전략이 현실적이다.
팀 규모가 커질수록 “액션 폴더 폭증”이 온다. 도메인별 폴더(주문·회원·콘텐츠)로 나누고, 공통 권한 검사·공통 오류 형식은 한 모듈로 모은다. 폴더만 늘리고 규칙이 없으면 결국 숨은 공개 인터페이스가 된다.
배포 전 체크리스트 | 액션 운영 통과 조건
코드 리뷰에서 아래를 통과시킨 뒤 병합한다. 로컬에서 폼 한 번 성공했다고 끝이 아니다.
항목
확인
서버 검증
스키마·길이·타입, 화면 검증과 별개
인증·소유권
액션 내부에서 세션·리소스 검사
캐시
경로·태그 무효화 목록이 읽기 측과 일치
반환 계약
성공·실패 형태 통일, 비밀값 미포함
중복 제출
진행 중 비활성 또는 멱등 키
관측
오류 로그·알림, 치명 액션 감사 기록
경계
웹훅·공개 연동은 핸들러로 분리했는지
스모크 목록에 로그아웃 상태 호출, 다른 사용자 식별자로 위조한 삭제, 태그 누락으로 홈이 안 바뀌는 경우를 넣는다. 스테이징에서 실제 세션 쿠키로 한 바퀴 돌린 뒤에야 운영 반영 후보가 된다. 자동 테스트가 있다면 “권한 없는 호출이 거절되는지”, “성공 후 목록 쿼리가 새 값을 보는지” 두 가지만 있어도 회귀가 크게 줄어든다.
버전·함수 이름은 배포 중인 넥스트·리액트 마이너에 따라 다를 수 있다. 위 문서를 기준으로 프로젝트 변경 기록을 대조한다. 보안·캐시 동작은 마이너 업그레이드 노트에 자주 등장하니, 액션을 많이 쓰는 팀은 업그레이드 체크리스트에 “액션 출처 검사·캐시 기본값” 항목을 넣어 두는 것이 좋다.
자주 묻는 질문
서버 액션과 라우트 핸들러 중 무엇을 먼저 쓰나요?
같은 넥스트 앱 화면의 폼·버튼 쓰기면 서버 액션이 기본이다. 외부 웹훅, 공개 제이슨, 다른 클라이언트가 같은 계약을 써야 하면 라우트 핸들러를 쓴다.
페이지에서 로그인을 막았는데 액션에 또 인증이 필요한가요?
필요하다. 페이지를 그릴 때의 가드와 액션 진입은 별개다. 세션·소유권 검사는 액션 함수 안에서 매번 수행한다.
저장 후 목록이 안 바뀌면 어디를 보나요?
경로 갱신·태그 갱신이 빠졌거나, 읽기 쪽에 붙인 태그와 문자열이 다른 경우가 많다. 홈·목록·상세에 쓴 캐시 이름을 액션 성공 분기와 표로 맞춰 본다.
화면에서 스키마 검증만 하면 충분한가요?
충분하지 않다. 화면 검증은 입력 실수를 줄이는 편의 기능이다. 우회 요청은 서버 액션의 스키마·권한 검사로 막는다.
서버 액션으로 파일 업로드가 가능한가요?
가능하다. 폼으로 받은 파일을 서버에서 받아 저장소에 올린다. 크기·형식·위험한 확장자 검사를 서버에 두고, 아주 큰 파일은 직접 업로드 주소 패턴을 검토한다.
오류 메시지를 사용자에게 어떻게 보여 주나요?
액션이 성공 여부·메시지·필드 오류 객체를 반환하고, 폼 컴포넌트가 필드 아래 한국어 문장으로 그린다. 스택과 질의문은 서버 로그만 남긴다.