Next.js를 알아보자 - 공식문서 따라하기(12~13장)
12. 데이터 변형
송장 생성, 업데이트 및 삭제 기능을 추가해 송장 페이지를 구현한다.
12-1. 서버 작업이란?
React Server Actions를 사용하면 서버에서 직접 비동기 코드를 실행할 수 있다.
데이터를 변경하기 위해 API 엔드포인트를 생성할 필요가 없지만, 서버에서 실행되고 클라이언트 또는 서버 컴포넌트에서 호출될 수 있는 비동기 함수를 작성한다.
웹 애플리케이션은 다양한 위협에 취약할 수 있으므로 보안이 최우선이다.
서버 작업이 필요한 곳이다.
효과적인 보안 솔루션을 제공해 다양한 유형의 공격으로부터 보호하고 데이터를 보호하며 승인된 액세스를 보장한다.
서버 작업은 POST 요청, 암호화된 클로저, 오류 메세지 해싱, 호스트 제한과 같은 기술을 통해 보호하며 모두 함께 작동하여 앱의 안전성을 크게 향상시킨다.
12-2. Server Action과 함께 폼 사용하기
React에서는 <form> 요소의 action 속성을 사용해 액션을 호출할 수 있다.
액션은 캡쳐된 데이터가 포함된 기본 FormData 객체를 자동으로 수신한다.
// Server Component
export default function Page() {
// Action
async function create(formData: FormData) {
'use server';
// 데이터 변형 로직...
}
// action 속성을 사용해 액션 호출하기
return <form action={create}>...</form>;
}
서버 컴포넌트 내에서 서버 액션을 호출하면 점진적인 향상이 가능하다는 장점이 있다.
클라이언트에서 JavaScript가 비활성화된 경우에도 폼이 작동한다.
12-3. Server Action이 포함된 Next.js
서버 액션은 Next.js 캐싱과도 긴밀하게 통합된다.
서버 액션을 통해 폼이 제출되면 액션을 사용해 데이터를 변경할 수 있을 뿐만 아니라 revalidatePath 및 revalidateTag와 같은 API를 사용하여 관련 캐시의 유효성을 다시 검사할 수도 있다.
12-4. 송장 생성하기
새 송장을 생성하기 위한 단계는 다음과 같다.
- 유저의 입력을 캡쳐하는 폼을 만든다.
- 서버 액션을 만들고 폼에서 호출한다.
- 서버 액션 내 formData 객체에서 데이터를 추출한다.
- 데이터베이스에 삽입할 데이터를 검증하고 준비한다.
- 데이터를 삽입하고 오류를 처리한다.
- 캐시를 재검증하고 유저를 청구서 페이지로 다시 리다이렉션한다.
1. 새 경로와 폼 생성하기
/invoices 폴더에 page.tsx 파일을 담은 새로운 /create 경로 세그먼트를 추가한다. 이 경로를 사용하여 새 송장을 생성한다.
// 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>
);
}
Page는 customers를 가져와 <Form> 컴포넌트에 전달하는 서버 컴포넌트이다.
<Form> 컴포넌트로 이동하면 볼 수 있는 요소들이 있다.
- customers 리스트를 가진 하나의 <select>
- type="number"인 금액에 대한 하나의 <input>
- type="radio"인 상태에 대한 두 개의 <input>
- type="submit"인 하나의 버튼
2. 서버 액션 생성하기
폼이 제출될 때 호출될 서버 액션을 만들자!
lib 디렉토리로 이동해 actions.ts 파일을 생성한다. 이 파일의 최상단에 React use server를 선언한다.
// app/lib/actions.ts
'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
// ...
import { createInvoice } from '@/app/lib/actions';
export default function Form({
customers,
}: {
customers: customerField[];
}) {
return (
<form action={createInvoice}>
// ...
)
}
3. formData에서 데이터 추출하기
action.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);
}
연결이 잘 되었는지 확인하기 위해 폼을 작성한 후 제출을 하면 터미널에 입력한 데이터가 표시된다.
데이터가 객체 형태이므로 작업하기가 훨씬 쉬워진다!
4. 데이터 검증 및 준비
폼 데이터를 데이터베이스로 보내기 전에 올바른 형식과 유형인지 확인한다.
정의되어 있는 Invoice 테이블을 보면 필요한 데이터 형식을 알 수 있다.
// app/lib/definitions.ts
export type Invoice = {
id: string;
customer_id: string;
amount: number;
status: 'pending' | 'paid';
date: string;
};
지금 폼에서는 customer_id, amount, status만 존재한다.
타입 검증 및 변환
폼의 데이터가 데이터베이스의 예상 유형과 일치하는지 확인하는 것이 중요하다.
console.log(typeof rawFormData.amount);
amount가 number가 아닌 string 타입임을 알 수 있다. 이는 type='number'인 input 요소가 실제로 숫자가 아닌 문자열을 반환하기 때문이다.
타입 유효성 검사를 처리하기 위한 몇 가지 옵션
타입을 수동으로 검증할 수 있지만 타입 검증 라이브러리를 사용하면 시간과 노력을 절약할 수 있다.
여기서는작업을 단순화할 수 있는 TypeScript 우선 검증 라이브러리인 Zod를 사용한다.
action.ts에서 Zod를 가져오고 타입 객체의 모양과 일치하는 스키마를 정의한다.
이 스키마는 formData를 데이터베이스에 저장하기 전에 유효성을 검사한다.
'use server';
import { z } from '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 필드는 해당 타입의 유효성을 검사하는 동시에 문자열을 숫자로 변경하도록 설정되어 있다.
rawFormData를 CreateInvoice에 전달해 타입을 확인할 수 있다.
// ...
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 부동 소수점 오류를 제거하고 정확성을 높이기 위해 데이터베이스에 금전적인 값을 센트 단워로 저장하는 것이 좋다.
// ...
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" 형식으로 새 날짜를 생성한다.
// ...
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];
}
5. 데이터베이스에 데이터 추가
데이터베이스에 필요한 모든 값을 준비했으니 SQL 쿼리를 생성해 데이터베이스에 새 송장을 추가하고 변수를 전달할 수 있다.
import { z } from 'zod';
import { sql } from '@vercel/postgres';
// ...
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})
`;
}
그런데 오류가...?
6. 재검증 및 리다이렉션
Next.js에는 한동안 사용자 브라우저에 경로 세그먼트를 저장하는 Client-side Router Cache가 있다.
프리패칭과 함께 이 캐시를 사용하면 유저가 서버에 대한 요청 수를 줄이면서 경로 간을 빠르게 탐색할 수 있다.
invoice 경로에 표시된 데이터를 업데이트하고 있으므로, 이 캐시를 지우고 서버에 대한 새 요청을 트리거한다.
Next.js의 revalidatePath 함수를 사용해 이 작업을 수행할 수 있다.
- revalidatePath : 특정 경로에 대해 필요에 따라 캐시된 데이터를 제거
'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페이지로 다시 리다이렉션한다.
'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');
}
이제 새 송장을 추가할 수 있다!
12-5. 송장 업데이트
업데이트는 생성하기와 유사하지만, 데이터베이스의 레코드를 업데이트하려면 송장의 id를 전달해야 한다.
송장을 업데이트하기 위한 단계
- 송장 id를 사용해 새 동적 경로 세그먼트를 만든다.
- 페이지 매개변수에서 송장 id를 읽는다.
- 데이터베이스에서 특정 송장을 가져온다.
- 송장 데이터로 폼을 미리 채운다.
- 데이터베이스의 송장 데이터를 업데이트한다.
1. 송장 id를 사용해 동적 경로 세그먼트 생성하기
Next.js를 사용하면 정확한 세그먼트 이름을 모르고 데이터를 기반으로 경로를 생성하려는 경우 동적 경로 세그먼트를 생성할 수 있다.
폴더 이름을 대괄호로 묶어 동적 경로 세그먼트를 만들 수 있다.
/invoices 폴더에서 [id]라는 새 동적 경로를 만든 다음 page.tsx 파일을 사용해 edit이라는 새 경로를 만든다.
<Table> 컴포넌트에는 테이블 레코드에서 송장 id를 받는 <UpdateInvoice /> 버튼이 있다.
<UpdateInvoice /> 컴포넌트로 이동해 Link의 href를 업데이트해서 id prop을 수락한다. 템플릿 리터럴을 사용해 동적 경로 세그먼트에 연결할 수 있다.
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>
);
}
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 invoice 페이지와 유사하다.
이 폼은 고객 이름, 송장 금액 및 상태에 대한 defaultValue로 미리 채워져 있어야 한다.
폼의 필드를 미리 채우려면 id를 사용해 특정 송장을 가져와야 한다.
searchParams 외에도 페이지 컴포넌트는 id에 액세스하는 데 사용할 수 있는 params prop도 허용한다.
<Page> 컴포넌트를 업데이트한다.
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({ params }: { params: { id: string } }) {
const id = params.id;
// ...
}
3. 특정 송장 가져오기
- fetchInvoiceById 함수를 가져와 인수로 id를 전달한다.
- fetchCustomers를 가져와 고객 이름 드롭다운을 요청한다.
Promise.all을 사용해 송장과 고객을 동시에 가져올 수 있다.
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(),
]);
// ...
}
4. Server Action에 id 전달하기
데이터베이스에서 올바를 레코드를 업데이트할 수 있도록 서버 액션에 id를 인수로 전달하면 동작하지 않는다..!
대신 JS bind를 사용해 서버 액션에 id를 전달할 수 있다.
이렇게 하면 서버 액션에 전달된 모든 값이 인코딩된다.
// app/ui/invoices/edit-form.tsx
// ...
import { updateInvoice } from '@/app/lib/actions';
export default function EditInvoiceForm({
invoice,
customers,
}: {
invoice: InvoiceForm;
customers: CustomerField[];
}) {
const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
return (
<form action={updateInvoiceWithId}>
<input type="hidden" name="id" value={invoice.id} />
</form>
);
}
그 다음 actions.ts 파일에 updateInvoice 액션을 만든다.
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');
}
updateInvoice는 createInvoice와 유사하게 동작한다.
- formData에서 데이터를 추출한다.
- Zod를 사용해 타입을 검증한다.
- amount를 센트로 변환한다.
- 변수를 SQL 쿼리에 전달한다.
- 클라이언트 캐시를 지우고 새 서버 요청을 하기 위해 revalidatePath를 호출한다.
- 유저를 청구서 페이지로 리다이렉션하기 위해 redirect를 호출한다,
12-6. 송작 삭제하기
서버 액션을 사용해 송장을 삭제하려면 삭제 버튼을 <form>으로 감싸고 bind를 사용해 서버 액션에 id를 전달한다.
// app/ui/invoices/button.tsx
import { deleteInvoice } from '@/app/lib/actions';
// ...
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를 호출하면 새 서버 요청이 트리거되고 테이블이 다시 렌더링된다.
13. 오류 처리
JavaScript의 try/catch 명령문과 Next.js API를 사용해 오류를 "우아하게" 처리하는 방법
13-1. Server Action에 try/catch 추가하기
먼저 오류를 적절하게 처리할 수 있도록 서버 액션에 JavaScript try/catch문을 추가한다.
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];
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.',
};
}
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
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;
try {
await sql`
UPDATE invoices
SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
WHERE id = ${id}
`;
} catch (error) {
return { message: 'Database Error: Failed to Update Invoice.' };
}
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
export async function deleteInvoice(id: string) {
try {
await sql`DELETE FROM invoices WHERE id = ${id}`;
revalidatePath('/dashboard/invoices');
return { message: 'Deleted Invoice.' };
} catch (error) {
return { message: 'Database Error: Failed to Delete Invoice.' };
}
}
redirect는 try/catch 블록 밖에서 호출된다. 이는 redirect이 catch 블록에 의해 포착되는 오류를 발생시켜 작동하기 때문이다.
이를 방지하려면 try/catch 후에 redirect를 호출하면 된다. try가 성공한 경우에만 redirect에 연결할 수 있다.
서버 액션에서 오류가 발생하면 어떤 일이 발생할까?
try 이전에 오류를 발생시켜 확인해 볼 수 있다. 예를 들어 deleteInvoice 액션에서 함수 상단에 오류를 발생시킨다.
export async function deleteInvoice(id: string) {
throw new Error('Failed to Delete Invoice');
try {
await sql`DELETE FROM invoices WHERE id = ${id}`;
revalidatePath('/dashboard/invoices');
return { message: 'Deleted Invoice' };
} catch (error) {
return { message: 'Database Error: Failed to Delete Invoice' };
}
}
삭제 버튼을 누르면 오류가 발생한다.
이러한 오류를 확인하면 잠재적인 문제를 초기에 발견할 수 있으므로 개발하는 동안 도움이 된다.
그러나 갑작스러운 오류를 방지하고 애플리케이션이 계속 실행될 수 있도록 사용자에게 오류를 표시할 수도 있다.
13-2. error.tsx로 모든 오류 처리하기
error.tsx 파일은 경로 세그먼트에 대한 UI 경계를 정의하는 데 사용할 수 있다.
예상치 못한 오류에 대한 포괄적인 역할을 하며 사용자에게 대체 UI를 표시할 수 있다.
/dashboard/invoices 폴더 안에 error.tsx 파일을 만들고 코드를 작성한다.
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Optionally log the error to an error reporting service
console.error(error);
}, [error]);
return (
<main className="flex h-full flex-col items-center justify-center">
<h2 className="text-center">Something went wrong!</h2>
<button
className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"
onClick={
// Attempt to recover by trying to re-render the invoices route
() => reset()
}
>
Try again
</button>
</main>
);
}
- "use clinet" - error.tsx은 클라이언트 컴포넌트여야 함
- 두 개의 props를 받음
- error : JavaScript의 기본 Error 객체의 인스턴스
- reset : 에러 바운더리를 재설정하는 함수. 실행되면 이 함수는 경로 세그먼트를 다시 렌더링하려고 시도함
삭제 버튼을 클릭하면 대체 UI가 표시된다.
13-3. notFound 함수로 404 에러 처리하기
오류를 적절하게 처리할 수 있는 또 다른 방법은 notFound 함수를 사용하는 것이다.
error.tsx는 모든 오류를 잡는데 유리하지만, notFound는 존재하지 않는 리소스를 가져오려고 할 때 사용할 수 있다.
404 오류를 표시해 사용자가 액세스하려는 리소스를 찾을 수 없음을 알릴 수 있다.
data.ts의 fetchInvoiceById 함수로 이동하고 반환된 invoice를 콘솔에서 기록하여 리소스를 찾지 못했음을 확인할 수 있다.
// app/lib/data.ts
export async function fetchInvoiceById(id: string) {
noStore();
try {
// ...
console.log(invoice); // Invoice is an empty array []
return invoice[0];
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch invoice.');
}
}
데이터베이스에 invoice가 없다는 것을 알았으니 notFound로 이를 처리한다.
/dashboard/invoices/[id]/edit/page.tsx로 이동해 'next/navigation'에서 {notFound}를 가져온다.
import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
import { updateInvoice } from '@/app/lib/actions';
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: { id: string } }) {
const id = params.id;
const [invoice, customers] = await Promise.all([
fetchInvoiceById(id),
fetchCustomers(),
]);
if (!invoice) {
notFound();
}
// ...
}
이제 특정 송장을 찾을 수 없으면 오류가 발생하고 사용자에게 오류 UI를 표시한다.
/edit 폴더 안에 not-found.tsx 파일을 만들고 코드를 작성한다.
import Link from 'next/link';
import { FaceFrownIcon } from '@heroicons/react/24/outline';
export default function NotFound() {
return (
<main className="flex h-full flex-col items-center justify-center gap-2">
<FaceFrownIcon className="w-10 text-gray-400" />
<h2 className="text-xl font-semibold">404 Not Found</h2>
<p>Could not find the requested invoice.</p>
<Link
href="/dashboard/invoices"
className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"
>
Go Back
</Link>
</main>
);
}
notFound는 error.tsx보다 우선하기 때문에 더 구체적인 오류를 처리하고 싶을때 사용하는 것을 명심하세요!