[NextJS] async usages on SSR

Darcy Daeseok YU ·2025년 6월 8일

1.Fetching Data for Server Components

async function getUserData() {
  // This fetch request runs on the server
  const res = await fetch('https://api.example.com/user/123', { cache: 'no-store' }); // Example: no-store for dynamic data
  if (!res.ok) {
    throw new Error('Failed to fetch user data');
  }
  return res.json();
}

export default async function DashboardPage() {
  const userData = await getUserData(); // Await the data fetching
  // The component will only start rendering after userData is resolved

  return (
    <div>
      <h1>Welcome, {userData.name}</h1>
      <p>Email: {userData.email}</p>
      {/* ... more content */}
    </div>
  );
}

2.Accessing Database Directly

// app/products/[id]/page.tsx
import { db } from '@/lib/db'; // Assuming your database connection

interface ProductPageProps {
  params: { id: string };
}

export default async function ProductPage({ params }: ProductPageProps) {
  const productId = params.id;
  const product = await db.product.findUnique({ // Directly query database
    where: { id: productId },
  });

  if (!product) {
    // Handle product not found
    return <div>Product not found</div>;
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

3.Reading from File System (Server-side)

// app/docs/[slug]/page.tsx
import fs from 'fs/promises';
import path from 'path';

interface DocPageProps {
  params: { slug: string };
}

export default async function DocPage({ params }: DocPageProps) {
  const filePath = path.join(process.cwd(), 'docs', `${params.slug}.md`);
  let content = '';
  try {
    content = await fs.readFile(filePath, 'utf8');
  } catch (error) {
    // Handle file not found or read error
    console.error('Error reading doc file:', error);
    return <div>Document not found.</div>;
  }

  return (
    <article>
      <h1>{params.slug}</h1>
      <pre>{content}</pre>
    </article>
  );
}

4.Handling Server Actions

// app/items/actions.ts (a separate file for server actions)
'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

export async function addItem(formData: FormData) {
  const name = formData.get('name') as string;
  await db.item.create({ data: { name } }); // Async DB operation
  revalidatePath('/items'); // Revalidate cache for the items page
}

Example
actions.ts

// app/todos/actions.ts
'use server'; // ⭐ 서버 액션임을 나타내는 필수 지시어

import { revalidatePath } from 'next/cache'; // 캐시 재검증을 위한 함수

// 💡 가상의 데이터 저장소 (실제 앱에서는 데이터베이스가 이 역할을 합니다)
interface Todo {
  id: string;
  text: string;
  completed: boolean;
  createdAt: Date;
}

const todos: Todo[] = [
  { id: '1', text: '예제 할 일 1', completed: false, createdAt: new Date() },
  { id: '2', text: '예제 할 일 2 (완료)', completed: true, createdAt: new Date() },
];

/**
 * 모든 할 일 목록을 가져오는 함수 (서버 컴포넌트에서 호출)
 * 이 함수는 서버 액션이 아니지만, 서버에서만 사용됨
 */
export async function getTodosList(): Promise<Todo[]> {
  // 실제 데이터베이스 쿼리 대신 가상 데이터 반환
  return todos.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
}

/**
 * 새로운 할 일을 데이터 저장소에 추가하는 서버 액션
 * @param formData - 폼에서 전송된 데이터
 */
export async function addTodo(formData: FormData) {
  const todoText = formData.get('todoText') as string;

  if (!todoText || todoText.trim() === '') {
    return { error: '할 일 내용을 입력해주세요.' };
  }

  const newTodo: Todo = {
    id: String(Date.now()), // 간단한 고유 ID 생성
    text: todoText.trim(),
    completed: false,
    createdAt: new Date(),
  };

  todos.push(newTodo); // 가상 데이터 저장소에 추가

  // 특정 경로의 캐시를 재검증하여 해당 페이지가 다시 렌더링되도록 합니다.
  // 여기서는 할 일 목록이 표시되는 /todos 경로를 재검증합니다.
  revalidatePath('/todos');

  return { success: true }; // 성공 시 반환
}

/**
 * 할 일의 완료 상태를 토글하는 서버 액션
 */
export async function toggleTodoCompleted(id: string) {
  // `'use server'` 지시어는 파일 최상단에 있어도 개별 함수에도 명시할 수 있습니다.
  // 명시적으로 표시하는 것이 유지보수에 도움이 될 수 있습니다.
  'use server';

  const todo = todos.find(t => t.id === id);
  if (todo) {
    todo.completed = !todo.completed; // 상태 토글
    revalidatePath('/todos'); // 캐시 재검증
  } else {
    return { error: '할 일을 찾을 수 없습니다.' };
  }
  return { success: true };
}

ListPage.tsx (SSR)

import { getTodosList, addTodo, toggleTodoCompleted } from './actions'; // 서버 액션 및 가상 데이터 가져오기 함수 임포트

export default async function TodosPage() {
  const todos = await getTodosList(); // 서버에서 직접 할 일 목록 가져오기

  return (<></>)
}

Form.tsx(CSR + Server Action Function to revalidate SSR Page)

import { addTodo } from './actions'; // 서버 액션 임포트

export default function TodoForm() {
  const formRef = useRef<HTMLFormElement>(null); // 폼 요소를 참조하여 초기화

  // 클라이언트에서 서버 액션을 호출하고 결과를 처리하는 함수
  const clientAction = async (formData: FormData) => {
    const result = await addTodo(formData); // 서버 액션 호출 및 결과 대기

    if (result?.error) {
      alert(result.error); // 서버 액션에서 반환된 에러 메시지 표시
    } else {
      formRef.current?.reset(); // 성공 시 폼 입력 필드 초기화
    }
  };

5.generateStaticParams (for Static Site Generation)

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json());
  return posts.map((post: { slug: string }) => ({ slug: post.slug }));
}
profile
React, React-Native https://darcyu83.netlify.app/

0개의 댓글