[NextJS] Learn 12장 - 데이터 변형

알찬코·2024년 1월 22일

NextJS

목록 보기
13/20
post-thumbnail

이전 장에서는 URL 검색 매개변수 및 Next.js API를 사용하여 검색 및 페이지 매김을 구현했습니다. 송장 생성, 업데이트 및 삭제 기능을 추가하여 송장 페이지 작업을 계속해 보겠습니다.

  • React Server Actions가 무엇이며 이를 사용하여 데이터를 변경하는 방법.
  • 양식 및 서버 구성 요소로 작업하는 방법.
  • 유형 유효성 검사를 포함하여 기본 formData 개체 작업에 대한 모범 사례입니다.
  • revalidatePath API를 사용하여 클라이언트 캐시의 유효성을 다시 검사하는 방법.
  • 특정 ID를 사용하여 동적 경로 세그먼트를 만드는 방법.

1. 서버 작업이란 무엇입니까?

React Server Actions를 사용하면 서버에서 직접 비동기 코드를 실행할 수 있습니다. 데이터를 변경하기 위해 API 엔드포인트를 생성할 필요가 없습니다. 대신, 서버에서 실행되고 클라이언트 또는 서버 구성 요소에서 호출될 수 있는 비동기 함수를 작성합니다.

웹 애플리케이션은 다양한 위협에 취약할 수 있으므로 보안이 최우선입니다. 여기서 서버 작업이 필요합니다. 서버 작업은 효과적인 보안 솔루션을 제공하여 다양한 유형의 공격으로부터 보호하고 데이터를 보호하며 승인된 액세스를 보장합니다. 서버 작업은 POST 요청, 암호화된 폐쇄, 엄격한 입력 확인, 오류 메시지 해싱, 호스트 제한과 같은 기술을 통해 이를 달성하며 모두 함께 작동하여 앱의 안전성을 크게 향상시킵니다.

2. 서버 작업과 함께 양식 사용

React에서는 <form> 요소안에 action 속성을 사용하여 작업을 호출할 수 있습니다. 작업(action)은 기본 FormData(캡처된 데이터가 포함된 개체)를 자동으로 수신합니다.

예를 들어:

// 서버 구성 요소
export default function Page() {
  // 액션
  async function create(formData: FormData) {
    'use server';
 
    // 데이터를 변화시키는 로직...
  }
 
  // "action" 속성을 사용하여 액션을 호출합니다
  return <form action={create}>...</form>;
}

서버 구성 요소 내에서 서버 작업을 호출하면 점진적인 향상이 가능하다는 장점이 있습니다. 클라이언트에서 JavaScript가 비활성화된 경우에도 양식이 작동합니다.

3. 서버 작업이 포함된 Next.js

서버 작업은 Next.js 캐싱과도 긴밀하게 통합됩니다. 서버 작업을 통해 양식이 제출되면 해당 작업을 사용하여 데이터를 변경할 수 있을 뿐만 아니라 revalidatePathrevalidateTag와 같은 API를 사용하여 관련 캐시의 유효성을 다시 검사할 수도 있습니다.

🔍 퀴즈

  • 서버 작업을 사용하면 어떤 이점이 있나요?
    a. 향상된 SEO.
    b. 점진적인 향상.
    c. 더 빠른 웹사이트.
    d. 데이터 암호화.

이 모든 것이 어떻게 함께 작동하는지 봅시다.

4. 송장 만들기

새 송장을 생성하기 위해 수행할 단계는 다음과 같습니다.
1. 사용자의 입력을 캡처하는 양식을 만듭니다.
2. 서버 작업을 만들고 양식에서 호출합니다.
3. 서버 작업 내에 formData 개체에서 데이터를 추출합니다.
4. 데이터베이스에 삽입할 데이터를 검증하고 준비합니다.
5. 데이터를 삽입하고 오류를 처리합니다.
6. 캐시를 재검증하고 사용자를 청구서 페이지로 다시 리디렉션합니다.

4-1. 새 경로 및 양식 만들기

시작하려면 /invoices 폴더 내에 /create라는 새 경로 세그먼트를 page.tsx 파일로 추가하세요.
create-invoice-route

이 경로를 사용하여 새 송장을 생성하게 됩니다. page.tsx 파일 안에 다음 코드를 붙여넣은 후 잠시 연구해 보세요.

// /dashboard/invoices/create/page.tsx

import Form from '@/app/ui/invoices/create-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
 
export default async function Page() {
  const customers = await fetchCustomers();
 
  return (
    <main>
      <Breadcrumbs
        breadcrumbs={[
          { label: 'Invoices', href: '/dashboard/invoices' },
          {
            label: 'Create Invoice',
            href: '/dashboard/invoices/create',
            active: true,
          },
        ]}
      />
      <Form customers={customers} />
    </main>
  );
}

customers는 귀하의 페이지를 가져와서 <Form> 구성 요소로 전달하는 서버 구성 요소입니다. 시간을 절약하기 위해 우리는 이미 귀하를 위한 <Form> 구성 요소를 만들었습니다.

<Form> 구성 요소로 이동하면 다음과 같은 형식이 표시됩니다.

  • 고객 목록이 포함된 하나의 <select> (dropdown) 요소가 있습니다 .
  • type="number"인 금액을 위한 하나의 <input> 요소가 있습니다.
  • type="radio" 상태를 위한 두 가지 <input> 요소가 있습니다.
  • type="submit"인 버튼이 하나 있습니다.

http://localhost:3000/dashboard/invoices/create 에서, 다음 UI가 표시됩니다.
create-invoice-page

4-2. 서버 액션 생성

좋습니다. 이제 양식이 제출될 때 호출될 서버 작업을 만들어 보겠습니다.

lib 디렉터리 로 이동하여 actions.ts 이름의 파일을 만드세요. 이 파일 상단에 React use server 명령을 추가하세요:

'use server';

'use server'를 추가하면 파일 내에서 내보낸 모든 기능을 서버 기능으로 표시합니다. 그런 다음 이러한 서버 기능을 클라이언트 및 서버 구성 요소로 가져올 수 있으므로 매우 다양하게 사용할 수 있습니다.

액션 내부에 "use server"를 추가하여 서버 구성 요소 내에서 직접 서버 액션을 작성할 수도 있습니다. 하지만 이 과정에서는 모든 항목을 별도의 파일에 정리하여 보관하겠습니다.

actions.ts 파일에서 formData를 허용하는 새 비동기 함수를 만듭니다.

'use server';
 
export async function createInvoice(formData: FormData) {} // 비동기 함수 생성

그런 다음 <Form> 구성 요소안에, actions.ts 파일에서 createInvoice를 가져옵니다. <form> 요소에 action 속성을 추가 하고 createInvoice 작업을 호출합니다.

// /app/ui/invoices/create-form.tsx

'use client';
 
import { customerField } from '@/app/lib/definitions';
import Link from 'next/link';
import {
  CheckIcon,
  ClockIcon,
  CurrencyDollarIcon,
  UserCircleIcon,
} from '@heroicons/react/24/outline';
import { Button } from '@/app/ui/button';
import { createInvoice } from '@/app/lib/actions'; // createInvoice 불러오기
 
export default function Form({
  customers,
}: {
  customers: customerField[];
}) {
  return (
    <form action={createInvoice}> // 액션 추가하기
      // ...
  )
}

알아두면 좋은 점 : HTML에서는 action 속성에 URL을 전달합니다. 이 URL은 양식 데이터를 제출해야 하는 대상(일반적으로 API 엔드포인트)입니다.
그러나 React에서 이 action 속성은 특별한 prop으로 간주됩니다. 즉, React가 그 위에 작업을 호출할 수 있도록 구축한다는 의미입니다.
그 뒤에서 서버 작업은 POST API 엔드포인트를 생성합니다. 이것이 바로 서버 작업을 사용할 때 API 엔드포인트를 수동으로 생성할 필요가 없는 이유입니다.

4-3. formData에서 데이터를 추출합니다.

actions.ts 파일로 돌아가서 formData 값을 추출해야 합니다. 당신이 사용할 수 있는 몇 가지 방법이 있습니다. 이 예에서는 .get(name)을 사용합니다.

'use server';
// 데이터 추출하기
export async function createInvoice(formData: FormData) {
  const rawFormData = {
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  };
  // 테스트해 보세요:
  console.log(rawFormData);
}

팁: 필드가 많은 양식으로 작업하는 경우, JavaScript의 Object.fromEntries()와 함께 entries() 메소드를 사용하는 것을 고려해볼 수 있습니다. 예를 들어:
const rawFormData = Object.fromEntries(formData.entries())

모든 것이 올바르게 연결되었는지 확인하려면 양식을 사용해 보세요. 제출한 후에는 터미널에 로그인된 양식에 방금 입력한 데이터가 표시되어야 합니다.

이제 데이터가 개체 형태이므로 작업하기가 훨씬 더 쉬워집니다.

4-4. 데이터 검증 및 준비

양식 데이터를 데이터베이스로 보내기 전에 올바른 형식과 올바른 유형인지 확인하려고 합니다. 과정 앞부분에서 배운 내용을 기억하시면 송장 테이블에 다음 형식의 데이터가 필요합니다.

// /app/lib/definitions.ts

export type Invoice = {
  id: string; // 데이터베이스에 작성됩니다
  customer_id: string;
  amount: number; // 센트 단위로 저장
  status: 'pending' | 'paid';
  date: string;
};

지금까지는 양식에서 customer_id, amountstatus만 있습니다 .

유형 검증 및 강제
양식의 데이터가 데이터베이스의 예상 유형과 일치하는지 확인하는 것이 중요합니다. 예를 들어 작업 내부에 console.log를 추가하는 경우:

console.log(typeof rawFormData.amount);

amount 유형이 number가 아닌 string 유형임을 알 수 있습니다. 이는 type="number"input 요소가 실제로 숫자가 아닌 문자열을 반환하기 때문입니다.

유형 유효성 검사를 위한 몇 가지 옵션이 있습니다. 유형을 수동으로 검증할 수도 있지만 유형 검증 라이브러리를 사용하면 시간과 노력을 절약할 수 있습니다. 귀하의 예에서는 이 작업을 단순화할 수 있는 TypeScript 우선 검증 라이브러리인 Zod를 사용하겠습니다.

actions.ts 파일에서 Zod를 가져오고 양식 개체의 모양과 일치하는 스키마를 정의합니다. 이 스키마는 데이터베이스에 저장하기 전에 formData 유효성을 검사합니다.

// /app/lib/actions.ts

'use server';
 
import { z } from 'zod'; // zod 불러오기
// 스키마 정의
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum(['pending', 'paid']),
  date: z.string(),
});
 
const CreateInvoice = FormSchema.omit({ id: true, date: true }); // 유효성 검증
 
export async function createInvoice(formData: FormData) {
  // ...
}

amount 필드는 해당 유형의 유효성을 검사하는 동시에 문자열을 숫자로 강제(변경)하도록 특별히 설정되어 있습니다.

그런 다음 rawFormDataCreateInvoice에 전달하여 유형을 검증할 수 있습니다.

// /app/lib/actions.ts

// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({ // 유형 검증
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
}

값을 센트 단위로 저장
일반적으로 JavaScript 부동 소수점 오류를 제거하고 정확성을 높이기 위해 데이터베이스에 금전적 가치를 센트 단위로 저장하는 것이 좋습니다.

금액을 센트로 변환해 보겠습니다.

// /app/lib/actions.ts

// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100; // 금액 변환
}

새 날짜 만들기
마지막으로 송장 생성 날짜에 대해 "YYYY-MM-DD" 형식으로 새 날짜를 생성해 보겠습니다.

// /app/lib/actions.ts

// ...
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0]; // 날짜 생성
}

4-5. 데이터베이스에 데이터 삽입

이제 데이터베이스에 필요한 모든 값이 있으므로 SQL 쿼리를 생성하여 데이터베이스에 새 송장을 삽입하고 변수를 전달할 수 있습니다.

// /app/lib/actions.ts

import { z } from 'zod';
import { sql } from '@vercel/postgres'; // sql 불러오기
 
// ...
 
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];
  // 쿼리 생성
  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
}

현재로서는 오류를 처리하지 않습니다. 우리는 다음 장에서 그것을 할 것입니다. 지금은 다음 단계로 넘어가겠습니다.

4-6. 재검증 및 리디렉션

Next.js에는 한동안 사용자 브라우저에 경로 세그먼트를 저장하는 클라이언트 측 라우터 캐시가 있습니다. prefetching과 함께 이 캐시를 사용하면 사용자가 서버에 대한 요청 수를 줄이면서 경로 간을 빠르게 탐색할 수 있습니다.

청구서 경로에 표시된 데이터를 업데이트하고 있으므로 이 캐시를 지우고 서버에 대한 새 요청을 트리거하려고 합니다. Next.js의 revalidatePath 함수를 사용하여 이 작업을 수행할 수 있습니다.

// /app/lib/actions.ts

'use server';
 
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache'; // 함수 불러오기
 
// ...
 
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];
 
  await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
  
  revalidatePath('/dashboard/invoices'); // 경로 탐색
}

데이터베이스가 업데이트되면 /dashboard/invoices 경로의 유효성이 다시 검사되고 서버에서 새로운 데이터를 가져옵니다.

이 시점에서 사용자를 /dashboard/invoices 페이지로 다시 리디렉션하려고 합니다. Next.js의 redirect 함수를 사용하여 이 작업을 수행할 수 있습니다.

// /app/lib/actions.ts

'use server';
 
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation'; // 함수 불러오기
 
// ...
 
export async function createInvoice(formData: FormData) {
  // ...
 
  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices'); // 해당 URL로 재요청
}

축하해요! 방금 첫 번째 서버 작업을 구현했습니다. 모든 것이 올바르게 작동하는지 새 송장을 추가하여 테스트해 보세요.

  1. 제출 시 /dashboard/invoices 경로로 리디렉션되어야 합니다 .
  2. 표 상단에 새 인보이스가 표시됩니다.

5. 송장 업데이트

송장 양식 업데이트는 송장 양식 생성과 유사하지만, 데이터베이스의 기록을 업데이트하려면 송장 id를 전달해야 한다는 점만 다릅니다. 송장 id를 받고 전달하는 방법을 살펴보겠습니다.

송장을 업데이트하기 위해 수행할 단계는 다음과 같습니다.

  1. 송장 id를 사용하여 새 동적 경로 세그먼트를 만듭니다.
  2. 페이지 매개변수에서 송장 id를 읽어보세요.
  3. 데이터베이스에서 특정 송장을 가져옵니다.
  4. 송장 데이터로 양식을 미리 채웁니다.
  5. 데이터베이스의 송장 데이터를 업데이트하십시오.

5-1. 송장 id를 사용하여 동적 경로 세그먼트를 생성합니다.

Next.js를 사용 하면 정확한 세그먼트 이름을 모르고 데이터를 기반으로 경로를 생성하려는 경우 동적 경로 세그먼트를 생성할 수 있습니다 . 블로그 게시물 제목, 제품 페이지 등이 될 수 있습니다. 폴더 이름을 대괄호로 묶어 동적 경로 세그먼트를 만들 수 있습니다. 예를 들어 [id], [post] 또는 [slug].

/invoices 폴더에서 [id]라는 새 동적 경로를 만든 다음 edit으로 호출되는 새 경로를 page.tsx 파일과 함께 만듭니다. 파일 구조는 다음과 같아야 합니다.
edit-invoice-route

<Table> 구성 요소에는 테이블 기록에서 송장의 id를 받는 <UpdateInvoice /> 버튼이 있습니다.

// /app/ui/invoices/table.tsx

export default async function InvoicesTable({
  query,
  currentPage,
}: {
  query: string;
  currentPage: number;
}) {
  return (
    // ...
    <td className="flex justify-end gap-2 whitespace-nowrap px-6 py-4 text-sm">
      <UpdateInvoice id={invoice.id} /> // 버튼 추가
      <DeleteInvoice id={invoice.id} />
    </td>
    // ...
  );
}

<UpdateInvoice /> 구성 요소로 이동하고 id prop을 수락하도록 Linkhref를 업데이트합니다. 템플릿 리터럴을 사용하여 동적 경로 세그먼트에 연결할 수 있습니다.

// /app/ui/invoices/buttons.tsx

import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
 
// ...
 
export function UpdateInvoice({ id }: { id: string }) {
  return (
    <Link
      href={`/dashboard/invoices/${id}/edit`} // 템플릿 리터럴 사용
      className="rounded-md border p-2 hover:bg-gray-100"
    >
      <PencilIcon className="w-5" />
    </Link>
  );
}

5-2. 페이지 params에서 송장 id를 읽으십시오

<Page> 구성 요소로 돌아가서 다음 코드를 붙여넣습니다.

// /app/dashboard/invoices/[id]/edit/page.tsx

import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
 
export default async function Page() {
  return (
    <main>
      <Breadcrumbs
        breadcrumbs={[
          { label: 'Invoices', href: '/dashboard/invoices' },
          {
            label: 'Edit Invoice',
            href: `/dashboard/invoices/${id}/edit`,
            active: true,
          },
        ]}
      />
      <Form invoice={invoice} customers={customers} />
    </main>
  );
}

(edit-form.tsx 파일에서) 다른 양식을 가져오는 점을 제외하면 /create 송장 페이지와 얼마나 유사한지 확인하세요. 이 양식 에는 고객 이름, 송장 금액 및 상태에 대한 정보가 미리 채워져 있어야 합니다. 양식 필드를 미리 채우려면 defaultValue를 사용하여 특정 송장 id를 가져와야 합니다.

searchParams 외에도 페이지 구성 요소는 params에 액세스하는 데 사용할 수 있는 id prop이라는 속성도 허용합니다. prop을 받으려면 <Page> 구성 요소를 업데이트하세요.

// /app/dashboard/invoices/[id]/edit/page.tsx

import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
// Page 업데이트
export default async function Page({ params }: { params: { id: string } }) {
  const id = params.id;
  // ...
}

5-3. 특정 송장을 가져옵니다.

그 다음에:

  • fetchInvoiceById라는 새 함수를 가져오고 id를 인수로 전달합니다.
  • 드롭다운에 대한 고객 이름을 가져오려면 fetchCustomers 가져오기를 수행하세요.

Promise.all을 사용하여 송장과 고객을 동시에 가져올 수 있습니다.

// /dashboard/invoices/[id]/edit/page.tsx

import Form from '@/app/ui/invoices/edit-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data'; // 데이터 불러오기
 
export default async function Page({ params }: { params: { id: string } }) {
  const id = params.id;
  // 특정 송장 가져오기
  const [invoice, customers] = await Promise.all([
    fetchInvoiceById(id),
    fetchCustomers(),
  ]);
  // ...
}

invoice는 잠재적으로 undefined가 발생할 수 있으므로 터미널의 invoice prop에 대한 일시적인 TS 오류가 표시됩니다. 지금은 걱정하지 마세요. 다음 장에서 오류 처리를 추가하면 문제가 해결될 것입니다.

엄청납니다. 이제 모든 것이 올바르게 연결되었는지 테스트하십시오. http://localhost:3000/dashboard/invoices 를 방문하세요. 송장을 편집하려면 연필 아이콘을 클릭하세요. 탐색 후에는 송장 세부정보가 미리 채워진 양식이 표시됩니다.
edit-invoice-page

http://localhost:3000/dashboard/invoice/uuid/edit URL도 id와 같이 업데이트되어야 합니다.

UUID와 자동 증가(Auto-incrementing) 키
키를 증가시키는 대신(예: 1, 2, 3 등) UUID를 사용합니다. 이렇게 하면 URL이 길어집니다. 그러나 UUID는 ID 충돌 위험을 제거하고 전역적으로 고유하며 열거 공격 위험을 줄여 대규모 데이터베이스에 이상적입니다.
그러나 더 깔끔한 URL을 선호한다면 자동 증가 키를 사용하는 것이 좋습니다.

5-4. 서버 작업에 id 전달

마지막으로 데이터베이스에서 올바른 레코드를 업데이트할 수 있도록 서버 작업에 id를 전달하려고 합니다. 다음과 같이 id를 인수로 전달할 수 없습니다.

// /app/ui/invoices/edit-form.tsx

// id를 인수로 전달하면 작동하지 않습니다
<form action={updateInvoice(id)}>

대신 JS bind를 사용하여 서버 작업에 id를 전달할 수 있습니다. 이렇게 하면 서버 작업에 전달된 모든 값이 인코딩됩니다.

// /app/ui/invoices/edit-form.tsx

// ...
import { updateInvoice } from '@/app/lib/actions'; // updateInvoice 불러오기
 
export default function EditInvoiceForm({
  invoice,
  customers,
}: {
  invoice: InvoiceForm;
  customers: CustomerField[];
}) {
  const updateInvoiceWithId = updateInvoice.bind(null, invoice.id); // id 전달
 
  return (
    <form action={updateInvoiceWithId}> // 인코딩
      <input type="hidden" name="id" value={invoice.id} />
    </form>
  );
}

참고: 양식에 숨겨진 입력 필드를 사용하는 것도 가능합니다(예: <input type="hidden" name="id" value={invoice.id} />). 그러나 값은 HTML 소스에 전체 텍스트로 표시되므로 ID와 같은 민감한 데이터에는 적합하지 않습니다.

그런 다음 actions.ts 파일에서 새 작업, updateInvoice을 만듭니다.

// Zod를 사용하여 예상되는 유형 업데이트
const UpdateInvoice = FormSchema.omit({ id: true, date: true });
 
// ...
 
export async function updateInvoice(id: string, formData: FormData) {
  const { customerId, amount, status } = UpdateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
 
  const amountInCents = amount * 100;
 
  await sql`
    UPDATE invoices
    SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
    WHERE id = ${id}
  `;
 
  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}

createInvoice 작업과 유사하게, 다음과 같습니다.
1. formData에서 데이터를 추출합니다.
2. Zod를 사용하여 유형을 검증합니다.
3. 금액을 센트로 변환합니다.
4. 변수를 SQL 쿼리에 전달합니다.
5. revalidatePath는 클라이언트 캐시를 지우고 새 서버 요청을 하기 위해 호출합니다 .
6. redirect는 사용자를 청구서 페이지로 리디렉션하기 위해 호출합니다.

송장을 편집하여 테스트해보세요. 양식을 제출한 후 청구서 페이지로 리디렉션되고 청구서가 업데이트되어야 합니다.

6. 송장 삭제

서버 작업을 사용하여 송장을 삭제하려면 <form> 요소에 삭제 버튼을 래핑하고 bind를 사용하여 서버 작업에 id를 전달합니다.

import { deleteInvoice } from '@/app/lib/actions'; // deleteInvoice 불러오기
 
// ...
// id 전달
export function DeleteInvoice({ id }: { id: string }) { 
  const deleteInvoiceWithId = deleteInvoice.bind(null, id); 
 
  return (
    <form action={deleteInvoiceWithId}> // 삭제 버튼 래핑
      <button className="rounded-md border p-2 hover:bg-gray-100">
        <span className="sr-only">Delete</span>
        <TrashIcon className="w-4" />
      </button>
    </form>
  );
}

actions.ts 파일 내부에 deleteInvoice라는 새 작업을 만듭니다.

export async function deleteInvoice(id: string) {
  await sql`DELETE FROM invoices WHERE id = ${id}`;
  revalidatePath('/dashboard/invoices');
}

이 작업은 /dashboard/invoices 경로에서 호출되므로 redirect를 호출할 필요가 없습니다. revalidatePath를 호출하면 새 서버 요청이 트리거되고 테이블이 다시 렌더링됩니다.

7. 추가 읽기

이 장에서는 서버 작업을 사용하여 데이터를 변경하는 방법을 배웠습니다. 또한 revalidatePath API를 사용하여 Next.js 캐시의 유효성을 다시 검사하고, 사용자를 새 페이지로 리디렉션하는 redirect 방법도 배웠습니다.

추가 학습을 위해 서버 작업을 통한 보안에 대해 자세히 알아볼 수도 있습니다.

📖 참고자료

Chapter 12. Mutating Data

0개의 댓글