서버 액션으로 폼 처리
서버 액션을 활용하여 폼 데이터를 효율적으로 처리하는 방법을 실습합니다.
Next.js 기초 입문 17회차: 서버 액션으로 폼 처리
Next.js는 풀스택 웹 애플리케이션 개발을 위한 강력한 프레임워크입니다. 특히, 서버 액션(Server Actions)은 클라이언트 측에서 서버 측 코드를 직접 호출하여 폼 제출과 같은 데이터 변경 작업을 효율적으로 처리할 수 있도록 지원합니다. 본 회차에서는 서버 액션을 활용하여 폼 데이터를 안전하고 성능 효율적으로 처리하는 방법을 학습합니다.
📌 이번 회차 학습 목표
- Next.js 서버 액션의 기본 개념과 동작 원리를 이해합니다.
- HTML
<form>요소의action속성에 서버 액션을 연결하여 폼 데이터를 처리할 수 있습니다. - 서버 액션을 통해 데이터 변경(생성, 수정, 삭제) 로직을 구현할 수 있습니다.
useFormStatus훅을 활용하여 폼 제출 상태에 따라 UI를 업데이트하는 방법을 익힙니다.- 클라이언트 컴포넌트에서 서버 액션을 호출하는 방법을 이해합니다.
📝 개념 설명
서버 액션(Server Actions)이란?
서버 액션은 Next.js 13.4 버전부터 도입된 기능으로, 클라이언트 컴포넌트나 서버 컴포넌트에서 직접 서버 측 코드를 실행할 수 있게 해줍니다. 이를 통해 API 라우트를 별도로 생성하지 않고도 데이터베이스 작업이나 파일 시스템 접근과 같은 서버 전용 로직을 폼 제출과 같은 사용자 인터랙션에 직접 연결할 수 있습니다. 이는 네트워크 왕복 횟수를 줄여 성능을 향상시키고, 클라이언트-서버 간의 경계를 모호하게 하여 개발 경험을 개선합니다.
폼 액션(Form Actions)의 기본 원리
HTML의 <form> 요소는 action 속성을 가집니다. 일반적으로 이 속성에는 폼 데이터를 전송할 URL을 지정하고, 해당 URL에서 요청을 처리하는 API 엔드포인트가 필요했습니다. 그러나 Next.js 서버 액션을 사용하면, action 속성에 서버에서 실행될 함수를 직접 할당할 수 있습니다. 이 함수는 폼이 제출될 때 서버에서 실행되며, 폼 데이터는 자동으로 함수의 인자로 전달됩니다.
데이터 변경(Mutations)과 서버 액션
서버 액션은 주로 데이터 변경(Data Mutations) 작업에 사용됩니다. 예를 들어, 새로운 게시글을 생성하거나, 기존 게시글을 수정하거나, 사용자를 삭제하는 등의 작업에 적합합니다. 서버 액션은 기본적으로 서버에서 실행되므로, 민감한 로직이나 데이터베이스 접근 코드를 클라이언트에 노출하지 않고 안전하게 처리할 수 있습니다.
useFormStatus 훅 활용
폼이 제출되는 동안 사용자에게 로딩 상태를 보여주는 것은 좋은 사용자 경험을 제공하는 데 중요합니다. Next.js는 이를 위해 useFormStatus 훅을 제공합니다. 이 훅은 클라이언트 컴포넌트 내에서 호출되며, 현재 폼 제출 상태(예: pending)에 대한 정보를 제공합니다. 이를 활용하여 폼 제출 버튼을 비활성화하거나, 로딩 스피너를 표시하는 등의 UI 업데이트를 구현할 수 있습니다.
참고: useFormStatus 훅은 반드시 <form> 컴포넌트의 자식 컴포넌트 내에서 사용되어야 합니다. 또한, 클라이언트 컴포넌트에서만 사용할 수 있습니다.
💡 예제 & 실습
간단한 할 일 목록(Todo List) 애플리케이션을 만들어 서버 액션으로 할 일을 추가하는 기능을 구현해 보겠습니다.
1. 프로젝트 설정
먼저 Next.js 프로젝트를 생성합니다. 이미 프로젝트가 있다면 이 단계는 건너뛰세요.
npx create-next-app@latest nextjs-server-actions-todo --typescript --eslint --app
cd nextjs-server-actions-todo
2. 서버 액션 정의 (app/actions.ts)
프로젝트 루트에 actions.ts 파일을 생성하여 서버 액션을 정의합니다. 이 파일은 'use server' 지시어를 포함해야 합니다.
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
interface Todo {
id: number;
text: string;
completed: boolean;
}
// 임시 데이터베이스 역할을 할 배열
let todos: Todo[] = [];
let nextId = 1;
export async function addTodo(formData: FormData) {
const text = formData.get('todoText') as string;
if (!text || text.trim() === '') {
return { error: '할 일 내용을 입력해주세요.' };
}
const newTodo: Todo = {
id: nextId++,
text: text.trim(),
completed: false,
};
todos.push(newTodo);
console.log('새로운 할 일 추가됨:', newTodo);
// 캐시를 무효화하여 데이터가 업데이트되었음을 Next.js에 알립니다.
// 'next/cache'에서 revalidatePath를 임포트해야 합니다.
revalidatePath('/'); // 루트 경로의 캐시를 무효화합니다.
return { success: true, todo: newTodo };
}
'use server';: 이 파일의 모든 함수가 서버에서 실행됨을 명시합니다.addTodo(formData: FormData): 폼 제출 시 호출될 서버 액션 함수입니다. 폼 데이터는 자동으로FormData객체로 전달됩니다.formData.get('todoText'): 폼 필드의name속성을 사용하여 값을 가져옵니다.revalidatePath('/'): Next.js의 캐싱 기능을 활용하여, 데이터가 변경되었음을 알리고 해당 경로의 데이터를 다시 가져오도록 합니다. 이는 UI를 최신 상태로 유지하는 데 필수적입니다.
3. 폼 컴포넌트 생성 (app/page.tsx)
app/page.tsx 파일을 수정하여 할 일 목록과 폼을 렌더링합니다.
// app/page.tsx
import { addTodo } from './actions';
import { useFormStatus } from 'react-dom'; // useFormStatus는 'react-dom'에서 임포트됩니다.
import { revalidatePath } from 'next/cache'; // revalidatePath는 서버 전용이므로 클라이언트 컴포넌트에서는 직접 사용하지 않습니다.
interface Todo {
id: number;
text: string;
completed: boolean;
}
// 임시 데이터베이스 역할을 할 배열 (서버 컴포넌트에서 직접 접근)
let todos: Todo[] = []; // 실제 애플리케이션에서는 DB에서 가져옵니다.
let nextId = 1; // 서버 컴포넌트에서 초기화되므로, 실제 DB 사용 시에는 DB에서 ID를 관리합니다.
// 서버 컴포넌트에서 할 일 목록을 가져오는 함수 (임시)
async function getTodos(): Promise<Todo[]> {
// 실제 애플리케이션에서는 데이터베이스에서 데이터를 가져오는 비동기 로직이 들어갑니다.
// 여기서는 단순히 현재 메모리 내 todos 배열을 반환합니다.
// revalidatePath가 호출되면 이 함수가 다시 실행되어 최신 데이터를 가져옵니다.
return todos;
}
// 폼 제출 상태를 보여주는 클라이언트 컴포넌트
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type='submit' disabled={pending} className='p-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed'
>
{pending ? '추가 중...' : '할 일 추가'}
</button>
);
}
export default async function Home() {
const currentTodos = await getTodos(); // 서버 컴포넌트에서 데이터 가져오기
return (
<main class='flex min-h-screen flex-col items-center p-24'
>
<h1 class='text-4xl font-bold mb-8'
>Next.js 서버 액션 Todo 앱</h1>
<form action={addTodo} class='flex gap-4 mb-8'
>
<input
type='text'
name='todoText'
placeholder='새로운 할 일을 입력하세요'
class='p-2 border border-gray-300 rounded-md w-80 text-black'
required
/>
<SubmitButton />
</form>
<h2 class='text-2xl font-semibold mb-4'
>나의 할 일 목록</h2>
{
currentTodos.length === 0 ? (
<p>아직 할 일이 없습니다. 새로운 할 일을 추가해보세요!</p>
) : (
<ul class='list-disc pl-5'
>
{currentTodos.map((todo) => (
<li key={todo.id} class='text-lg mb-2'
>
{todo.text}
</li>
))}
</ul>
)
}
</main>
);
}
import { addTodo } from './actions';: 앞서 정의한 서버 액션을 임포트합니다.import { useFormStatus } from 'react-dom';: 폼 제출 상태를 관리하기 위한 훅을 임포트합니다.<form action={addTodo}>: 폼의action속성에 서버 액션 함수addTodo를 직접 할당합니다. 폼 제출 시 이 함수가 서버에서 실행됩니다.SubmitButton컴포넌트:'use client'지시어가 없지만,useFormStatus를 사용하므로 클라이언트 컴포넌트로 간주됩니다. 폼 제출이 진행 중일 때 버튼 텍스트를 변경하고 비활성화합니다.Home컴포넌트는async함수로 정의되어 서버에서getTodos()를 호출하여 데이터를 가져옵니다.revalidatePath에 의해 캐시가 무효화되면 이getTodos함수가 다시 실행되어 최신 할 일 목록을 가져와 UI를 업데이트합니다.
참고: 이 예제는 데이터를 메모리에 저장하므로, 서버를 재시작하면 데이터가 초기화됩니다. 실제 애플리케이션에서는 데이터베이스(예: PostgreSQL, MongoDB)를 사용하여 데이터를 영구적으로 저장해야 합니다.
4. 실행 및 확인
프로젝트를 실행하고 웹 브라우저에서 확인합니다.
npm run dev
브라우저에서 http://localhost:3000으로 접속하여 할 일을 추가해보세요. ‘할 일 추가’ 버튼을 누르면 잠시 ‘추가 중…’으로 바뀌면서 할 일이 목록에 추가되는 것을 확인할 수 있습니다.
⚠️ 자주 틀리는 것 / 주의사항
'use server'지시어 누락: 서버 액션 파일이나 함수 상단에'use server';를 반드시 명시해야 합니다. 이를 누락하면 서버 액션으로 인식되지 않습니다.useFormStatus사용 위치:useFormStatus훅은 반드시<form>의 자식 컴포넌트 내에서 호출되어야 합니다. 또한, 클라이언트 컴포넌트에서만 사용할 수 있습니다.- 데이터 재검증(Revalidation) 누락: 서버 액션으로 데이터를 변경한 후에는
revalidatePath또는revalidateTag를 사용하여 캐시를 무효화하고 UI를 업데이트해야 합니다. 이를 누락하면 변경된 데이터가 화면에 즉시 반영되지 않을 수 있습니다. - 클라이언트 컴포넌트에서 서버 액션 직접 호출: 클라이언트 컴포넌트에서 서버 액션을 직접 호출할 수 있지만, 이때는
startTransition과 같은 React의 동시성 기능을 활용하여 UI 블로킹을 방지하는 것이 좋습니다. (이 회차에서는 폼 액션에 집중) - 보안 고려사항: 서버 액션은 서버에서 실행되므로 민감한 작업을 수행할 수 있습니다. 항상 적절한 인증 및 권한 부여 로직을 추가하여 보안을 강화해야 합니다.
- 서버 액션은 클라이언트에서 서버 코드를 직접 호출하여 폼 처리 및 데이터 변경을 효율적으로 수행하는 Next.js의 기능입니다.
- HTML
<form>의action속성에 서버 액션 함수를 직접 할당하여 폼 데이터를 처리합니다. - 서버 액션은
'use server';지시어로 정의되며, 폼 데이터는FormData객체로 전달됩니다. useFormStatus훅은 폼 제출 상태(pending)를 제공하여 로딩 UI를 구현하는 데 사용됩니다.- 데이터 변경 후에는
revalidatePath를 사용하여 Next.js 캐시를 무효화하고 UI를 최신 상태로 업데이트해야 합니다.
🔗 다음 회차 예고
이번 회차에서는 서버 액션을 활용하여 폼 데이터를 처리하고 UI를 업데이트하는 방법을 학습했습니다. 다음 18회차에서는 서버 액션의 고급 활용법으로, 오류 처리 및 유효성 검사를 심화 학습합니다. 사용자 입력에 대한 유효성 검사를 서버 액션 내에서 어떻게 효율적으로 처리하고, 발생한 오류를 사용자에게 어떻게 피드백할 것인지에 대해 다룰 예정입니다.