검증
에러를 알리고 해결 방법을 안내하는 패턴이다. 송금 앱에서 에러 상태가 모호하면 신뢰가 빠르게 무너진다. 이 패턴은 에러를 언제, 어디에, 어떻게 보여 줄지 표준으로 정해서 사용자가 무엇이 잘못됐고 어떻게 고치면 되는지 항상 알 수 있게 한다.
Senco에서 나는 에러는 대개 단순한 오타가 아니다. 거부된 계좌 번호, 초과한 한도, 인식하지 못한 서류 같은 것이고, 사용자는 이미 두 번째 시도인 경우가 많다. 여기 있는 패턴은 모두 두 번째 시도에서 성공하도록 맞춰져 있다.
다루는 것
- 인라인 에러
- 에러 요약
- 필수 상태
- 해결 안내
검증 시점
| Moment | What runs | Why |
|---|---|---|
| 입력하는 동안 | 검증하지 않음 | 이메일은 처음 여섯 글자를 칠 때까지 당연히 틀린 형식이다 |
| blur 시점 | 해당 필드의 규칙 | 사용자가 그 필드 입력을 마쳤다 |
| 에러가 난 필드로 돌아왔을 때 | 해당 필드의 규칙을 실시간으로 | 지금 고치는 중이라 아무 반응이 없으면 도움이 안 된다 |
| 제출 시점 | 필드 간 규칙을 포함한 모든 규칙 | 요청을 보내기 전 마지막 기회다 |
| 서버에서 | 클라이언트가 알 수 없는 것 | 계좌 번호 거부, 한도 초과, 환율 변동 |
"키를 누를 때마다 검증하지 않는다"는 규칙의 유일한 예외는 이미 에러가 난 필드다. 사용자가 고치는 중이라면 실시간 피드백으로 제대로 고쳐진 순간을 알려 준다. 사용자가 확인받고 싶은 게 바로 그 순간이다.
인라인 에러
- 입력 필드 아래에 표시한다
- 필드 이름과 무엇이 잘못됐는지를 담는다
- 에러 색, 아이콘, 텍스트를 함께 쓴다 (색만으로는 안 된다)
- aria-describedby로 입력과 연결한다
- blur나 폼 제출 시점에 표시한다 (키를 누를 때마다 표시하지 않는다)
구조
| Part | Token |
|---|---|
| 필드 테두리 | --ds-red-600 |
| 포커스 링 | --ds-focus-ring-error |
| 아이콘 | --ds-error-icon, 16px |
| 메시지 | --ds-error-text, --ds-text-sm |
| 간격 | 필드 아래 --ds-space-2 |
에러가 필드 아래에 나타나도 도움말 텍스트는 필드 위에 그대로 둔다. 도움말에는 대개 입력 형식이 적혀 있는데, 에러가 났을 때 이걸 없애면 고치는 방법을 알려 주던 줄이 사라진다. aria-describedby에는 에러를 먼저 넣어서 바로 행동할 수 있는 내용부터 읽히게 한다.
인라인 에러는 나타날 때 알리지 않는다
blur할 때마다 에러를 소리로 알리면, 이미 다음 필드로 넘어간 사용자를 방해하게 된다. 메시지는 aria-describedby로 연결해 두고 사용자가 그 필드로 돌아왔을 때 읽힌다. 소리로 알리는 건 사용자가 결과를 기다리고 있는 제출 실패 때뿐이다.
Try editing the value to see the error appear/disappear.
에러 요약
- 제출에 실패하면 폼 상단에 표시한다
- 모든 에러를 해당 필드로 가는 링크와 함께 나열한다
- 제출에 실패하면 포커스를 에러 요약으로 옮긴다
- 요약에 있는 각 에러는 인라인 에러 메시지와 똑같이 쓴다
- role="alert"나 aria-live="assertive"로 알린다
포커스를 옮겨야 하는 이유
사용자가 닿지 못하는 요약은 아무도 읽지 않는 목록일 뿐이다. 긴 폼에서 제출했는데 아무 소리도 듣지 못한 스크린 리더 사용자는 무언가 실패했다는 사실조차 알 수 없다. 화면을 보는 모바일 사용자도 페이지 맨 아래에서 아무 반응 없는 버튼만 보고 있게 된다.
포커스를 요약으로 옮기면 둘 다 해결된다. 요약이 스스로 읽히고, 화면도 요약 위치로 스크롤된다.
요구사항
| Requirement | Detail |
|---|---|
| 위치 | 폼 상단, 첫 필드 위 |
| 제목 | "There are 3 problems with this form" — 에러 개수를 넣는다 |
| 항목 | 에러 하나당 한 항목, 화면의 필드 순서대로 |
| 링크 | 각 항목은 필드 id로 연결되고 포커스를 그 필드로 옮긴다 |
| 문구 | 인라인 메시지와 똑같이 쓴다. 같은 문제를 두 가지로 쓰면 문제가 두 개인 것처럼 읽힌다 |
| 닫기 | 없다. 에러를 모두 고치면 사라진다 |
서버 에러도 같은 요약에 넣는다
계좌 번호 거부, 한도 초과, 환율 변동처럼 서버만 알 수 있는 에러도 같은 요약에 같은 형식으로 보여 준다. 사용자는 클라이언트 검증과 서버 검증을 구분하지 않는다. 모양이 다른 에러 영역이 하나 더 있으면 사용자는 두 군데를 확인해야 한다고 배우게 된다.
"This transfer exceeds your monthly limit"처럼 특정 필드에 속하지 않는 서버 에러는 필드 링크 없이 요약 맨 위에 둔다.
There are 2 errors in this form
필수 상태
- 필수 필드는 라벨 텍스트로 표시한다 ("required" 또는 설명이 붙은 별표)
- 대부분이 필수라면 선택 필드 쪽을 표시해도 된다
- 별표 하나에만 의존하지 않는다. 폼 상단에 별표의 뜻을 적어 둔다
- 필수 입력에는 aria-required="true"를 쓴다
수가 적은 쪽을 표시한다
필드 대부분이 필수라면 선택 필드를 표시하고, 대부분이 선택이라면 필수 필드를 표시한다. 아홉 개가 모두 필수인 폼에서 전부 표시하면 아무 정보도 없는 별표만 아홉 개 늘어난다.
별표만으로 표시해서는 안 된다. 별표의 뜻을 적어 둬야 하고, 빨간색으로만 구분한 별표는 SC 1.4.1을 통과하지 못한다.
해결 안내
- 에러 메시지에는 문제를 고치는 방법을 쓴다
- 기술 용어를 피한다 ("Invalid input" → "Enter a valid email address")
- 사용자가 입력한 내용을 지우지 않아서 다시 입력할 필요가 없게 한다
- 여러 필드에 걸친 에러는 어떤 조합이 잘못됐는지 설명한다
메시지 구조
모든 메시지에는 무엇이 잘못됐고 무엇을 하면 되는지를 쓴다. 시스템이 실제 값을 알고 있다면 그 값을 말해 준다. "insufficient funds"보다 "your balance is 180.00 USD"가 더 쓸모 있다. 얼마를 보내면 되는지 알 수 있기 때문이다.
| Do | Don't |
|---|---|
| "Enter an amount between 1.00 and 5,000.00 USD" | "Invalid amount" |
| "This account number is 10 digits — you entered 9" | "Format error" |
| "You don't have enough to send this. Your balance is 180.00 USD." | "Insufficient funds" |
| "The receiving bank rejected this account number. Check it with your recipient." | "Transfer failed (ERR_4021)" |
| "The ID photo was too blurry to read. Try again in better light." | "Verification unsuccessful" |
| "Enter the recipient's name exactly as it appears on their bank account" | "Name mismatch" |
에러 코드, 스키마 필드 이름, "invalid" 한 단어만 덩그러니 보여 주지 않는다.
입력한 내용은 지우지 않는다
검증 에러가 나도 사용자가 입력한 내용은 모두 그대로 둔다. "처음부터 다시 하라고" 필드를 비우면 사용자가 한 일을 날리는 셈이다. 실물 서류를 보고 옮겨 적은 값이었다면 그 서류를 다시 찾아야 한다.
제출에 실패했을 때도 마찬가지다. 폼의 모든 필드 값을 유지하고, 플로우의 단계도 그대로 둔다.
사용자 잘못이 아닌 에러
네트워크 실패, 서버 에러, 결제망 장애는 검증 에러가 아니므로 검증 에러처럼 써서는 안 된다. 무슨 일이 있었는지, 사용자의 돈은 어떤 상태인지, 다음에 어떻게 되는지를 알려 준다.
We couldn't reach the bank — nothing has been sent and your balance is unchanged. Try again in a few minutes.
중요한 건 가운데 문장이다. 송금 중에 에러를 만난 사용자는 돈이 나갔는지 알 수 없고, 누군가 알려 주기 전까지는 최악의 상황을 떠올린다.
접근성
- 모든 에러 메시지를 스크린 리더로 인지할 수 있어야 한다
- 포커스를 관리해서 사용자를 첫 번째 에러로 안내한다
- 색만으로 에러 상태를 나타내지 않는다
- 에러 메시지는 짧고, 무엇을 하면 되는지 담는다
구현 체크리스트
- 에러가 난 모든 필드에
aria-invalid="true"를 붙였다 -
aria-describedby가 에러를 먼저, 그다음 도움말 텍스트를 가리킨다 - 에러 요약에
role="alert"를 쓰고, 제출에 실패하면 포커스를 받는다 - 요약 항목이 필드
id로 연결되고, 스크롤만 하는 게 아니라 포커스를 옮긴다 - 요약 순서가 화면의 필드 순서와 같다
- 인라인 에러는 라이브 영역이 아니다
- 에러는 빨간색만이 아니라 아이콘과 텍스트로도 표시한다
- 필수 표시에 그 뜻을 적은 설명이 있다
- 어떤 에러 경로에서도 필드 값이 유지된다
- 서버 에러가 클라이언트 에러와 같은 영역에 표시된다
알려진 공백
- 대기 상태를 포함해 필드를 비동기로 검증하는 패턴이 없다.
- 서버 에러 분류, 즉 어떤 코드가 어떤 사용자 메시지에 대응하는지가 문서화되지 않았다.
- 오프라인 제출 실패는 Forms에 설명돼 있지만 메시지가 정해지지 않았다.
- 필드 간 검증("이 두 날짜의 순서가 반대다")은 규칙만 있고 실제 예시가 없다.