인증번호 입력 UI 만들 때 겪는 9가지 상황 — 폼 검증까지 챙긴
인증번호 입력 UI는 6자리 숫자를 한 칸씩 나눠 받는 폼 부품으로, 다음 칸 이동부터 붙여넣기·오류 처리까지 한 번에 챙겨야 동작이 매끄러워집니다. 아홉 개 중 하나는 화면에 아무것도 안 보여도 실제로는 코드를 통째로 채워 넣습니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 다음 칸 자동 이동
- 02 백스페이스 이전 칸 포커스
- 03 코드 전체 붙여넣기 분배
- 04 모바일 숫자 키패드 강제
- 05 마스킹 표시 토글
- 06 오류 시 흔들리고 초기화
- 07 재전송 타이머
- 08 SMS OTP 자동채움
- 09 모션 축소 시 정적 포커스
아홉 개는 인기순이 아니라 인증번호 입력 폼이 실제로 겪는 순서로 놓았습니다. 입력이 시작되는 흐름(01~04) 다음 확인과 보안(05~06), 재전송과 자동입력(07~08), 마지막으로 접근성(09)입니다. 화면마다 자리 하나씩 붙잡고 그 순간에 필요한 속성과 이벤트만 뽑아 실었습니다.
01다음 칸 자동 이동
숫자를 입력하면 커서가 자동으로 다음 칸으로 넘어갑니다. 회원가입 인증이나 로그인 2단계 인증처럼 6자리를 연속으로 받는 화면에 씁니다.
boxes.forEach(function (b, i) {
b.addEventListener('input', function () {
b.value = b.value.replace(/[^0-9]/g, '').slice(-1);
if (b.value && boxes[i + 1]) boxes[i + 1].focus({ preventScroll: true });
});
});
02백스페이스 이전 칸 포커스
빈 칸에서 백스페이스를 누르면 이전 칸으로 돌아가 지웁니다. 인증번호를 잘못 눌렀을 때 처음부터 다시 지우지 않아도 됩니다.
boxes.forEach(function (b, i) {
b.addEventListener('keydown', function (e) {
if (e.key === 'Backspace' && !b.value && boxes[i - 1]) {
boxes[i - 1].focus({ preventScroll: true });
}
});
});
03코드 전체 붙여넣기 분배
메일이나 메시지 앱에서 복사한 6자리 코드를 붙여넣으면 한 번에 칸마다 나눠 들어갑니다. 숫자가 아닌 글자는 걸러내고 남는 자리는 그대로 둡니다.
pin.addEventListener('paste', function (e) {
var t = (e.clipboardData || window.clipboardData)
.getData('text').replace(/[^0-9]/g, '');
if (!t) return;
e.preventDefault();
t.slice(0, 6).split('').forEach(function (ch, k) {
if (boxes[k]) boxes[k].value = ch;
});
});
04모바일 숫자 키패드 강제
모바일에서는 문자 키패드 대신 숫자 키패드가 바로 뜹니다. inputmode와 pattern 속성만으로 되고 별도 JS가 필요 없습니다.
<input
type="text"
inputmode="numeric"
pattern="[0-9]*"
maxlength="1"
autocomplete="one-time-code"
aria-label="인증번호 1번째 자리"
>
05마스킹 표시 토글
눈 아이콘을 누르면 입력된 숫자가 점 대신 그대로 보입니다. 은행 앱 계좌 비밀번호나 결제 PIN처럼 값을 확인하고 싶을 때 씁니다.
var masked = true;
toggle.addEventListener('click', function () {
masked = !masked;
boxes.forEach(function (b) {
b.type = masked ? 'password' : 'text';
});
toggle.textContent = masked ? '가려진 숫자 보기' : '다시 가리기';
});
06오류 시 흔들리고 초기화
잘못된 코드를 넣으면 칸 전체가 흔들리고 자동으로 비워집니다. 재입력을 유도하면서 화면에는 상태 문구를 따로 남겨 스크린 리더에도 실패 사실이 전달됩니다.
confirmBtn.addEventListener('click', function () {
var code = boxes.map(function (b) { return b.value; }).join('');
if (code.length < 6 || code !== realCode) {
pin.classList.add('is-error');
boxes.forEach(function (b) { b.value = ''; });
}
});
07재전송 타이머
재전송 버튼은 60초 카운트다운 뒤에야 다시 누를 수 있습니다. SMS 인증번호를 짧은 시간에 여러 번 요청하는 클릭을 막습니다.
var left = 60;
function tick() {
resend.textContent = left > 0 ? left + '초 후 재전송' : '재전송';
resend.disabled = left > 0;
if (left > 0) { left--; setTimeout(tick, 1000); }
}
tick();
08SMS OTP 자동채움
문자로 온 인증번호를 OS가 감지해 자동으로 채워 넣습니다. autocomplete="one-time-code" 속성 하나로 iOS와 안드로이드 양쪽에서 동작합니다.
<input
type="text"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="1"
>
09모션 축소 시 정적 포커스
모션을 줄인 설정에서는 애니메이션 없이 다음 칸에 바로 포커스가 옮겨갑니다. 전정기관에 민감한 사용자를 배려하면서 자동 이동이라는 기능 자체는 그대로 남깁니다.
@media (prefers-reduced-motion: reduce) {
.pin__ghost, .pin__caret, .pin__signal {
animation: none !important;
transition: none !important;
}
.pin.is-demo .pin__ghost { opacity: 1; }
}
어디서 깨지는가 — 함정
칸마다 autocomplete="one-time-code"를 걸면 대부분의 최신 브라우저에서 문자 인증코드를 자동으로 채워주지만, 일부 사파리·구형 안드로이드 브라우저는 6칸으로 나뉜 자동 채움을 종종 놓칩니다. web.dev의 OTP 폼 패턴 문서는 화면에는 6칸을 그대로 두되 자동 채움 자체는 화면 밖에 숨긴 input 하나에만 받고, 값이 들어오면 JS로 6칸에 나눠 넣는 절충을 권합니다. 이 zip의 08 항목은 그 절충 없이 칸마다 속성만 걸어 두었는데, 실제 서비스에 붙이기 전에 자동 채움이 기기 몇 대에서 실제로 성공하는지 직접 눌러 보고 넘어가는 편이 안전합니다. 압축은 39kzjrs9로 열리며, 09까지 아홉 항목 전부가 그 안에 완결본으로 들어 있습니다. 붙여넣기 이벤트도 마찬가지 함정이 있어서, 칸마다 걸면 여러 번 겹쳐 실행될 수 있습니다. 03 항목처럼 감싸는 컨테이너 하나에만 paste 리스너를 걸어야 코드가 한 번만 분배됩니다.
접근성
아홉 개 모두 운영체제의 동작 줄이기 옵션에 반응합니다. 01·04·09는 캐럿과 신호가 사라지고 칸은 즉시 채워진 상태로만 보이며, 05는 눈 아이콘 깜빡임 없이 마스킹 상태만 바뀌고, 06은 흔들림 없이 오류 문구만 화면에 남습니다. 칸마다 aria-label로 몇 번째 자리인지 읽어주고, 06의 오류 문구는 role="status"로 스크린 리더가 따로 안내를 받습니다. 폼 검증을 시각 효과로 강조하고 싶다면 모달 애니메이션 아홉 가지에서 오류 흔들림과 페이드를 참고할 수 있고, 사이트 운영 방향은 소개 페이지에 정리해 두었습니다.
FAQ
PIN 입력에 꼭 6칸으로 나눠야 합니까?
아니요. <input maxlength="6"> 하나로도 받을 수 있지만, 자리마다 칸을 나누면 남은 숫자가 몇 개인지 한눈에 보이고 자동 이동·백스페이스 같은 동작을 자리별로 다르게 다루기 쉬워집니다.
autocomplete="one-time-code"는 어느 브라우저에서 됩니까?
사파리·크롬·엣지 최신 버전은 autocomplete="one-time-code"를 인식해 문자로 온 코드를 채워 줍니다. 정확한 브라우저·OS 조합은 caniuse.com에서 확인하고, 함정 항목에 적은 숨은 input 방식을 함께 테스트하는 편이 안전합니다.
여섯 자리가 아니라 네 자리 코드에도 씁니까?
됩니다. 이 zip의 리액트 컴포넌트는 length 값만 바꾸면 칸 수가 그대로 따라가므로 4자리 은행 PIN이나 8자리 백업 코드에도 같은 구조를 재사용할 수 있습니다.