Next.js 서버 액션을 통한 간소화된 폼 처리 및 유효성 검사
Daniel Hayes
Full-Stack Engineer · Leapcell

소개
현대 웹 개발에서 사용자 상호 작용은 종종 폼을 통한 데이터 교환으로 귀결됩니다. 회원 가입, 피드백 제출 또는 기본 설정 업데이트 등 이 데이터의 무결성과 보안은 가장 중요합니다. 전통적으로 폼 제출을 처리하는 것은 사용자 경험을 위한 클라이언트 측 JavaScript와 데이터 영구 저장 및 유효성 검사를 위한 서버 측 논리 간의 섬세한 춤이었습니다. 이는 종종 아키텍처 복잡성과 중복된 유효성 검사 노력으로 이어졌습니다.
Next.js 서버 액션은 클라이언트 컴포넌트에서 직접 서버 측 작업을 위한 간소화된 접근 방식을 제공하여 매력적인 솔루션을 제시합니다. 이 새로운 패러다임은 클라이언트와 서버 로직 간의 경계를 모호하게 하여 개발자 경험을 크게 단순화합니다. 그러나 위대한 힘에는 위대한 책임이 따르며, 특히 데이터 유효성 검사에 관해서는 더욱 그렇습니다. 서버에서 수신된 데이터가 깔끔하고 올바르게 형식화되었으며 안전한지 확인하는 것은 오류 방지, 데이터 무결성 유지 및 악의적인 입력으로부터 보호하는 데 중요합니다. 이 글은 Next.js 서버 액션이 폼 처리를 혁신하는 방법과 특히 Zod와 같은 강력한 라이브러리를 사용하여 강력한 데이터 유효성 검사를 통합하는 방법을 자세히 살펴봄으로써 애플리케이션을 더 안정적이고 안전하게 만들 것입니다.
핵심 개념 및 구현
세부 사항에 들어가기 전에 관련 핵심 개념에 대한 기초적인 이해를 확립해 보겠습니다:
- Next.js 서버 액션: 클라이언트 컴포넌트에서 호출할 수 있는 서버에서 직접 실행되는 비동기 함수입니다. 별도의 API 계층을 구축할 필요 없이 서버 측 데이터 변경, 데이터베이스 호출 또는 복잡한 연산을 수행할 수 있습니다. 관련 로직을 한 곳에 모아 개발자 경험을 향상시킵니다.
- 폼 제출: 웹 폼에서 사용자 입력 데이터를 처리 서버로 전송하는 프로세스를 말합니다. Next.js에서는 네이티브 HTML
<form>
요소의action
속성을 서버 액션으로 지정하여 이를 효율적으로 처리할 수 있습니다. - 데이터 유효성 검사: 데이터가 특정 규칙, 형식 및 제약 조건을 준수하는지 확인하는 프로세스입니다. 이는 데이터 무결성, 보안 및 예상치 못한 오류 방지에 중요합니다.
- Zod: TypeScript 우선 스키마 선언 및 유효성 검사 라이브러리입니다. 개발자는 데이터 구조에 대한 스키마를 정의할 수 있으며, 이를 사용하여 들어오는 데이터를 유효성 검사하고, TypeScript 유형을 추론하고, 상세한 오류 메시지를 제공할 수 있습니다. 선언적 특성과 강력한 유형 추론은 서버 액션으로의 유효성 검사에 탁월한 선택입니다.
서버 액션을 사용한 폼 제출 처리
Next.js 서버 액션은 서버 측 함수를 폼의 action
속성에 직접 연결하여 폼 제출을 단순화합니다. 폼이 제출되면 데이터가 자동으로 직렬화되어 지정된 서버 액션으로 전송됩니다.
사용자 등록 폼의 간단한 예시를 통해 설명해 보겠습니다:
// app/register/page.tsx 'use client'; import { useFormStatus } from 'react-dom'; // React 18.2+의 새 훅 async function registerUser(formData: FormData) { 'use server'; // 이 함수를 서버 액션으로 표시 const name = formData.get('name'); const email = formData.get('email'); const password = formData.get('password'); // 실제 애플리케이션에서는 이를 데이터베이스에 저장합니다. console.log('Registering user:', { name, email, password }); // 지연 시뮬레이션 await new Promise(resolve => setTimeout(resolve, 1000)); return { success: true, message: 'User registered successfully!' }; } function SubmitButton() { const { pending } = useFormStatus(); return ( <button type="submit" disabled={pending}> {pending ? 'Registering...' : 'Register'} </button> ); } export default function RegisterPage() { return ( <form action={registerUser} className="space-y-4"> <div> <label htmlFor="name" className="block text-sm font-medium text-gray-700">Name</label> <input type="text" id="name" name="name" required className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" /> </div> <div> <label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label> <input type="email" id="email" name="email" required className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" /> </div> <div> <label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label> <input type="password" id="password" name="password" required className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" /> </div> <SubmitButton /> </form> ); }
이 예시에서:
registerUser
함수는'use server'
로 표시되어 서버 액션으로 만듭니다.form
요소의action
속성은registerUser
를 직접 가리킵니다.- Next.js에서 제공하는
formData
객체에는 모든 폼 필드가 포함됩니다. useFormStatus
훅(React DOM에서 제공)을 사용하면 클라이언트 컴포넌트에서 부모 폼의 제출 상태를 읽어 제출 버튼 비활성화와 같은 UI 피드백을 사용할 수 있습니다.
강력한 데이터 유효성 검사를 위한 Zod 통합
이제 Zod를 사용하여 registerUser
서버 액션을 강화하여 강력한 데이터 유효성 검사를 통합해 보겠습니다. 이렇게 하면 유효한 데이터만 애플리케이션 로직으로 진행되도록 보장할 수 있습니다.
먼저 Zod를 설치합니다:
npm install zod # 또는 yarn add zod # 또는 pnpm add zod
그런 다음 registerUser
서버 액션을 수정합니다:
// app/register/page.tsx 'use client'; import { useFormStatus } from 'react-dom'; import { z } from 'zod'; // Zod 가져오기 // 등록 데이터에 대한 스키마 정의 const registerSchema = z.object({ name: z.string().min(3, { message: 'Name must be at least 3 characters long.' }), email: z.string().email({ message: 'Invalid email address.' }), password: z.string().min(8, { message: 'Password must be at least 8 characters long.' }) .regex(/[A-Z]/, { message: 'Password must contain at least one uppercase letter.' }) .regex(/[a-z]/, { message: 'Password must contain at least one lowercase letter.' }) .regex(/[0-9]/, { message: 'Password must contain at least one number.' }) .regex(/[^A-Za-z0-9]/, { message: 'Password must contain at least one special character.' }), }); async function registerUser(prevState: { message: string; errors: Record<string, string[]> | undefined }, formData: FormData) { 'use server'; const rawFormData = Object.fromEntries(formData.entries()); // 스키마에 대한 폼 데이터 유효성 검사 const validationResult = registerSchema.safeParse(rawFormData); if (!validationResult.success) { // 유효성 검사에 실패하면 오류 반환 const fieldErrors = validationResult.error.flatten().fieldErrors; return { message: 'Validation failed. Please check your inputs.', errors: fieldErrors, }; } const { name, email, password } = validationResult.data; // 실제 애플리케이션에서는 이를 데이터베이스에 저장합니다. console.log('Registering user:', { name, email, password }); // 지연 시뮬레이션 await new Promise(resolve => setTimeout(resolve, 1000)); // 성공하면 오류를 재설정하고 성공 메시지를 반환합니다. return { success: true, message: 'User registered successfully!', errors: undefined }; } // ... (SubmitButton 컴포넌트는 동일하게 유지) import { useFormState } from 'react-dom'; // useFormState 가져오기 export default function RegisterPage() { // 초기 상태와 서버 액션으로 useFormState 초기화 const initialState = { message: '', errors: undefined }; const [state, formAction] = useFormState(registerUser, initialState); return ( <form action={formAction} className="space-y-4"> {/* useFormState의 formAction 사용 */} {state.message && <p className={`text-sm ${state.success ? 'text-green-600' : 'text-red-600'}`}>{state.message}</p>} <div> <label htmlFor="name" className="block text-sm font-medium text-gray-700">Name</label> <input type="text" id="name" name="name" required className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" /> {state.errors?.name && <p className="text-red-500 text-xs mt-1">{state.errors.name[0]}</p>} </div> <div> <label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label> <input type="email" id="email" name="email" required className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" /> {state.errors?.email && <p className="text-red-500 text-xs mt-1">{state.errors.email[0]}</p>} </div> <div> <label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label> <input type="password" id="password" name="password" required className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" /> {state.errors?.password && <p className="text-red-500 text-xs mt-1">{state.errors.password[0]}</p>} </div> <SubmitButton /> </form> ); }
주요 변경 사항 및 개선 사항:
- Zod 스키마 정의:
z.object
를 사용하여name
,email
,password
에 대한 예상 타입 및 유효성 검사 규칙을 지정하는registerSchema
를 정의했습니다. Zod는 다양한 유효성 검사(min, max, regex, email 등)를 위한 풍부한 API를 제공합니다. Object.fromEntries(formData.entries())
:FormData
객체를 Zod로 더 쉽게 유효성 검사할 수 있는 일반 JavaScript 객체로 변환합니다.registerSchema.safeParse(rawFormData)
: 이 메서드는 데이터를 유효성 검사하려고 시도합니다. 성공하면validationResult.success
가true
가 되고validationResult.data
에 구문 분석된 데이터가 포함됩니다. 실패하면validationResult.success
가false
가 되고validationResult.error
에 상세한 오류 정보가 포함됩니다.- 오류 처리 및
useFormState
:registerUser
서버 액션은 이제 첫 번째 인수로prevState
를 허용하고message
,errors
,success
를 포함하는 객체를 반환합니다. 이는useFormState
와의 통합에 중요합니다.useFormState
는 폼 제출에 걸쳐 상태를 관리할 수 있게 해주는 React 훅으로, 특히 서버 측 유효성 검사 오류 또는 성공 메시지를 표시하는 데 유용합니다. 서버 액션과 초기 상태를 인수로 받아 현재 상태와 폼의action
속성에 전달할 새로운formAction
을 반환합니다.- 이제
state.errors
(있는 경우)를 반복하고 해당 입력 필드 옆에 유효성 검사 메시지를 표시하여 사용자에게 즉각적이고 구체적인 피드백을 제공할 수 있습니다.
고급 시나리오 및 고려 사항
- 사용자 지정 오류 메시지: Zod를 사용하면 예시에서 볼 수 있듯이 각 유효성 검사 규칙에 대한 사용자 지정 오류 메시지를 설정할 수 있습니다.
- 변환: Zod 스키마는 유효성 검사 전에 문자열에서 공백 제거 또는 문자열을 숫자로 변환과 같은 변환을 정의할 수도 있습니다.
- 중첩 객체 및 배열: Zod는 복잡한 중첩 데이터 구조 및 배열을 쉽게 처리합니다.
- 조건부 유효성 검사:
zod.refine
또는zod.superRefine
을 사용하여 다른 필드에 의존하는 규칙을 정의할 수 있습니다. - 보안: 서버 측 유효성 검사는 절대 선택 사항이 아닙니다. 클라이언트 측 유효성 검사(HTML5 네이티브 유효성 검사 또는 JavaScript 사용)는 즉각적인 사용자 피드백을 제공하지만 우회될 수 있습니다. Zod를 사용한 서버 측 유효성 검사는 유효하고 올바르게 형식화된 데이터만 데이터베이스 또는 추가 처리로 들어가도록 보장합니다.
- 멱등성: 서버 액션이 반복 제출 시 반복되지 않아야 하는 작업(예: 고유 레코드 생성)을 수행하는 경우 멱등성을 갖도록 설계해야 한다는 점은 Zod와 직접 관련이 없습니다.
- 오류 경계: 유효성 검사를 넘어선 더 강력한 오류 처리를 위해 React 오류 경계를 사용하여 클라이언트 컴포넌트에서 처리되지 않은 오류를 잡는 것을 고려해 보세요. 서버 액션 내에서 처리되지 않은 오류는 네트워크 오류를 발생시키며 트리에 더 높은 오류 경계에서 잡힐 수 있습니다.
결론
Next.js 서버 액션과 Zod와 같은 강력한 유효성 검사 라이브러리의 조합은 폼 제출 처리를 위한 매우 강력하고 인체공학적인 솔루션을 제공합니다. 예상 데이터에 대한 명확한 스키마를 정의함으로써 데이터 무결성을 보장하고 애플리케이션 보안을 강화하며 사용자에게 구체적이고 유용한 피드백을 제공할 수 있습니다. 이 접근 방식은 일반적으로 전체 스택 폼 관리에 수반되는 복잡성을 크게 줄여 개발자가 API 계층 및 중복 유효성 검사 로직과 씨름하는 대신 기능 구축에 집중할 수 있도록 합니다. 서버 액션과 Zod를 채택하면 더 유지 관리하기 쉽고 안전하며 사용자 친화적인 Next.js 애플리케이션을 만들 수 있습니다. 이 통합된 전략은 전체 스택 개발 경험을 진정으로 단순화하여 폼 처리를 번거로움이 아닌 즐거움으로 만듭니다.