본인인증 화면 UI 9가지 — 그대로 복사하는 인증 카드
본인인증 화면은 사용자가 본인임을 증명하기 위해 권한 요청부터 인증번호 입력, 로그아웃 확인까지 거치는 일련의 UI 조각을 말합니다. 아래 아홉 개는 서버 쪽 인증 로직이 아니라 이 흐름에서 반복되는 화면 패턴만 애니메이션과 함께 코드로 옮긴 것입니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 권한 요청 모달
- 02 캡차 확인 체크박스
- 03 인증번호 입력 그리드
- 04 보안 질문 확인 카드
- 05 계정 복구 방법 선택 카드
- 06 온보딩 위저드 스텝 카드
- 07 연결된 계정 목록
- 08 세션 만료 임박 배너
- 09 로그아웃 확인 카드
아홉 개는 인증을 처음 요청받는 순간부터 로그아웃하는 순간까지, 사용자가 실제로 거치는 순서대로 놓았습니다. 권한을 요청하고 사람인지 확인한 뒤 인증번호를 입력하고, 받지 못하면 보안 질문이나 복구 수단으로 넘어갑니다. 인증이 끝나면 온보딩과 계정 연결로 이어지고, 세션이 끊기기 전 경고와 로그아웃 확인으로 마무리됩니다.
01권한 요청 모달
카메라·위치 같은 접근 권한을 물을 때 아이콘이 통통 튀며 나타나고, 허용·거부 버튼 중 허용 쪽만 파란 배경으로 강조됩니다. 본인인증 전 카메라·마이크 접근을 요청하는 화면이나 앱 첫 실행 시 알림 권한 안내에 씁니다.
(function(){
var scene = document.querySelector('.perm-scene');
var modal = scene.querySelector('.perm-scene__modal');
var deny = scene.querySelector('.perm-scene__btn--ghost');
var allow = scene.querySelector('.perm-scene__btn--primary');
function close(){ scene.classList.remove('is-demo'); scene.classList.add('is-closed'); }
deny.addEventListener('click', close);
allow.addEventListener('click', close);
modal.addEventListener('keydown', function(e){
if (e.key === 'Escape') close();
});
}());
02캡차 확인 체크박스
체크박스를 누르면 로딩 스피너가 잠깐 돌다가 초록 체크가 스프링처럼 튀어 오르고, 뒤이어 '확인됨' 배지가 떠오릅니다. 인증번호 발송 전 사람인지 확인하는 단계나 무료 체험 신청 폼의 스팸 방지에 씁니다.
.captcha__input:checked ~ .captcha__box .captcha__check {
opacity: 1;
transform: scale(1);
transition: opacity $dur-quick $ease-out, transform $dur-quick $ease-pop;
}
.captcha__input:checked ~ .captcha__box { border-color: $color; }
.captcha__input:focus-visible ~ .captcha__box { outline: 2px solid $color; outline-offset: 2px; }
@keyframes capt-check {
0%, 28% { opacity: 0; transform: scale(.4); }
40% { opacity: 1; transform: scale(1.15); }
46%, 84% { opacity: 1; transform: scale(1); }
92%, 100% { opacity: 0; transform: scale(.4); }
}
03인증번호 입력 그리드
숫자 칸 6개가 나란히 있고, 한 칸에 입력하면 자동으로 다음 칸으로 넘어가며 칸마다 테두리가 파랗게 반짝입니다. 문자로 받은 인증번호 6자리 입력창이나 2단계 인증 코드 입력 화면에 씁니다.
boxes.forEach(function(box, i){
box.addEventListener('input', function(){
box.value = box.value.replace(/[^0-9]/g, '').slice(0, 1);
if (box.value && boxes[i + 1]) boxes[i + 1].focus({ preventScroll: true });
});
box.addEventListener('keydown', function(e){
if (e.key === 'Backspace' && !box.value && boxes[i - 1]) boxes[i - 1].focus({ preventScroll: true });
});
box.addEventListener('paste', function(e){
e.preventDefault();
var digits = (e.clipboardData.getData('text') || '').replace(/[^0-9]/g, '').split('');
boxes.forEach(function(b, j){ b.value = digits[j] || ''; });
(boxes[Math.min(digits.length, boxes.length) - 1] || boxes[0]).focus({ preventScroll: true });
});
});
04보안 질문 확인 카드
미리 등록한 질문 하나가 카드에 뜨고, 답을 입력하면 입력칸 테두리가 빨강에서 베이지로 서서히 식듯이 바뀝니다. OTP 를 못 받을 때의 대체 인증 단계나 관리자 계정의 2차 확인 화면에 씁니다.
.sq-card__input:focus-visible { border-color: $subject-blue; box-shadow: 0 0 0 3px rgba($subject-blue, .25); }
@keyframes sq-panel {
0%, 8% { background: mix($color, white, 24%); box-shadow: 0 0 0 3px rgba($color, .3), 0 0 34px 18px rgba($color, .3), $shadow-raised; }
23%, 77% { background: $stage-paper; box-shadow: $shadow-raised; }
92%, 100% { background: mix($color, white, 24%); box-shadow: 0 0 0 3px rgba($color, .3), 0 0 34px 18px rgba($color, .3), $shadow-raised; }
}
@keyframes sq-input {
0%, 8% { border-color: $color; box-shadow: 0 0 0 4px rgba($color, .22), 0 0 20px 10px rgba($color, .25); }
23%, 77% { border-color: #d8d2c4; box-shadow: none; }
92%, 100% { border-color: $color; box-shadow: 0 0 0 4px rgba($color, .22), 0 0 20px 10px rgba($color, .25); }
}
05계정 복구 방법 선택 카드
이메일·문자·보안 질문 세 옵션이 라디오 카드로 나열되고, 고르면 선택된 카드만 배경이 어두운 화면 위에서 밝게 떠오릅니다. 비밀번호를 잊었을 때 복구 수단을 고르는 첫 화면이나 계정 잠금 해제 전 확인 단계에 씁니다.
.recovery__card:has(.recovery__input:checked) {
background: mix($color, $stage-ink, 26%);
border-color: $color;
box-shadow: 0 0 0 3px rgba($color, .3), 0 0 24px 12px rgba($color, .28);
transform: scale(1.04);
}
.recovery__card:has(.recovery__input:focus-visible) { outline: 2px solid $color; outline-offset: 2px; }
.recovery.is-demo .recovery__card { animation: picker-cycle $duration $easing infinite; }
.recovery.is-demo .recovery__card:nth-child(1) { animation-delay: 0ms; }
.recovery.is-demo .recovery__card:nth-child(2) { animation-delay: 660ms; }
.recovery.is-demo .recovery__card:nth-child(3) { animation-delay: 1320ms; }
06온보딩 위저드 스텝 카드
인증이 끝난 뒤 프로필을 채우는 카드로, 다음 버튼을 누르면 지금 카드가 왼쪽으로 밀려나며 다음 카드가 파스텔 색으로 슬라이드해 들어옵니다. 회원가입 인증 직후 이어지는 프로필 설정 단계나 첫 실행 온보딩의 정보 입력 카드에 씁니다.
(function(){
var track = document.querySelector('.wizard__track');
var dots = Array.prototype.slice.call(document.querySelectorAll('.wizard__dot'));
var btn = document.querySelector('.wizard__next');
var step = 0;
btn.addEventListener('click', function(){
step = step === 0 ? 1 : 0;
track.style.transform = 'translateX(' + (step === 0 ? '0%' : '-50%') + ')';
dots.forEach(function(d, i){ d.classList.toggle('is-active', i === step); });
btn.textContent = step === 0 ? 'Next' : 'Back';
});
}());
07연결된 계정 목록
구글·깃허브 같은 로그인 수단이 줄마다 나열되고, 연결 스위치를 누르면 토글이 미끄러지듯 넘어가며 줄 배경이 옅은 색으로 물듭니다. 설정 화면의 소셜 로그인 연결 관리나 여러 계정을 하나로 묶는 통합 로그인 대시보드에 씁니다.
.accounts__input:checked ~ .accounts__track { background: $color; }
.accounts__input:checked ~ .accounts__track .accounts__knob { transform: translateX(14px); }
.accounts__input:focus-visible ~ .accounts__track { outline: 2px solid $color; outline-offset: 2px; }
.accounts__row:has(.accounts__input:checked) { background: rgba($subject-mint, .18); }
@keyframes acct-knob-cycle {
0%, 8% { transform: translateX(0); }
14%, 36% { transform: translateX(14px); }
42%, 100% { transform: translateX(0); }
}
08세션 만료 임박 배너
화면 위쪽에서 배너가 미끄러져 내려오며 남은 시간이 초 단위로 줄어들고, 연장 버튼을 누르면 남은 시간이 다시 30초로 채워집니다. 은행·결제 앱의 자동 로그아웃 전 경고나 관리자 콘솔의 유휴 세션 알림에 씁니다.
(function(){
var cnt = document.querySelector('.timeout-banner__count'),
btn = document.querySelector('.timeout-banner__extend'),
n = 30,
timer = setInterval(function(){
n--; if (n < 0) n = 0; cnt.textContent = n;
}, 1000);
btn.addEventListener('click', function(){
n = 30; cnt.textContent = n;
});
}());
09로그아웃 확인 카드
확인 카드가 뜨는 순간 살짝 흔들려 눈길을 끌고, 정말 나가기를 누르면 카드 배경이 어두운 빨강으로 짙게 물듭니다. 실수로 누른 로그아웃을 한 번 더 확인하는 카드나 여러 기기 로그아웃을 경고하는 계정 설정 화면에 씁니다.
(function(){
var card = document.querySelector('.signout-card'),
cancel = card.querySelector('.signout-card__btn--cancel'),
danger = card.querySelector('.signout-card__btn--danger');
cancel.addEventListener('click', function(){ card.classList.remove('is-confirmed'); });
danger.addEventListener('click', function(){ card.classList.add('is-confirmed'); });
}());
어디서 깨지는가 — 자동 포커스와 스크롤
인증번호 입력 그리드(03)를 만들면서 실제로 걸린 함정은 포커스 이동과 스크롤이었습니다. 칸을 채울 때마다 다음 인풋으로 .focus()를 그냥 호출했더니, 브라우저가 기본값으로 그 요소를 화면 안으로 밀어 넣으려 했습니다. 데스크톱 화면에서는 표가 안 났지만 모바일 폭 320px 로 좁혀 인풋이 스크롤 컨테이너 안에 들어가자, 한 자리씩 입력할 때마다 화면이 미세하게 들썩였습니다.
이 들썩임이 데모 하나로 끝나지 않는 이유가 있습니다. 이 사이트는 아홉 개 데모를 홈 목록과 글 안 격자에 iframe 으로 심어 두는데, iframe 안에서 .focus() 가 기본 동작대로 스크롤을 일으키면 그 스크롤이 부모 페이지까지 번집니다. 방문자가 목록을 위로 넘기고 있는데 격자 안 인증번호 칸이 포커스를 받을 때마다 페이지가 아래로 끌려 내려가는 식입니다. 아홉 개 가운데 스크립트에서 .focus() 를 부르는 것은 03 하나뿐이라 고칠 곳은 세 줄이었지만, 자동 이동·백스페이스 되돌아가기·붙여넣기 분배 세 경로가 전부 같은 호출을 지나므로 한 곳만 고치면 나머지 두 경로에서 다시 들썩입니다. { preventScroll: true } 옵션을 넣어 이미 보이는 요소 사이를 옮겨 다닐 때는 스크롤을 막았고, 이 처리는 위 03 코드와 압축 비밀번호 zkv9xjxt로 여는 zip 안 소스 아홉 개 전부에 그대로 들어 있습니다. 이후로는 화면 안에서 다음 요소로 넘어가는 모든 .focus() 호출에 같은 옵션을 붙이는 게 이 사이트의 규칙이 됐습니다. 옵션 하나가 어떤 경우에 스크롤을 막는지는 MDN focus() preventScroll 문서에 정리돼 있습니다.
접근성
아홉 개 전부 prefers-reduced-motion: reduce에서 장식용 자동 재생을 끄되 실제 상태 값은 그대로 남깁니다. 권한 모달(01)은 role=alertdialog와 aria-modal로 보조기기에 모달임을 알리고 Esc 로 닫힙니다. 캡차(02)의 체크 배지와 인증번호 그리드(03)의 자동 이동은 reduced-motion 에서 튀는 움직임만 사라지고 체크 상태·inputmode=numeric 같은 실제 정보 전달은 남습니다. 보안 질문(04)은 aria-label로 입력칸의 용도를 알리고, 복구 카드(05)는 role=radiogroup과 라디오 버튼 상태로 지금 선택된 항목을 스크린 리더에 전달하며, 세션 배너(08)는 role=alert로 남은 시간이 줄어드는 것을 중간에 끼어들어서라도 읽어 줍니다.
본인인증 다음 단계로 이어지는 로그인·회원가입 카드도 같은 계정 흐름을 다루고, 이 사이트가 다루는 범위는 소개에 있습니다.
FAQ
실제 서버 인증(OTP 검증·세션 관리)까지 포함하나요?
아니요, 화면 UI 와 애니메이션 패턴만 담았습니다. 인증번호가 맞는지 확인하거나 세션 만료를 판단하는 서버 쪽 로직은 각자의 백엔드에 맞춰 직접 붙여야 합니다.
React 버전도 실제로 상태가 바뀌나요?
네, 상태가 있는 항목은 전부 useState로 들고 있습니다. 인증번호 그리드의 입력 값, 온보딩의 현재 단계, 로그아웃 카드의 확인 여부가 그렇고, 변수 몇 개는 props 로 받아 CSS 사용자 지정 속성으로 넘깁니다.
인증번호 입력 그리드는 실제 전화번호로 문자를 보내나요?
아니요, 칸에 숫자를 채우고 다음 칸으로 넘어가는 동작만 실제로 작동합니다. 문자 발송이나 서버 쪽 인증번호 검증은 포함돼 있지 않고, 화면에 보이는 자동 이동과 붙여넣기 분배 로직만 그대로 코드에 들어 있습니다.