Next.js Server Action과 프론트엔드 보안

Next.js Server Action의 기본 개념과 사용법
Next.js의 서버 액션(Server Action)은 Next.js 13.4 버전에서 App Router와 함께 정식으로 도입된 기능입니다. 이는 서버에서 실행되는 함수(Server-side function)를 React 컴포넌트 내에서 손쉽게 호출할 수 있도록 도와줍니다. use server
지시어가 있는 함수를 정의하면 클라이언트에서 간편하게 서버 로직을 호출할 수 있게 됩니다.
서버 액션을 활용하면 기존처럼 별도의 API 엔드포인트를 수동으로 관리하지 않아도 됩니다. 개발자는 서버 로직의 엔드포인트, 인터페이스 등을 신경 쓸 필요 없이, 마치 로컬 함수를 사용하는 것처럼 자연스럽게 서버 로직을 사용할 수 있습니다. 서버 액션이 클라이언트와 서버 간 통신을 추상화해주기 때문입니다.
서버 액션을 정의하는 방식으로는 React 컴포넌트 내부에 정의하는 방법과 별도의 파일에 독립적으로 정의하는 방식이 있습니다. 다음은 별도의 파일에 정의하는 방식입니다.
'use server';
export default async function createAction({ name, age, email }: { name: string; age: number; email: string; }) {
await connection.query(
'INSERT INTO users (name, age, email) VALUES (?, ?, ?)',
[name, age, email]
);
}
'use client';
import createAction from './create-action.ts';
export default function CreateForm() {
return (
<form action={createAction}>
<input type="text" name="name" />
<input type="number" name="age" />
<input type="email" name="email" />
</form>
)
}
서버 액션의 내부 동작 방식
위에서 예로 든 CreateForm
컴포넌트는 빌드 시 다음과 같은 형태로 변환됩니다. (참고: 이는 이해를 돕기 위한 코드이며, 실제 빌드된 코드와는 차이가 있을 수 있습니다.)
'use client';
import { startTransition } from 'react'
import { callServer } from 'next/client/app-call-server';
// 서버 액션 ID로 변환됨
const createAction = '$$action_1234567890';
export default function CreateForm() {
return (
<form
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = {
name: formData.get('name'),
age: Number(formData.get('age')),
email: formData.get('email')
};
// startTransition으로 UI 블로킹 방지
startTransition(() => {
callServer(createAction, [data]);
});
}}
>
<input type="text" name="name" />
<input type="number" name="age" />
<input type="email" name="email" />
</form>
)
}
즉, 서버 액션 함수를 로컬 함수처럼 import
해서 사용하던 부분이 사라지고, 대신 onSubmit
이벤트가 발생할 때 formData
가 처리되고 Next.js에서 제공하는 callServer
함수가 호출됩니다.
callServer
함수는 내부적으로 fetch
API를 사용해 HTTP 요청을 수행합니다. 요청 메서드는 POST
로 설정되어 있으며, 헤더에는 생성된 서버 액션 ID 등이 포함됩니다. 이를 통해 서버 측에서 어떤 액션이 호출되었는지를 식별할 수 있습니다. Next.js의 Repository에서 app-call-server.ts 파일을 참고하면 이러한 동작 방식을 확인할 수 있습니다.
서버 사이드에서는 callServer
로부터 요청을 받아 action-handler.ts 파일이 처리합니다. 이 핸들러는 CSRF 보호 등의 검증 과정을 거친 후, 요청 헤더를 분석하여 적절한 액션을 식별하고 로직을 수행한 뒤 응답을 반환하는 흐름으로 구성되어 있습니다.
서버 액션이 개발 환경에 가져온 변화
2010년대 이후 웹 개발 환경에서는 서버와 클라이언트 사이의 경계가 명확했고, 이를 넘나드는 작업은 종종 번거로웠습니다. Next.js의 서버 액션은 이러한 경계를 상당 부분 제거하여 개발자에게 매우 편리한 환경을 제공합니다.
기존에는 REST API 엔드포인트를 만들어 클라이언트가 HTTP 요청을 통해 호출하는 방식이 일반적이었습니다. 하지만 서버 액션은 로컬 함수를 호출하는 듯 직관적이고 간단한 형태로 사용할 수 있어 편의성이 뛰어납니다.
예전에는 "Full Stack"이 "Poor Stack"이라며 조롱받기도 했지만, AI 시대에 접어들면서 개발자 한 사람이 할 수 있는 일이 크게 늘어나 Full Stack의 위상이 높아졌습니다. 이제는 서버 액션과 같은 기술 덕분에 더욱 쉽고 효율적으로 풀스택 개발이 가능해졌습니다.
편리하지만 놓쳐서는 안 되는 서버 액션의 보안 문제
Next.js의 서버 액션은 마치 마법처럼 서버와 클라이언트가 연결된 듯한 느낌을 줍니다. 그러나 이 마법 같은 편의성 뒤에는 반드시 보안이라는 문제를 고려해야 합니다. 실제로는 HTTP 요청과 응답이 존재하며, 이 과정에서 다양한 보안적 고려 사항들이 존재합니다.
Next.js 공식 문서에서도 서버 액션의 보안에 대한 주의를 당부하고 있습니다. 이 점을 놓친 개발자는 의도하지 않게 중요한 로직을 무방비로 노출시킬 수 있으며, 이는 매우 치명적인 보안 이슈로 이어질 가능성이 높습니다.(안타깝게도 Security 섹션은 가장 마지막 섹션입니다.)
By default, when a Server Action is created and exported, it creates a public HTTP endpoint and should be treated with the same security assumptions and authorization checks. This means, even if a Server Action or utility function is not imported elsewhere in your code, it's still publicly accessible.
문서 초반 서버 액션의 마법에 홀려 마지막 Security 섹션까지 보지 못한 안타까운 개발자들은 아무런 잠금장치를 달지 않은 서버 함수를 온세상에 공개해버릴 수 있습니다. 인증, 인가가 필요한데 검증과정을 두지 않았다면 더 아찔합니다.
Next.js 서버 액션이 제공하는 기본적인 보안과 개발자의 역할
그렇다고 아주 무방비 상태는 아닙니다. 공식 문서에 따르면 Next.js는 서버 액션과 관련하여 다음과 같은 기본적인 보안 메커니즘을 제공합니다:
- Secure Action IDs: 서버와 클라이언트 간 통신의 유효성을 내부적으로 생성된 ID로 검증합니다.
- CSRF(Cross-Site Request Forgery) 방지: 서버 액션 호출 시 POST 요청만 허용하고 동일 출처 요청만 허용합니다.
- 클로저 변수 보호: 클라이언트로 전달되는 상태값을 암호화 및 서명하여 보호합니다.
- 코드 격리: 서버 전용 코드는 클라이언트 번들에 포함되지 않도록 처리합니다.
- 에러 메시지 보호: 프로덕션 환경에서는 민감한 상세 오류 메시지를 노출하지 않습니다.
- 미사용 액션 제거: 사용되지 않는 서버 액션을 자동으로 제거하여 공격 표면 최소화합니다.
하지만 개발자가 반드시 신경 써야 할 부분도 있습니다:
- 런타임 입력값 검증(Validation): 타입스크립트는 컴파일 시점 타입 확인만 가능하므로, 런타임 검증이 필수다. Zod와 같은 라이브러리를 통해 철저히 검증해야 합니다.
- 인증과 인가: 액션 내에서 사용자 권한을 매번 재확인해야 합니다.
- SSRF(Server Side Request Forgery) 방지: 사용자 입력 URL 등을 신뢰하지 말고, 반드시 화이트리스트 기반 접근만 허용해야 합니다.
- 서버 로직이면 필수적으로 고려해야 할 요소들
- 환경변수 보호: 데이터베이스 연결 정보 등 자산 정보를 사용할 떄 .env 파일에 저장하여 설정값 노출을 방지해야 합니다.
- Injection 방지:
- SQL Injection: SQL Injection을 방지하기 위해서는 ORM(Object-Relational Mapping) 같은 도구를 활용하여 데이터베이스 쿼리를 안전하게 작성해야 합니다.
- XSS 방지: sanitize-html 등의 라이브러리를 사용하여 입력값을 검증하고, 잠재적인 Cross-Site Scripting 공격에 대비해야 합니다.
- 보안 업데이트 및 패치 관리: 사용 중인 라이브러리와 프레임워크에 대한 정기적인 보안 점검과 업데이트를 통해 취약점을 사전에 방지해야 합니다.
- 로그와 모니터링: 중요한 서버 이벤트와 오류에 대한 로그를 체계적으로 관리하고 모니터링 시스템을 통해 실시간 보안 위협을 탐지하도록 설정해야 합니다.
런타임 입력값 검증 및 인증과 인가는 https://www.npmjs.com/package/next-safe-action, https://www.npmjs.com/package/zsa 같은 라이브러리 사용을 추천합니다. 라이브러리를 사용하면 추상화된 API를 선언적으로 작성할 수 있고, 제공되는 미들웨어를 통해 인증 및 인가를 손쉽게 처리할 수 있습니다. 서버뿐 아니라 클라이언트 측 서버 액션 관련 Hooks를 제공해 더욱 편리하게 활용 가능합니다.
더욱 중요해지는 프론트엔드 보안
개인적으로 프론트엔드 영역에서 서버 사이드 기능이 다시 부상하는 흐름을 긍정적으로 보고 있습니다.
과거 웹 개발 초기에는 서버와 클라이언트의 구분이 명확하지 않았습니다. 그러나 SPA(Single Page Application)와 모바일 앱 시대에 프론트엔드에서 서버의 존재감이 약해졌고, 클라이언트 측에서 많은 로직을 처리하게 되었습니다. 심지어 웹 프론트엔드를 곧 클라이언트로 이해하는 사람들도 생겨났습니다. 이는 모바일 앱 환경의 패턴이 웹 환경으로 넘어왔기 때문입니다.
하지만 웹은 모바일 앱과 근본적으로 다르며, 서버와 긴밀히 연동될 때 가장 강력한 기능을 발휘합니다. 클라이언트 중심 SPA의 한계를 극복하고자 SEO에 유리한 Next.js와 같은 SSR(Server Side Rendering)을 지원하는 프레임워크가 인기를 끌기 시작했습니다. 이제는 SSR을 넘어 서버 컴포넌트가 등장하는 등 프론트엔드에서도 서버 기능을 다시 적극적으로 활용하는 흐름이 나타나고 있습니다.
Next.js의 App Router와 서버 액션은 프론트엔드 개발자가 UI뿐 아니라 데이터 처리와 서버 로직까지 폭넓게 담당할 수 있는 기반을 만들어 주었습니다. 웬만큼 복잡한 웹 애플리케이션이 아닌 이상 DB가 존재하더라도 이제 독립적인 백엔드 애플리케이션 없이 개발이 가능합니다. 이는 개발자에게 효율적인 개발 경험을 제공하지만, 동시에 더 많은 복잡성과 보안에 대한 책임이 따릅니다.

AI 시대의 보안과 Next.js
AI가 주목받는 시대가 오면서 주요 프레임워크의 안정성과 보안은 더욱 중요해지고 있습니다. AI 역시 프론트엔드 서버 사이드 도구를 기반으로 작동할 가능성이 크므로, 프레임워크의 고수준의 추상화에 따른 보안적 실수를 할 수 있는 구조는 AI를 통해 더 큰 위험을 초래할 수 있습니다.
앞으로의 웹 환경은 AI 등 다양한 기술과의 융합으로 복잡성이 증가할 것입니다. 이 과정에서 Next.js와 같은 널리 사용하는 프론트엔드 프레임워크들은 보안과 안정성 면에서 계속 발전할 필요가 있습니다.
그리고 무엇보다 AI 기술과의 융합이 심화될수록 개발자 스스로의 보안 지식과 개념이 매우 중요하며, 늘 신경 써야 한다는 점을 잊지 말아야 합니다.