[NextJS] Learn 14장 - 접근성 개선

알찬코·2024년 1월 22일

NextJS

목록 보기
15/20
post-thumbnail

이전 장에서는 오류(404 오류 포함)를 포착하고 사용자에게 대체 메시지를 표시하는 방법을 살펴보았습니다. 그러나 여전히 퍼즐의 또 다른 부분인 양식 유효성 검사에 대해 논의해야 합니다. 서버 작업을 사용하여 서버측 유효성 검사를 구현하는 방법과 접근성을 염두에 두고 useFormState 후크를 사용하여 양식 오류를 표시하는 방법을 살펴보겠습니다.

  • Next.js와 함께 eslint-plugin-jsx-a11y를 사용하여 접근성 모범 사례를 구현하는 방법.
  • 서버 측 양식 유효성 검사를 구현하는 방법.
  • React useFormState 후크를 사용하여 양식 오류를 처리하고 사용자에게 표시하는 방법.

1. 접근성이란 무엇입니까?

접근성이란 장애가 있는 사용자를 포함하여 모든 사람이 사용할 수 있는 웹 애플리케이션을 설계하고 구현하는 것을 의미합니다. 키보드 탐색, 의미론적 HTML, 이미지, 색상, 비디오 등과 같은 많은 영역을 다루는 광범위한 주제입니다.

이 과정에서는 접근성에 대해 자세히 다루지는 않지만 Next.js에서 사용할 수 있는 접근성 기능과 애플리케이션의 접근성을 높이는 몇 가지 일반적인 방법에 대해 논의하겠습니다.

접근성에 대해 자세히 알아보려면 web.dev접근성 학습 코스를 권장합니다.

2. Next.js에서 ESLint 접근성 플러그인 사용

기본적으로 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를 실행하여 접근성 문제를 파악할 수 있습니다.

3. 양식 접근성 개선

양식의 접근성을 개선하기 위해 이미 세 가지 작업을 수행하고 있습니다.

  • Semantic HTML: <div> 대신 시맨틱 요소(<input>, <option> 등)를 사용합니다. 이를 통해 보조 기술(AT)은 입력 요소에 집중하고 사용자에게 적절한 상황 정보를 제공하여 양식을 더 쉽게 탐색하고 이해할 수 있습니다.
  • Labelling: <label>htmlFor 속성을 포함하면 각 양식 필드에 설명 텍스트 라벨이 포함됩니다. 이는 컨텍스트를 제공하여 AT 지원을 향상시키고 사용자가 레이블을 클릭하여 해당 입력 필드에 집중할 수 있게 함으로써 유용성을 향상시킵니다.
  • Focus Outline: 초점이 맞춰졌을 때 윤곽선을 표시하도록 필드의 스타일이 적절하게 지정되었습니다. 이는 페이지의 활성 요소를 시각적으로 표시하여 키보드와 화면 판독기 사용자 모두가 양식의 위치를 이해하는 데 도움이 되므로 접근성에 매우 중요합니다. tab을 눌러 이를 확인할 수 있습니다.

이러한 방법은 많은 사용자가 양식에 더 쉽게 액세스할 수 있도록 하는 좋은 기반을 마련합니다. 그러나 양식 유효성 검사 및 오류는 해결되지 않습니다.

4. 양식 유효성 검사

http://localhost:3000/dashboard/invoices/create 로 이동하세요. 빈 양식을 제출하세요. 무슨 일이 일어납니까?

오류가 발생했습니다. 이는 빈 양식 값을 서버 작업으로 보내기 때문입니다. 클라이언트나 서버에서 양식의 유효성을 검사하면 이를 방지할 수 있습니다.

4-1. 클라이언트 측 검증

클라이언트에서 양식의 유효성을 검사할 수 있는 몇 가지 방법이 있습니다. 가장 간단한 방법은 양식 의 <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 속성을 추가한 경우 삭제하세요.

4-2. 서버측 검증

서버에서 양식의 유효성을 검사하여 다음을 수행할 수 있습니다.

  • 데이터를 데이터베이스로 보내기 전에 데이터가 예상된 형식인지 확인하세요.
  • 악의적인 사용자가 클라이언트 측 유효성 검사를 우회하는 위험을 줄입니다.
  • 유효한 데이터로 간주되는 정보에 대한 하나의 진실 소스를 확보하세요.

create-form.tsx 구성 요소에서 react-domuseFormState 후크를 가져옵니다. 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는 사용자가 정의하는 모든 것이 될 수 있습니다. 이 경우 두 개의 빈 키인 messageerrors를 사용하여 객체를 만듭니다.

// /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(),
});
  • customerId - Zod는 유형 string을 예상하여 고객 필드가 비어 있으면 미리 오류를 발생시킵니다. 하지만 사용자가 고객을 선택하지 않으면 친근한 메시지를 추가해 보겠습니다.
  • amount - 금액 유형을 string에서 number로 강제 변환하므로 문자열이 비어 있으면 기본값은 0이 됩니다. .gt() 함수를 사용하여 Zod에게 우리는 항상 0보다 큰 양을 원한다고 말합시다.
  • status - Zod는 "pending" 또는 "paid"을 예상하므로 상태 필드가 비어 있으면 미리 오류를 발생시킵니다. 사용자가 상태를 선택하지 않은 경우에도 친근한 메시지를 추가해 보겠습니다.

다음으로 두 매개변수를 허용하도록 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의 내부 오류가 업데이트되면 정중하게 사용자에게 알려야 합니다. 콘텐츠가 변경되면(예: 사용자가 오류를 수정하는 경우) 스크린 리더는 이러한 변경 사항을 알려주지만, 사용자가 이를 중단하지 않도록 유휴(대기) 상태인 경우에만 알려줍니다.

✍ 연습문제: aria 라벨 추가하기

위의 예를 사용하여 나머지 양식 필드에 오류를 추가하세요. 또한 누락된 필드가 있는 경우 양식 하단에 메시지를 표시해야 합니다. UI는 다음과 같아야 합니다.
form-validation-page

준비가 되면 npm run lint를 실행하여 aria 라벨을 올바르게 사용하고 있는지 확인하세요.

스스로 도전하고 싶다면 이 장에서 배운 지식을 활용하여 edit-form.tsx 구성 요소에 양식 유효성 검사를 추가하세요.

다음을 수행해야 합니다.

  • edit-form.tsx 구성요소 에 useFormState를 추가하세요.
  • Zod의 유효성 검사 오류를 처리하기 위한 updateInvoice 작업을 수정합니다.
  • 구성 요소에 오류를 표시하고 aria 레이블을 추가하여 접근성을 향상시킵니다.

📖 참고자료

Chapter 14. Improving Accessibility

0개의 댓글