GODRICH

인증번호 입력 UI 만들 때 겪는 9가지 상황 — 폼 검증까지 챙긴

인증번호 입력 UI는 6자리 숫자를 한 칸씩 나눠 받는 폼 부품으로, 다음 칸 이동부터 붙여넣기·오류 처리까지 한 번에 챙겨야 동작이 매끄러워집니다. 아홉 개 중 하나는 화면에 아무것도 안 보여도 실제로는 코드를 통째로 채워 넣습니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기순이 아니라 인증번호 입력 폼이 실제로 겪는 순서로 놓았습니다. 입력이 시작되는 흐름(01~04) 다음 확인과 보안(05~06), 재전송과 자동입력(07~08), 마지막으로 접근성(09)입니다. 화면마다 자리 하나씩 붙잡고 그 순간에 필요한 속성과 이벤트만 뽑아 실었습니다.

01다음 칸 자동 이동

숫자를 입력하면 커서가 자동으로 다음 칸으로 넘어갑니다. 회원가입 인증이나 로그인 2단계 인증처럼 6자리를 연속으로 받는 화면에 씁니다.

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자리 코드를 붙여넣으면 한 번에 칸마다 나눠 들어갑니다. 숫자가 아닌 글자는 걸러내고 남는 자리는 그대로 둡니다.

clipboard6칸 분배숫자 외 무시
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가 필요 없습니다.

inputmode numericpattern [0-9]*텐키
<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오류 시 흔들리고 초기화

잘못된 코드를 넣으면 칸 전체가 흔들리고 자동으로 비워집니다. 재입력을 유도하면서 화면에는 상태 문구를 따로 남겨 스크린 리더에도 실패 사실이 전달됩니다.

흔들림 300ms자동 비움빨간 테두리
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 인증번호를 짧은 시간에 여러 번 요청하는 클릭을 막습니다.

60초비활성카운트다운
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와 안드로이드 양쪽에서 동작합니다.

autocompleteone-time-codeOS 감지
<input
  type="text"
  inputmode="numeric"
  autocomplete="one-time-code"
  maxlength="1"
>

09모션 축소 시 정적 포커스

모션을 줄인 설정에서는 애니메이션 없이 다음 칸에 바로 포커스가 옮겨갑니다. 전정기관에 민감한 사용자를 배려하면서 자동 이동이라는 기능 자체는 그대로 남깁니다.

prefers-reduced-motion즉시 이동정적 포커스
@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자리 백업 코드에도 같은 구조를 재사용할 수 있습니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.