07.1 잘못된 입력입니다는 왜 나쁜 메시지일까요?
검증이 아무리 정확해도, 그 결과를 사용자에게 어떻게 전하느냐가 폼의 완성도를 좌우해요. 제일 흔한 나쁜 예가 잘못된 입력입니다 한 줄이에요. 뭐가 잘못됐는지, 어떻게 고쳐야 하는지 하나도 안 알려주거든요.
좋은 에러 메시지는 두 가지를 지켜요. 구체적이고 실행 가능해야 해요. 잘못된 입력입니다 대신 비밀번호는 8자 이상이어야 해요처럼, 무엇이 문제고 어떻게 하면 되는지가 한 줄에 들어 있어야 하죠. 사용자는 메시지를 읽자마자 바로 손을 움직일 수 있어야 해요.
몇 개만 비교해볼게요. 이메일 오류보다는 골뱅이(@)가 빠졌어요가 낫고, 형식이 틀렸습니다보다는 휴대폰 번호는 숫자만 11자리로 적어주세요가 나아요. 규칙을 알려주면 사용자가 스스로 고쳐요. 반대로 두루뭉술한 말은 다시 시도하게만 만들 뿐이에요.
07.2 에러 메시지는 어디에 붙여야 눈에 들어올까요?
메시지 내용만큼 위치도 중요해요. 원칙은 문제가 난 칸 바로 옆이에요. 이걸 인라인 에러(inline error, 필드에 붙는 에러)라고 해요. 보통 그 입력 칸 바로 아래에 한 줄로 놔요.
화면 맨 위에 에러를 몰아놓거나 팝업(popup, 떠 있는 창) 하나로 잘못된 부분이 있어요라고만 띄우면, 사용자는 눈을 위아래로 굴리며 어느 칸이 문제인지 스스로 찾아야 해요. 틀린 칸 바로 밑에 답이 있으면 그 수고가 사라지죠. 문제와 설명이 붙어 있을수록 고치기 쉬워요.
07.3 스크린리더 쓰는 사람에게도 에러가 들릴까요?
여기서 놓치기 쉬운 게 있어요. 빨간 글씨는 눈으로 보는 사람에게만 전달돼요. 스크린리더(screen reader, 화면 내용을 소리로 읽어주는 보조 도구)를 쓰는 사람에게는 그 빨간 글씨가 존재하지 않는 거나 마찬가지예요. 그래서 ARIA(에어리아, 보조기술에 의미를 알려주는 속성 묶음)로 연결해줘야 해요.
핵심 속성은 둘이에요. 무효한 칸에는 aria-invalid(이 칸의 값이 유효하지 않음)를 붙이고, 그 에러 문장을 aria-describedby(이 칸을 설명하는 요소의 id)로 연결해요. 그러면 스크린리더가 그 칸에 포커스가 갔을 때 에러 문장까지 같이 읽어줘요.
상황: 이메일 칸과 에러 문단을 id로 이어놓고, 검증 결과에 따라 켜고 끄는 함수를 만들었어요.
<div class="field">
<label for="email">이메일</label>
<input type="email" id="email" aria-describedby="email-err">
<p id="email-err" class="err"></p>
</div>
function showError(input, errId, msg) {
const el = document.getElementById(errId);
if (msg) {
input.setAttribute('aria-invalid', 'true');
el.textContent = msg;
} else {
input.removeAttribute('aria-invalid'); // false 대신 아예 제거
el.textContent = '';
}
}
동작을 보면 에러가 있을 땐 aria-invalid="true"가 붙고, 스크린리더는 이메일 칸을 읽을 때 연결된 email-err 문장을 이어서 읽어줘요. 여기서 한 가지 꼭 짚을 게 있어요. 값이 유효해지면 aria-invalid를 false로 바꾸는 게 아니라 아예 제거해요. 일부 스크린리더는 invalid false를 그대로 소리 내 읽어서 사용자를 헷갈리게 하거든요. 그래서 유효할 땐 속성 자체를 removeAttribute로 빼는 게 안전해요.
07.4 빨간 테두리만으로 충분할까요?
디자인할 때 흔한 실수가 에러를 색깔 하나로만 표현하는 거예요. 칸에 빨간 테두리만 두르는 식이죠. 그런데 색약(색을 구별하기 어려운 시각 특성)이 있는 사람에게는 빨강과 회색이 잘 구분되지 않아요. 스크린리더 사용자에게 색은 아예 전달되지 않고요.
그래서 에러는 여러 신호를 겹쳐서 알려요. 빨간 테두리에 더해 에러 문장(텍스트)을 함께 보여주고, 필요하면 경고 아이콘(icon, 작은 그림 기호)도 곁들이고, 방금 본 aria-invalid까지 붙여요. 색은 거들 뿐이고, 진짜 정보는 글자가 전해야 해요. 그래야 누구에게나 똑같이 전달돼요.
07.5 제출했더니 에러가 우르르, 어디부터 고치죠?
인라인 에러가 잘 돼 있어도, 긴 폼을 다 비운 채 제출 버튼을 눌러버리면 에러가 여러 개 한꺼번에 떠요. 이럴 땐 인라인만으로는 부족해요. 화면 밖으로 스크롤된 칸의 에러는 안 보이니까요. 그래서 제출 시점에는 상단 에러 요약을 같이 띄워요.
요약 상자에는 틀린 칸들의 목록을 넣고, 각 항목을 해당 칸으로 가는 링크로 만들어요. 그리고 제출 직후 포커스를 이 요약 상자로 옮겨서 스크린리더가 바로 읽게 해요.
<div id="summary" role="alert" tabindex="-1" hidden>
<p>다음 항목을 확인해 주세요</p>
<ul id="summary-list"></ul>
</div>
function showSummary(errors) { // errors: [{id, label, msg}]
const box = document.getElementById('summary');
document.getElementById('summary-list').innerHTML = errors
.map(e => `<li><a href="#${e.id}">${e.label}: ${e.msg}</a></li>`).join('');
box.hidden = false;
box.focus(); // 요약 상자로 포커스 이동
}
동작을 보면 제출하자마자 상단에 이메일: 형식이 아니에요, 비밀번호: 8자 이상 같은 목록이 뜨고, 각 줄을 누르면 그 칸으로 바로 이동해요. 상자에 붙인 role="alert"는 스크린리더에게 이건 지금 알려야 할 중요 메시지라고 표시하는 역할이라, 포커스가 옮겨오면 내용을 곧바로 읽어줘요. tabindex="-1"은 원래 포커스를 못 받는 요소에 포커스를 줄 수 있게 열어두는 값이고요.
07.6 오늘 배운 에러 메시지 설계, 이렇게 정리돼요
에러 메시지는 검증의 마무리이자 사용자와 만나는 지점이에요. 챙길 것만 짚을게요. 첫째, 메시지는 구체적이고 실행 가능하게. 무엇이 틀렸고 어떻게 고치는지를 담아요. 둘째, 위치는 문제 난 칸 바로 옆, 인라인으로요.
셋째, aria-invalid와 aria-describedby로 스크린리더에도 에러를 전하고, 유효해지면 aria-invalid는 false 대신 제거해요. 넷째, 색 하나에 기대지 말고 글자와 아이콘을 겹쳐요. 다섯째, 제출 시엔 상단 요약과 포커스 이동으로 어디부터 고칠지 안내해요.
정리하면 좋은 에러 메시지는 친절한 안내문이에요. 다그치는 대신 길을 알려주는 거죠. 여러분 폼의 에러 문구를 소리 내 한 번 읽어보세요. 그 문장만 듣고도 뭘 고쳐야 할지 알겠다면, 잘 만든 거예요.