이번 글에서는 Next.js 의 서버 액션 구현을 더 안전하고 효율적으로 만들어주는 Next Safe Action 라이브러리를 사용하여 프로젝트에 적용하는 방법을 소개합니다. 특히, 실제 프로젝트 코드 기반으로 설명하며, 기존 방식과의 차이점, 그리고 이를 통해 얻을 수 있는 이점을 상세히 다룹니다.
Next.js 에서는 클라이언트와 서버 간의 데이터 통신이 필수적입니다. 그러나 전통적인 방식은 다음과 같은 문제를 유발할 수 있습니다:
CSRF 공격 : 클라이언트에서 서버로 데이터를 보낼 때 요청의 출처를 확인하지 않으면 공격에 노출될 수 있습니다.
데이터 검증 부족 : 클라이언트 데이터가 적절히 검증되지 않으면 시스템 무결성을 해칠 수 있습니다.
타입 안전성이 부족한 경우, 클라이언트-서버 간 데이터 불일치로 에러가 발생할 가능성이 높습니다.
요청 검증, 에러 핸들링, 데이터 전처리를 모두 수작업으로 처리해야 하는 경우 코드가 복잡해질 수 있습니다.
Next Safe Action 은 Next.js에서 서버 액션을 보다 안전하고 편리하게 관리하도록 도와주는 라이브러리입니다. 주요 기능은 다음과 같습니다:
타입 안전성 : 서버와 클라이언트 간 데이터 흐름을 타입으로 보장.
CSRF 방지 : 기본적으로 CSRF 보호 로직 내장.
코드 간소화 : 반복적인 작업을 자동화하여 코드 가독성을 높임.
npm install next-safe-action
lib/safe-action.ts
파일을 생성하고, 다음과 같이 설정합니다:
import {
createSafeActionClient,
DEFAULT_SERVER_ERROR_MESSAGE
} from 'next-safe-action' ;
export class ActionError extends Error {}
export const actionClient = createSafeActionClient ({
handleServerError ( e ) {
console. error ( 'Failed to execute action:' , e.message);
if (e instanceof ActionError ) {
return e.message;
}
return DEFAULT_SERVER_ERROR_MESSAGE ;
}
});
이 설정은 서버 에러 핸들링을 간소화하고, 사용자 정의 에러를 처리할 수 있도록 도와줍니다.
app/actions.ts
에 Contact Form 처리를 위한 액션을 정의합니다:
'use server' ;
import { Resend } from 'resend' ;
import { ContactEmail } from '@/components/emails/contact-template' ;
import { validateTurnstileToken } from '@/lib/turnstile' ;
import { actionClient, ActionError } from '@/lib/safe-action' ;
import { ContactActionSchema } from '@/lib/validators' ;
const EMAIL_FROM = process.env. EMAIL_FROM ;
const EMAIL_TO = process.env. EMAIL_TO ;
export const contactSubmit = actionClient
. use ( async ({ next , clientInput }) => {
const data = clientInput as { token ?: string };
if ( ! data?.token) {
throw new ActionError ( 'CAPTCHA 검증에 실패했습니다.' );
}
const res = await validateTurnstileToken (data.token);
if ( ! res.success) {
throw new ActionError ( 'CAPTCHA 검증에 실패했습니다.' );
}
return next ();
})
. schema (ContactActionSchema)
. action ( async ({ parsedInput : { name , email , message } }) => {
const resend = new Resend (process.env. RESEND_API_KEY );
if ( ! EMAIL_FROM || ! EMAIL_TO ) {
throw new Error ( 'Email 설정이 잘못되었습니다.' );
}
const { error } = await resend.emails. send ({
from: EMAIL_FROM ,
to: EMAIL_TO ,
subject: `${ name }의 문의` ,
react: ContactEmail ({ name, email, message })
});
if (error) throw new Error ( JSON . stringify (error));
return { success: '연락 주셔서 감사합니다!' };
});
components/ContactForm.tsx
에서 서버 액션을 호출합니다:
'use client' ;
// <import 생략>
const ContactForm = () => {
const form = useForm ({
resolver: zodResolver (ContactFormSchema),
defaultValues: {
name: '' ,
email: '' ,
message: ''
}
});
const { execute , result , status } = useAction (contactSubmit);
const onSubmit = async ( values : any ) => {
execute (values);
};
return (
< form onSubmit = {form. handleSubmit (onSubmit)}>
< input { ... form. register ( 'name' )} placeholder = "Name" />
< input { ... form. register ( 'email' )} placeholder = "Email" />
< textarea { ... form. register ( 'message' )} placeholder = "Message" />
< button type = "submit" disabled = {status === 'executing' }>
Submit
</ button >
</ form >
);
};
export default ContactForm;
fetch ( '/api/contact' , {
method: 'POST' ,
body: JSON . stringify (data),
headers: { 'Content-Type' : 'application/json' }
})
. then (( res ) => res. json ())
. then (( data ) => console. log (data));
axios
. post ( '/api/contact' , data)
. then (( res ) => console. log (res.data))
. catch (( err ) => console. error (err));
단점 :
타입 안정성이 부족.
CSRF 공격에 취약.
에러 핸들링이 복잡.
const { execute , result } = useAction (contactSubmit);
execute ({ name: 'John' , email: 'john@example.com' , message: 'Hello!' });
장점:
타입 안전성.
에러 핸들링 내장.
코드 간결화.
Next Safe Action은 보안과 생산성을 동시에 만족시키는 Next.js 프로젝트의 필수 도구입니다.
특히 서버 액션을 자주 사용하는 프로젝트에서 코드 품질을 크게 향상시킬 수 있습니다.