Server Actions란?
React Server Actions
를 통해 서버에서 직접 비동기 코드를 실행할 수 있다.
- Server Actions는 다양한 유형의 공격으로부터 데이터를 보호하고 권한 있는 액세스를 보장하는 등 효과적인 보안 솔루션을 제공한다.
- Server Actions는 Next.js 캐싱 기능과도 깊이 연관되어 있는데, form이 Server Actions를 통해 제출되었을 때 데이터를 업데이트 할 뿐만 아니라
revalidatePath
, revalidateTag
등의 API를 이용해서 관련된 캐시를 revalidate 할 수 있다.
export default function Page() {
async function create(formData: FormData) {
'use server';
}
return <form action={create}>...</form>;
}
<form>
에 action
속성을 추가함으로써 Server Actions를 호출할 수 있다.
action
은 자동적으로 입력 데이터를 포함한 foramData
객체를 전달 받는다.
- Server Actions를 서버 컴포넌트에서 호출하면 클라이언트에서 자바스크립트가 사용 불가능한 경우에도 form이 작동한다는 장점이 있다.
DB에 데이터 생성하기
1. server action 생성하기
export async function createInvoice(formData: FormData) {}
<form action={createInvoice}>
lib
폴더에 actions.ts
파일을 생성한다.
- 파일 최상단에
'use server'
명령어를 추가해서 해당 파일에서 export
되는 모든 함수가 서버 함수로 작동하도록 한다.
actions.ts
파일에 createInvoice
함수를 생성해서 <form>
태그의 action
속성에 createInvoice
함수를 전달한다.
export async function createInvoice(formData: FormData) {
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
};
}
formData
에서 데이터를 추출하기 위해서 다양한 메서드를 사용할 수 있는데 해당 예제에서는 get(name)
메서드를 사용해서 form에 입력한 데이터를 추출한다.
3. 데이터 유효성 검사하기
import { z } from 'zod';
const InvoiceSchema = z.object({
id: z.string(),
customerId: z.string(),
amount: z.coerce.number(),
status: z.enum(['pending', 'paid']),
date: z.string(),
});
const CreateInvoice = InvoiceSchema.omit({ id: true, date: true });
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];
}
- 데이터를 DB에 전송하기 전에 데이터가 올바른 형식 및 타입으로 입력되었는지 확인해야 한다. 해당 예제에서는 유효성 검사 라이브러리인
zod
를 사용해서 이를 수행한다.
zod
를 사용해서 데이터 타입을 체크한 후 Schema
와 일치하지 않을 경우 데이터 타입을 강제 변환한다.
4. DB에 데이터 삽입하기
import { sql } from '@vercel/postgres';
export async function createInvoice(formData: FormData) {
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
}
- SQL 쿼리문을 작성해서
invoices
DB에 form으로 입력 받은 데이터를 추가한다.
5. 데이터 갱신 및 리다이렉트
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createInvoice(formData: FormData) {
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
revalidatePath
함수를 이용해서 클라이언트의 캐시를 지우고 서버에 새로운 요청을 보내서 업데이트된 데이터를 새로 받아온다.
- form 제출 후 데이터 생성에 성공하면
/dashboard/invoices
페이지로 리다이렉트한다.
DB의 데이터 업데이트하기
1. Dynamic Route Segment 생성하기
/invoices
폴더 안에 [id]
라는 새로운 dynamic route와 edit
페이지를 생성한다. 해당 폴더 구조의 페이지의 경로는 /invoices/uuid/edit
이 된다.
2. page params에서 invoice id 읽어오기
export default async function Page({ params }: { params: { id: string } }) {
const id = params.id;
}
Page
컴포넌트는 searchParams
외에도 params
를 props로 전달받는데, 이 params
를 통해 id
에 접근할 수 있다.
3. 상세 데이터 불러오기
export default async function Page({ params }: { params: { id: string } }) {
const id = params.id;
const [invoice, customers] = await Promise.all([
fetchInvoiceById(id),
fetchCustomers(),
]);
return (
<Form invoice={invoice} customers={customers} />
)
}
/invoices/[id]/edit/page.tsx
파일에 Form
컴포넌트를 추가하는데 이 컴포넌트에서는 해당 invoice id를 가진 특정 데이터만 불러와서 form의 defaultValue
를 미리 채워넣는다.
fetchInvoiceById()
함수에 params
로 가져온 id
를 전달해서 해당 invoice의 데이터를 불러온다.
4. server action에 id 전달하기
const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
return (
<form action={updateInvoiceWithId}>
);
id
를 argument로 전달하면 작동하지 않기 때문에 bind()
메서드를 이용해서 updateInvoice
함수에 id
를 첫 번째 파라미터로 함께 전달한다.
- 수정한 데이터를 제출하면
createInvoice
action와 비슷한 로직으로 DB의 데이터가 업데이트된다.
server actions에 try/catch 추가하기
export async function createInvoice(formData: FormData) {
try {
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
} catch (error) {
return {
message: 'Database Error: Failed to Create Invoice.',
};
}
}
server actions
함수에 try/catch
를 추가해서 에러를 정상적으로 처리할 수 있도록 한다.
error.tsx로 모든 에러 핸들링하기
"use client"
export default function Error({ error, reset,}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<button onClick={() => reset()}>
Try again
</button>
)
}
error.tsx
파일을 사용해서 해당 페이지의 모든 에러를 catch해서 사용자에게 fallback UI를 보여줄 수 있다.
error.tsx
파일은 클라이언트 컴포넌트여야 한다.
error.tsx
파일은 두 개의 props
를 받는다.
1. error
: 자바스크립트의 Error object
인스턴스다.
2. reset
: error boundary
를 리셋하는 함수다. 이 함수를 실행하면 해당 페이지를 리렌더링한다.
notFound 함수로 404 에러 핸들링하기
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: { id: string } })
if (!invoice) {
notFound();
}
}
error.tsx
가 모든 에러를 핸들링 할 수 있다면, notFound
는 DB에 존재하지 않는 리소스를 요청했을 때 사용할 수 있다.
- 404 에러의 경우에 사용자에게 에러 UI를 보여주고 싶다면 해당 라우트의 폴더에
not-found.tsx
파일을 생성한다.
notFound()
는 error.tsx
보다 우선시되기 때문에 보다 구체적인 에러를 핸들링하고 싶을 때 사용할 수 있다.
접근성(Accessibility)이란?
- 접근성은 장애인을 포함한 모든 사람이 웹 애플리케이션을 사용할 수 있도록 설계하고 구현하는 것을 의미한다.
- 키보드 내비게이션, 시맨틱 HTML, 이미지, 색상, 동영상 등 다양한 영역을 포함한다.
- 시맨틱 HTML:
<div>
대신 <input>
, <option>
등의 시맨틱 HTML 요소를 사용한다. 이를 통해 보조 기술(AT)이 입력 요소에 초점을 맞추고 사용자에게 적절한 상황 정보를 제공하여 form을 탐색하고 이해하기 쉽게 한다.
- 라벨링: 각 필드에
label
이나 htmlFor
속성을 추가함으로써 필드에 상세 정보를 제공할 수 있다. 맥락을 제공함으로써 AT 지원 기능을 향상시키고, 사용자가 라벨을 통해 해당 input 필드를 클릭할 수 있도록 하여 사용성을 향상시킨다.
- 포커스 아웃라인: input 필드에 포커스가 갔을 때 스타일 요소를 적용한다. 이는 페이지의 활성 요소를 시각적으로 나타내기 때문에 접근성에 매우 중요하며, 키보드와 스크린 리더 사용자 모두 form의 어느 필드에 위치해 있는지를 이해할 수 있도록 도와준다.
- form 유효성 검사: form에 값을 입력하지 않았거나 형식에 맞지 않는 값을 입력했을 때 사용자들에게 에러를 보여줄 수 있다.
client side 유효성 검사
<input
id="amount"
name="amount"
type="number"
placeholder="Enter USD amount"
required
/>
<input>
와 <select>
요소에 required
속성을 추가한다.
- 값을 입력하지 않고 폼을 제출할 경우 브라우저에서 에러 메세지를 볼 수 있다.
server side 유효성 검사
import { useFormState } from 'react-dom';
export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState = { message: null, errors: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
return <form action={dispatch}>...</form>;
}
react-dom
에서 제공하는 useFormState
훅을 사용한다.
<form>
태그에 action
속성에 전달하는 함수를 dispatch
로 변경한다.
InvoiceSchema
에 에러 메세지를 추가하고 createInvoice
함수에 prevState
파라미터를 추가하는 등 관련 코드를 수정한다. 링크 참고
FormHelperText 보여주기
export default function FormHelperText({
errors,
fieldName,
}: FormHelperTextProps) {
const isError = !!errors;
if (!isError) {
return null;
}
return (
<div
id={`${fieldName}-error`}
aria-live="polite"
className="mt-2 text-sm text-red-500"
>
{errors.map((error: string) => (
<p key={error}>{error}</p>
))}
</div>
);
}
<FormHelperText errors={state.errors?.amount} fieldName="amount" />
useFormState
가 return 한 값인 state
를 이용해서 필드 값을 입력하지 않았을 경우 에러 메세지를 보여준다.
- 공식문서에는 없는 부분이지만 동일한 코드를 여러 번 재사용하기 때문에
FormHelperText
컴포넌트를 생성했다.