Next.js 기초 입문 16주차: 서버 액션 (Server Actions) 소개 및 활용

📚
제16주차 학습 목표

서버 액션 (Server Actions) 소개

서버에서 직접 데이터를 처리하는 서버 액션의 개념을 이해합니다.

Next.js 기초 입문 16회차: 서버 액션 (Server Actions) 소개

Next.js는 React 프레임워크를 기반으로 하며, 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG) 등 다양한 렌더링 방식을 지원하여 현대적인 웹 애플리케이션 개발에 최적화된 도구입니다. 이번 16회차에서는 Next.js 13 버전부터 도입된 서버 액션(Server Actions)이라는 혁신적인 기능에 대해 학습합니다. 서버 액션은 클라이언트 측 JavaScript 없이 서버에서 직접 데이터를 처리할 수 있게 하여, 웹 애플리케이션의 성능과 개발 편의성을 크게 향상시킵니다.

📌 이번 회차 학습 목표

  • 서버 액션(Server Actions)의 개념과 필요성을 설명할 수 있습니다.
  • 클라이언트 컴포넌트와 서버 컴포넌트에서 서버 액션을 정의하고 호출하는 방법을 이해합니다.
  • HTML 폼(Form)과 연동하여 서버 액션을 활용한 데이터 제출 과정을 구현할 수 있습니다.
  • 서버 액션 사용 시 발생할 수 있는 보안 및 에러 처리 방안을 고려할 수 있습니다.
  • 서버 액션을 통해 웹 애플리케이션의 데이터 처리 로직을 최적화하는 방법을 파악합니다.

📝 개념 설명

1. 서버 액션(Server Actions)이란?

서버 액션(Server Actions)은 Next.js 13.4 버전부터 정식으로 도입된 기능으로, 클라이언트 컴포넌트에서 직접 서버 코드를 호출하여 서버에서 데이터를 처리할 수 있도록 하는 메커니즘입니다. 이는 기존의 API 라우트(API Routes)를 대체하거나 보완하는 역할을 하며, 특히 폼 제출과 같은 데이터 변경 작업에서 강력한 이점을 제공합니다.

 

전통적인 웹 애플리케이션에서 클라이언트가 서버에 데이터를 전송하려면, 클라이언트 측 JavaScript를 사용하여 API 엔드포인트에 HTTP 요청(예: POST 요청)을 보내야 했습니다. 이 과정은 클라이언트와 서버 간의 통신을 위한 추가적인 코드 작성과 상태 관리를 필요로 했습니다. 서버 액션은 이러한 복잡성을 줄이고, 마치 클라이언트에서 함수를 호출하듯이 서버의 특정 로직을 실행할 수 있게 합니다.

2. 서버 액션의 주요 특징 및 장점

  • 간결한 데이터 처리: 클라이언트 측 JavaScript 없이 HTML 폼의 action 속성에 직접 서버 액션 함수를 연결할 수 있어 코드가 간결해집니다.
  • 성능 최적화: 클라이언트와 서버 간의 네트워크 왕복(roundtrip) 횟수를 줄여 데이터 전송 및 처리 속도를 향상시킬 수 있습니다.
  • 타입 안정성: TypeScript와 함께 사용 시, 클라이언트에서 서버 액션을 호출할 때 타입 안정성을 확보할 수 있습니다.
  • 보안 강화: 서버에서 직접 실행되므로 민감한 로직이나 데이터베이스 접근 코드를 클라이언트에 노출하지 않고 안전하게 처리할 수 있습니다.
  • 쉬운 에러 처리 및 재검증: 서버 액션 내부에서 발생한 에러를 클라이언트에서 쉽게 처리할 수 있으며, 데이터 재검증(revalidation)을 통해 UI를 자동으로 업데이트할 수 있습니다.
📌 핵심 포인트: 서버 액션의 장점
🚀
성능 향상
네트워크 왕복 감소, 클라이언트 JS 번들 크기 축소.
🔒
보안 강화
민감한 로직 서버에서 실행, 클라이언트 노출 방지.
개발 편의성
API 라우트 없이 직접 서버 코드 호출, 코드 간결화.
🔄
데이터 재검증
데이터 변경 후 UI 자동 업데이트 용이.

3. 서버 액션 정의 및 사용법

서버 액션은 'use server' 지시어를 파일의 최상단 또는 함수 내부에 선언하여 정의합니다. 이 지시어는 해당 파일 또는 함수가 서버에서만 실행되어야 함을 Next.js에게 알립니다.

3.1. 파일 최상단에 'use server' 선언

파일의 최상단에 'use server'를 선언하면 해당 파일 내의 모든 내보내기(export) 함수가 서버 액션으로 간주됩니다. 이는 주로 여러 서버 액션을 한 파일에 모아두고 싶을 때 유용합니다.

// app/actions.ts
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title');
  const content = formData.get('content');
  // 데이터베이스에 게시물 저장 로직 (예시)
  console.log('게시물 생성:', { title, content });
  // ... 데이터베이스 작업 ...

  // 데이터 재검증 (Next.js 캐시 무효화)
  // revalidatePath('/'); // 또는 revalidateTag('posts');

  return { success: true, message: '게시물이 성공적으로 생성되었습니다.' };
}

export async function deletePost(postId: string) {
  // ... 게시물 삭제 로직 ...
  console.log('게시물 삭제:', postId);
  return { success: true, message: '게시물이 삭제되었습니다.' };
}
3.2. 함수 내부에 'use server' 선언

특정 함수만 서버 액션으로 만들고 싶을 때는 함수 본문 내부에 'use server'를 선언할 수 있습니다. 이는 주로 서버 컴포넌트 내에서 특정 폼 액션만을 위한 서버 액션을 정의할 때 사용됩니다.

// app/page.tsx (서버 컴포넌트)
export default function HomePage() {
  async function submitForm(formData: FormData) {
    'use server';
    const name = formData.get('name');
    console.log('폼 제출:', name);
    // ... 데이터 처리 로직 ...
  }

  return (
    <form action={submitForm}>
      <input type='text' name='name' />
      <button type='submit'>제출</button>
    </form>
  );
}

4. 폼(Form)과 서버 액션 연동

서버 액션의 가장 일반적인 사용 사례는 HTML <form> 요소와 연동하여 데이터를 제출하는 것입니다. <form>action 속성에 서버 액션 함수를 직접 할당할 수 있습니다.

// app/components/PostForm.tsx (클라이언트 컴포넌트)
'use client';

import { createPost } from '@/app/actions'; // 서버 액션 임포트
import { useRef } from 'react';

export default function PostForm() {
  const formRef = useRef<HTMLFormElement>(null);

  async function handleSubmit(formData: FormData) {
    const result = await createPost(formData);
    if (result.success) {
      alert(result.message);
      formRef.current?.reset(); // 폼 초기화
    } else {
      alert('오류 발생: ' + result.message);
    }
  }

  return (
    <form ref={formRef} action={handleSubmit}>
      <label htmlFor='title'>제목:</label>
      <input type='text' id='title' name='title' required /><br /><br />

      <label htmlFor='content'>내용:</label>
      <textarea id='content' name='content' rows={5} required></textarea><br /><br />

      <button type='submit'>게시물 생성</button>
    </form>
  );
}

위 예시에서 createPost는 서버에서 실행되는 함수이지만, 클라이언트 컴포넌트인 PostForm에서 마치 일반 함수처럼 호출됩니다. Next.js는 빌드 시 이 호출을 서버 액션으로 변환하여 처리합니다.

🔄 서버 액션 데이터 처리 흐름
1. 클라이언트 컴포넌트에서 폼 제출
2. Next.js가 서버 액션 호출 감지
3. 서버에서 서버 액션 함수 실행
4. 데이터 처리 (DB 저장 등)
5. 결과 클라이언트로 반환
6. 클라이언트에서 UI 업데이트

💡 예제 & 실습

간단한 할 일 목록(Todo List) 애플리케이션을 만들어 서버 액션을 통해 할 일을 추가하고 삭제하는 기능을 구현해봅시다.

1. 프로젝트 설정

Next.js 프로젝트가 없다면 새로 생성합니다.

npx create-next-app@latest my-todo-app --typescript --app
cd my-todo-app

2. 서버 액션 파일 생성 (app/actions.ts)

할 일 추가 및 삭제 로직을 처리할 서버 액션 파일을 생성합니다. 여기서는 간단히 메모리 내 배열을 사용하지만, 실제 애플리케이션에서는 데이터베이스와 연동될 것입니다.

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

interface Todo {
  id: string;
  text: string;
}

let todos: Todo[] = []; // 임시 데이터 저장소

export async function addTodo(formData: FormData) {
  const text = formData.get('todoText') as string;

  if (!text || text.trim() === '') {
    return { success: false, message: '할 일 내용을 입력해주세요.' };
  }

  const newTodo: Todo = {
    id: String(Date.now()), // 고유 ID 생성
    text: text.trim(),
  };
  todos.push(newTodo);
  console.log('할 일 추가됨:', newTodo);

  revalidatePath('/'); // 루트 경로의 캐시를 무효화하여 UI 업데이트 유도
  return { success: true, message: '할 일이 성공적으로 추가되었습니다.' };
}

export async function deleteTodo(todoId: string) {
  const initialLength = todos.length;
  todos = todos.filter(todo => todo.id !== todoId);

  if (todos.length === initialLength) {
    return { success: false, message: '해당 ID의 할 일을 찾을 수 없습니다.' };
  }

  console.log('할 일 삭제됨:', todoId);
  revalidatePath('/'); // 루트 경로의 캐시를 무효화하여 UI 업데이트 유도
  return { success: true, message: '할 일이 성공적으로 삭제되었습니다.' };
}

export async function getTodos() {
  // 서버 컴포넌트에서 직접 호출될 때 사용
  return todos;
}

3. 메인 페이지 컴포넌트 수정 (app/page.tsx)

할 일 목록을 표시하고, 할 일 추가 폼을 포함하는 메인 페이지를 구성합니다. 이 페이지는 서버 컴포넌트로 유지하여 초기 데이터를 서버에서 가져오고, 클라이언트 컴포넌트에서 서버 액션을 호출하도록 합니다.

// app/page.tsx
import { addTodo, deleteTodo, getTodos } from './actions';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';

export default async function HomePage() {
  const currentTodos = await getTodos(); // 서버에서 할 일 목록 가져오기

  return (
    <div style={{ maxWidth: '600px', margin: '50px auto', padding: '20px', border: '1px solid #eee', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
      <h1 style={{ textAlign: 'center', color: '#333' }}>나의 할 일 목록</h1>
      <TodoForm />
      <TodoList todos={currentTodos} />
    </div>
  );
}

4. 할 일 추가 폼 컴포넌트 생성 (app/components/TodoForm.tsx)

할 일을 입력하고 제출하는 클라이언트 컴포넌트를 생성합니다. 여기서는 useRef를 사용하여 폼을 초기화하고, addTodo 서버 액션을 호출합니다.

// app/components/TodoForm.tsx
'use client';

import { useRef } from 'react';
import { addTodo } from '@/app/actions';

export default function TodoForm() {
  const formRef = useRef<HTMLFormElement>(null);

  async function handleAddTodo(formData: FormData) {
    const result = await addTodo(formData);
    if (result.success) {
      alert(result.message);
      formRef.current?.reset(); // 폼 초기화
    } else {
      alert('오류: ' + result.message);
    }
  }

  return (
    <form ref={formRef} action={handleAddTodo} style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
      <input
        type='text'
        name='todoText'
        placeholder='새로운 할 일을 추가하세요'
        required
        style={{ flexGrow: 1, padding: '10px', border: '1px solid #ddd', borderRadius: '4px' }}
      />
      <button
        type='submit'
        style={{ padding: '10px 15px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
      >
        추가
      </button>
    </form>
  );
}

5. 할 일 목록 컴포넌트 생성 (app/components/TodoList.tsx)

할 일 목록을 표시하고, 각 할 일 옆에 삭제 버튼을 추가합니다. 삭제 버튼은 deleteTodo 서버 액션을 호출합니다.

// app/components/TodoList.tsx
'use client';

import { deleteTodo } from '@/app/actions';

interface Todo { 
  id: string;
  text: string;
}

interface TodoListProps {
  todos: Todo[];
}

export default function TodoList({ todos }: TodoListProps) {
  async function handleDelete(todoId: string) {
    const result = await deleteTodo(todoId);
    if (result.success) {
      alert(result.message);
    } else {
      alert('오류: ' + result.message);
    }
  }

  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {todos.length === 0 ? (
        <li style={{ textAlign: 'center', color: '#666' }}>아직 할 일이 없습니다. 새로운 할 일을 추가해보세요!</li>
      ) : (
        todos.map(todo => (
          <li key={todo.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 0', borderBottom: '1px solid #eee' }}>
            <span>{todo.text}</span>
            <button
              onClick={() => handleDelete(todo.id)}
              style={{ padding: '5px 10px', backgroundColor: '#dc3545', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
            >
              삭제
            </button>
          </li>
        ))
      )}
    </ul>
  );
}

6. 애플리케이션 실행

npm run dev

브라우저에서 http://localhost:3000에 접속하여 할 일을 추가하고 삭제해보세요. 서버 액션이 어떻게 작동하는지 확인할 수 있습니다.

⚠️ 자주 틀리는 것 / 주의사항

  • 'use server' 지시어 누락: 서버 액션으로 만들고자 하는 파일이나 함수에 'use server' 지시어를 선언하지 않으면, 해당 코드는 서버 액션으로 인식되지 않고 클라이언트에서 실행되거나 오류가 발생할 수 있습니다.
  • 클라이언트 컴포넌트에서의 직접적인 서버 코드 작성: 'use client'가 선언된 파일 내에서 'use server' 없이 데이터베이스 접근과 같은 서버 전용 코드를 작성하면 빌드 오류가 발생합니다. 서버 액션은 클라이언트 코드와 서버 코드를 명확히 분리하는 역할을 합니다.
  • 폼 데이터(FormData) 처리: 서버 액션은 FormData 객체를 인자로 받도록 설계되었습니다. 폼의 name 속성을 통해 데이터에 접근하는 방법을 숙지해야 합니다.
  • 에러 처리 및 UI 피드백: 서버 액션은 비동기 함수이므로, try-catch 문을 사용하여 에러를 적절히 처리하고, 사용자에게 성공/실패 메시지 등의 UI 피드백을 제공하는 것이 중요합니다.
  • 데이터 재검증(Revalidation) 이해: 서버 액션으로 데이터를 변경한 후, 변경된 내용이 UI에 반영되도록 revalidatePath 또는 revalidateTag 함수를 사용하여 Next.js의 데이터 캐시를 무효화해야 합니다. 이를 누락하면 UI가 즉시 업데이트되지 않을 수 있습니다.

🔗 다음 회차 예고

이번 회차에서는 서버 액션의 기본적인 개념과 폼 제출과의 연동을 통해 데이터를 처리하는 방법을 학습했습니다. 다음 17회차에서는 서버 액션의 고급 활용 및 에러 처리 전략에 대해 더 깊이 있게 다룰 예정입니다. 구체적으로는 서버 액션에서 발생할 수 있는 다양한 에러 상황을 효과적으로 처리하는 방법, 폼 유효성 검사(validation)를 서버 액션 내에서 구현하는 방법, 그리고 서버 액션의 응답을 활용하여 클라이언트 UI를 더욱 동적으로 업데이트하는 기법들을 탐구할 것입니다. 또한, useFormStatus, useFormState와 같은 React Hook을 사용하여 서버 액션의 로딩 상태 및 결과를 클라이언트에서 관리하는 방법도 함께 학습할 것입니다.

✅ 이번 회차 핵심 정리
  • 서버 액션(Server Actions)은 Next.js에서 클라이언트 컴포넌트가 서버 코드를 직접 호출하여 데이터를 처리할 수 있게 하는 기능입니다.
  • 'use server' 지시어를 파일 최상단 또는 함수 내부에 선언하여 서버 액션을 정의합니다.
  • HTML <form>action 속성에 서버 액션 함수를 직접 연결하여 폼 데이터를 제출할 수 있습니다.
  • 서버 액션은 FormData 객체를 인자로 받아 폼 필드에 접근하며, 비동기적으로 실행됩니다.
  • 데이터 변경 후에는 revalidatePath 또는 revalidateTag를 사용하여 Next.js 캐시를 무효화하고 UI를 업데이트해야 합니다.
  • 서버 액션은 API 라우트 없이 데이터 처리 로직을 구현할 수 있어 개발 편의성과 성능을 향상시킵니다.

댓글 남기기

Wordpress Social Share Plugin powered by Ultimatelysocial
Copy link
URL has been copied successfully!
THREADS
RSS
error: 저작권 콘텐츠보호를 부탁드립니다.