이전 장에서는 오류(404 오류 포함)를 포착하고 사용자에게 대체 메시지를 표시하는 방법을 살펴보았습니다. 그러나 여전히 퍼즐의 또 다른 부분인 양식 유효성 검사에 대해 논의해야 합니다. 서버 작업을 사용하여 서버측 유효성 검사를 구현하는 방법과 접근성을 염두에 두고 useFormState 후크를 사용하여 양식 오류를 표시하는 방법을 살펴보겠습니다.
eslint-plugin-jsx-a11y를 사용하여 접근성 모범 사례를 구현하는 방법.useFormState 후크를 사용하여 양식 오류를 처리하고 사용자에게 표시하는 방법.접근성이란 장애가 있는 사용자를 포함하여 모든 사람이 사용할 수 있는 웹 애플리케이션을 설계하고 구현하는 것을 의미합니다. 키보드 탐색, 의미론적 HTML, 이미지, 색상, 비디오 등과 같은 많은 영역을 다루는 광범위한 주제입니다.
이 과정에서는 접근성에 대해 자세히 다루지는 않지만 Next.js에서 사용할 수 있는 접근성 기능과 애플리케이션의 접근성을 높이는 몇 가지 일반적인 방법에 대해 논의하겠습니다.
접근성에 대해 자세히 알아보려면 web.dev의 접근성 학습 코스를 권장합니다.
기본적으로 Next.js에는 접근성 문제를 조기에 발견하는 데 도움이 되는 eslint-plugin-jsx-a11y 플러그인이 포함됩니다. 예를 들어, 이 플러그인은 alt 텍스트가 없는 이미지가 있는 경우, aria-* 및 role 속성을 잘못 사용하는 경우 등을 경고합니다.
이것이 어떻게 작동하는지 봅시다.
package.json 파일에 스크립트로 next lint를 추가합니다.
"scripts": {
"build": "next build",
"dev": "next dev",
"seed": "node -r dotenv/config ./scripts/seed.js",
"start": "next start",
"lint": "next lint" // 여기 추가
},
그런 다음 터미널에서 npm run lint를 실행하십시오.
npm run lint
다음 경고가 표시됩니다.
✔ No ESLint warnings or errors
그런데 alt 텍스트 없이 이미지만 있으면 어떻게 될까요? 알아봅시다.
/app/ui/invoices/table.tsx로 이동하여 이미지로부터 alt prop을 제거하세요. 에디터의 검색 기능을 사용하여 <Image>를 빠르게 찾을 수 있습니다.
// /app/ui/invoices/table.tsx
<Image
src={invoice.image_url}
className="rounded-full"
width={28}
height={28}
alt={`${invoice.name}'s profile picture`} // 이 줄 지우기
/>
이제 npm run lint를 다시 실행하면 다음 경고가 표시됩니다.
./app/ui/invoices/table.tsx
45:25 Warning: Image elements must have an alt prop,
either with meaningful text, or an empty string for decorative images. jsx-a11y/alt-text
Vercel에 애플리케이션을 배포하려고 하면 빌드 로그에도 경고가 표시됩니다. 이는 빌드 프로세스의 일부로 next lint가 실행되기 때문입니다. 따라서 애플리케이션을 배포하기 전에 로컬로 lint를 실행하여 접근성 문제를 파악할 수 있습니다.
양식의 접근성을 개선하기 위해 이미 세 가지 작업을 수행하고 있습니다.
<div> 대신 시맨틱 요소(<input>, <option> 등)를 사용합니다. 이를 통해 보조 기술(AT)은 입력 요소에 집중하고 사용자에게 적절한 상황 정보를 제공하여 양식을 더 쉽게 탐색하고 이해할 수 있습니다.<label>과 htmlFor 속성을 포함하면 각 양식 필드에 설명 텍스트 라벨이 포함됩니다. 이는 컨텍스트를 제공하여 AT 지원을 향상시키고 사용자가 레이블을 클릭하여 해당 입력 필드에 집중할 수 있게 함으로써 유용성을 향상시킵니다.tab을 눌러 이를 확인할 수 있습니다.이러한 방법은 많은 사용자가 양식에 더 쉽게 액세스할 수 있도록 하는 좋은 기반을 마련합니다. 그러나 양식 유효성 검사 및 오류는 해결되지 않습니다.
http://localhost:3000/dashboard/invoices/create 로 이동하세요. 빈 양식을 제출하세요. 무슨 일이 일어납니까?
오류가 발생했습니다. 이는 빈 양식 값을 서버 작업으로 보내기 때문입니다. 클라이언트나 서버에서 양식의 유효성을 검사하면 이를 방지할 수 있습니다.
클라이언트에서 양식의 유효성을 검사할 수 있는 몇 가지 방법이 있습니다. 가장 간단한 방법은 양식 의 <input> 및 <select> 요소에 required 속성을 추가하여 브라우저에서 제공하는 양식 유효성 검사에 의존하는 것입니다. 예를 들어:
// /app/ui/invoices/create-form.tsx
<input
id="amount"
name="amount"
type="number"
placeholder="Enter USD amount"
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
required // 속성 추가
/>
양식을 다시 제출하고, 이제 빈 값이 포함된 양식을 제출하려고 하면 브라우저에 경고가 표시됩니다.
일부 AT는 브라우저 유효성 검사를 지원하므로 이 접근 방식은 일반적으로 괜찮습니다.
클라이언트 측 유효성 검사의 대안은 서버 측 유효성 검사입니다. 다음 섹션에서 어떻게 구현하는지 살펴보겠습니다. 지금은 required 속성을 추가한 경우 삭제하세요.
서버에서 양식의 유효성을 검사하여 다음을 수행할 수 있습니다.
create-form.tsx 구성 요소에서 react-dom의 useFormState 후크를 가져옵니다. useFormState는 후크이므로 "use client" 지시어를 사용하여 양식을 클라이언트 구성 요소로 바꿔야 합니다.
// /app/ui/invoices/create-form.tsx
'use client';
// ...
import { useFormState } from 'react-dom';
양식 구성 요소 내부의 useFormState 후크는 다음과 같습니다.
(action, initialState).[state, dispatch] - 양식 상태 및 디스패치 함수(useReducer와 유사)createInvoice 작업을 useFormState의 인수로 전달 하고 <form action={}> 속성 내에서 dispatch를 호출하세요.
// /app/ui/invoices/create-form.tsx
// ...
import { useFormState } from 'react-dom';
export default function Form({ customers }: { customers: CustomerField[] }) {
const [state, dispatch] = useFormState(createInvoice, initialState); // 인수 전달
return <form action={dispatch}>...</form>; // dispatch 호출
}
initialState는 사용자가 정의하는 모든 것이 될 수 있습니다. 이 경우 두 개의 빈 키인 message 및 errors를 사용하여 객체를 만듭니다.
// /app/ui/invoices/create-form.tsx
// ...
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>;
}
처음에는 혼란스러워 보일 수 있지만 서버 작업을 업데이트하면 더 이해가 될 것입니다. 지금 해봅시다.
action.ts 파일에서 Zod를 사용하여 양식 데이터의 유효성을 검사할 수 있습니다. 다음과 같이 FormSchema를 업데이트하세요.
const FormSchema = z.object({
id: z.string(),
customerId: z.string({
invalid_type_error: 'Please select a customer.', // 유효성 검사
}),
amount: z.coerce
.number()
.gt(0, { message: 'Please enter an amount greater than $0.' }), // 유효성 검사
status: z.enum(['pending', 'paid'], {
invalid_type_error: 'Please select an invoice status.', // 유효성 검사
}),
date: z.string(),
});
string을 예상하여 고객 필드가 비어 있으면 미리 오류를 발생시킵니다. 하지만 사용자가 고객을 선택하지 않으면 친근한 메시지를 추가해 보겠습니다.string에서 number로 강제 변환하므로 문자열이 비어 있으면 기본값은 0이 됩니다. .gt() 함수를 사용하여 Zod에게 우리는 항상 0보다 큰 양을 원한다고 말합시다.다음으로 두 매개변수를 허용하도록 createInvoice 작업을 업데이트합니다.
// /app/lib/actions.ts
// 이는 @types/react-dom이 업데이트될 때까지 일시적입니다
export type State = {
errors?: {
customerId?: string[];
amount?: string[];
status?: string[];
};
message?: string | null;
};
export async function createInvoice(prevState: State, formData: FormData) {
// ...
}
formData - 이전과 같습니다.prevState - useFormState 후크에서 전달된 상태를 포함합니다. 이 예의 작업에서는 이를 사용하지 않지만 필수 prop입니다.그런 다음 Zod parse() 함수를 safeParse()로 변경합니다.
// /app/lib/actions.ts
export async function createInvoice(prevState: State, formData: FormData) {
// Zod를 사용하여 양식 필드 검증
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// ...
}
safeParse()는 success 또는 error 필드를 포함하는 객체를 반환합니다. 이렇게 하면 이 로직을 try/catch 블록 안에 넣지 않고도 유효성 검사를 보다 원활하게 처리하는 데 도움이 됩니다.
데이터베이스에 정보를 보내기 전에 양식 필드가 조건부로 올바르게 검증되었는지 확인하세요.
// /app/lib/actions.ts
export async function createInvoice(prevState: State, formData: FormData) {
// Zod를 사용하여 양식 필드 검증
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// 양식 유효성 검사에 실패하면 오류를 조기에 반환하고, 그렇지 않으면 계속하십시오.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
// ...
}
validatedFields를 성공하지 못하면, Zod의 오류 메시지와 함께 함수를 조기에 반환합니다.
팁: console.log validatedFields를 작성 하고 빈 양식을 제출하여 모양을 확인하세요.
마지막으로, try/catch 블록 외부에서 양식 유효성 검사를 별도로 처리하므로 데이터베이스 오류에 대해 특정 메시지를 반환할 수 있습니다. 최종 코드는 다음과 같습니다.
// /app/lib/actions.ts
export async function createInvoice(prevState: State, formData: FormData) {
// Zod를 사용하여 양식 검증
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// 양식 유효성 검사에 실패하면 오류를 조기에 반환하고, 그렇지 않으면 계속하십시오.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
// 데이터베이스에 삽입할 데이터 준비
const { customerId, amount, status } = validatedFields.data;
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');
}
좋습니다. 이제 양식 구성 요소에 오류를 표시해 보겠습니다. create-form.tsx 구성요소로 돌아가서 양식 state를 사용하여 오류에 액세스할 수 있습니다.
각 특정 오류를 확인하는 삼항 연산자를 추가합니다. 예를 들어 고객 필드 뒤에 다음을 추가할 수 있습니다.
// /app/ui/invoices/create-form.tsx
<form action={dispatch}>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer 이름 */}
<div className="mb-4">
<label htmlFor="customer" className="mb-2 block text-sm font-medium">
Choose customer
</label>
<div className="relative">
<select
id="customer"
name="customerId"
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
defaultValue=""
aria-describedby="customer-error" // 오류 표시
>
<option value="" disabled>
Select a customer
</option>
{customers.map((name) => (
<option key={name.id} value={name.id}>
{name.name}
</option>
))}
</select>
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
</div>
// 삼항연산자 추가
<div id="customer-error" aria-live="polite" aria-atomic="true">
{state.errors?.customerId &&
state.errors.customerId.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>
// ...
</div>
</form>
팁: 구성 요소 내부에 console.log state를 기록하고 모든 것이 올바르게 연결되었는지 확인할 수 있습니다. 귀하의 양식이 이제 클라이언트 구성 요소이므로 개발자 도구에서 콘솔을 확인하십시오.
위 코드에서는 다음과 같은 aria 라벨도 추가합니다.
aria-describedby="customer-error" select: select 요소와 오류 메시지 컨테이너 간의 관계를 설정합니다. 이는 id="customer-error"인 컨테이너가 select 요소를 설명함을 나타냅니다. 화면 판독기는 사용자가 select 상자와 상호 작용하여 오류를 알릴 때 이 설명(description)을 읽습니다.id="customer-error": 이 id 속성은 select 입력에 대한 오류 메시지를 보유하는 HTML 요소를 고유하게 식별합니다. 이는 aria-describedby 관계를 확립하는 데 필요합니다.aria-live="polite": 스크린 리더는 div의 내부 오류가 업데이트되면 정중하게 사용자에게 알려야 합니다. 콘텐츠가 변경되면(예: 사용자가 오류를 수정하는 경우) 스크린 리더는 이러한 변경 사항을 알려주지만, 사용자가 이를 중단하지 않도록 유휴(대기) 상태인 경우에만 알려줍니다.위의 예를 사용하여 나머지 양식 필드에 오류를 추가하세요. 또한 누락된 필드가 있는 경우 양식 하단에 메시지를 표시해야 합니다. UI는 다음과 같아야 합니다.

준비가 되면 npm run lint를 실행하여 aria 라벨을 올바르게 사용하고 있는지 확인하세요.
스스로 도전하고 싶다면 이 장에서 배운 지식을 활용하여 edit-form.tsx 구성 요소에 양식 유효성 검사를 추가하세요.
다음을 수행해야 합니다.
edit-form.tsx 구성요소 에 useFormState를 추가하세요.updateInvoice 작업을 수정합니다.