1. 들어가며
- OTP 입력창은 인증번호를 입력할 때 가장 흔히 마주하는 UI다. 각 칸에 한 글자씩 입력되도록 설계되어 있어, 사용자는 키보드로 빠르게 인증을 완료할 수 있다.

[ apple의 OTP 입력창 동작 예시 ]
- 그러나 이 단순해 보이는 UI를 구현할 때 간과하기 쉬운 부분이 있다. 바로 Delete 키의 처리이다.
- 대게 입력을 지울 때 Backspace 키를 떠올리지만, Delete 키도 입력 삭제에 사용되므로,
- Delete 키가 작동하지 않으면 해당 키를 사용하는 사용자는 “버튼이 먹지 않는다”는 의문을 가질 수 있다.
그럼, OTP 입력 UI에서 Delete 키를 어떻게 구현해야 할까? 단순히 input 이벤트만으로 처리하면 될까?
- input 이벤트만으로는 부족하다!
- OTP 입력창에서는 값이 변경되었을 때(
oninput
)와 키를 누르는 즉시(onkeydown
) 모두 처리해야 한다. - 특히
oninput
은 실제 값이 변경된 후에 발생하기 때문에, 빈 입력 칸에서는 Delete나 Backspace를 눌러도 이벤트가 발생하지 않는다. - 반면
onkeydown
은 키를 누르는 순간 발생해, 빈 칸에서도 Delete/Backspace 키를 감지할 수 있다. - 따라서 두 이벤트를 결합해야만 사용자가 입력과 삭제 시 일관된 경험을 할 수 있다.
2. OTP 입력기를 만들어보자!
1) Delete vs Backspace: 이벤트 흐름과 inputType
- 키 입력과 관련된 주요 이벤트는 다음과 같다:
keydown
- 키를 누를 때 발생하는 이벤트
- 값 변경 이전에 실행되어, 빈 입력 칸에서도 삭제 키를 감지할 수 있음.
input
- 값이 실제로 변경될 때 발생하는 이벤트
- 사용자가 입력한 후에 발생하지만, 입력 칸이 빈 상태에서는 발생하지 않음.
keyup
- 키를 뗄 때 발생하는 이벤트

- 이 중 input 이벤트는 어떤 동작이 발생했는지
inputType
속성으로 구분할 수 있다.
inputType | 동작 종류 |
---|---|
insertText |
- 문자 입력 |
insertFromPaste |
- 붙여넣기 |
deleteByCut |
- 잘라내기 |
deleteContentBackward |
- Backspace 누름 |
deleteContentForward |
- Delete 누름 |
OTP 입력창에서 삭제 기능(Delete, Backspace)을 구현할 때,
deleteContentBackward
와deleteContentForward
타입을 사용하면 되겠군!
- 아쉽게도 input 이벤트만으로는 OTP 입력창을 완벽하게 구현할 수 없다.
- 그 이유는, 입력값이 없는 상태에서 Delete나 Backspace를 누르면 input 이벤트가 발생하지 않기 때문이다.
- 즉, oninput은 입력 값이 변경되었을 때만 의미가 있으므로, 빈 입력칸에서 삭제 시에는 아무런 반응을 하지 않는다.
- 그래서 OTP 입력창에서는 input 이벤트와 함께 keydown 이벤트를 반드시 사용해야 한다!
2) OTP 입력창 구현해보기
(1) OTP 입력창은 어떻게 동작해야할까?
- OTP 입력창 구현 시 고려해야 할 요구사항은 다음과 같다.
요구사항 | 설명 |
---|---|
각 칸에 한 글자씩 입력 | - 여러 글자가 입력되지 않도록 제어하여, 각 칸은 단일 문자만 저장해야 함. |
입력 후 자동 포커스 이동 | - 사용자가 문자를 입력하면 자동으로 다음 칸으로 포커스가 이동되어 빠른 입력이 가능해야 함. |
삭제 시 다른 동작 적용 | - Backspace 키: 현재 입력 칸의 값을 삭제하고 포커스를 왼쪽(이전 칸)으로 이동해야 함. - Delete 키: 현재 입력 칸의 값을 삭제하고 포커스를 오른쪽(다음 칸)으로 이동해야 함. |
붙여넣기(Paste) 대응 | - 사용자가 전체 인증번호를 한 번에 붙여넣을 때, 각 칸에 순서대로 값이 채워져야 함. |
다른 칸으로 포커스 이동 | - ArrowLeft 키: 이전 칸으로 포커스를 이동해야 함. - ArrowRight 키: 다음 칸으로 포커스를 이동해야 함. |
- 위의 기능들을 구현하기 위해 두 가지 이벤트를 조합하여 사용해야 한다.
이벤트 | input 이벤트 | keydown 이벤트 |
---|---|---|
이벤트 발생 시점 |
사용자가 값을 입력한 후, 실제 값이 변경될 때 발생. | 키를 누르는 순간 발생하여, 값이 변경되기 전에 실행됨. |
용도 | 입력된 값이 여러 문자인 경우 한 글자만 남기거나, 값을 가공하여 다음 칸으로 포커스 이동. |
Delete/Backspace와 같이 값이 없는 상태에서도 작동해야 하는 키 이벤트 처리. |
유의점 | 빈 입력 칸에서는 이벤트가 발생하지 않음. | preventDefault()를 통해 기본 동작을 차단하고, 직접 값 삭제 및 포커스 이동을 제어. |
- 즉, OTP 입력창에서는
input
와keydown
두 이벤트를 조합해야 입력과 삭제를 모두 구현할 수 있다. - 그럼 이제 실제로 어떻게 구현했는지 코드 단위로 살펴보자!
(1) HTML 구조
- HTML 구조는 간단한데, 하나의 컨테이너에 OTP용 입력창을 4개 배치하면 된다.
- 모바일 환경에서는, 값 입력시 숫자 키패드가 노출되도록
inputmode
를 "numeric"로 설정한다.

<div id="otp">
<input inputmode="numeric" />
<input inputmode="numeric" />
<input inputmode="numeric" />
<input inputmode="numeric" />
</div>
(2) JS 코드
- OTP 입력창에
input
,keydown
,paste
이벤트를 등록하여 다음과 같이 구현한다.
const inputs = document.querySelectorAll('#otp input');
inputs.forEach((input, i) => {
input.addEventListener('input', (e) => onInput(input, e, i));
input.addEventListener('keydown', (e) => onKeyDown(input, e, i));
input.addEventListener('paste', (e) => onPaste(e, i));
});
(2-1) onInput 함수
- 사용자가 입력한 값 중 숫자만 허용하거나, 입력창에 한 자리수 이상 입력시 최근에 입력한 문자만 허용한다.
function onInput(input, e, i) {
const value = input.value;
/* [a] 숫자 필터링: 숫자가 아닌 문자는 제거 */
const cleanedNumber = value.replace(/\D/g, '');
if (cleanedNumber.length === 0) {
input.value = '';
return;
}
/* [b] 커서 위치 결정: 입력된 숫자 중 사용자가 입력한 위치를 기준으로 선택 */
const cursorPosition = input.selectionStart ?? cleanedNumber.length;
/* [c] 방금 입력한 값 선택: 커서 직전에 위치한 숫자를 선택, 없으면 첫 번째 숫자 사용 */
const insertedValue = cleanedNumber[cursorPosition - 1] || cleanedNumber[0];
input.value = insertedValue;
/* [d] 입력 타입에 따라 추가 동작 실행 (여기서는 insertText만 처리) */
if (e.inputType === 'insertText') {
onInsertText(input, insertedValue, i);
}
}
코드번호 | 설명 |
---|---|
[a] 숫자 필터링 및 유효성 검사 | 입력값에서 숫자가 아닌 문자를 모두 제거하여 순수한 숫자만 남긴다. 만약 숫자가 없다면 입력 필드를 비워 실행을 중단한다. |
[b] 커서 위치 결정 | input.selectionStart를 사용해 커서의 위치를 파악한다. 이 값을 사용해 최근 입력한 숫자를 선택할 수 있도록 한다. |
[c] 방금 입력한 값 선택 | 커서 바로 앞 숫자를 선택하며, 만약 해당 위치에 값이 없으면 첫 번째 숫자를 사용한다. |
[d] 이벤트 처리 | insertText인 경우에만 onInsertText() 함수를 호출하여 추가 처리한다. |
(2-2) onInsertText 함수
- 값이 여러 문자가 입력될 경우 마지막 문자만 남기고, 입력 후 다음 칸으로 포커스를 자동으로 이동시킨다.
function onInsertText(input, value, i) {
/* 값이 없는 경우 함수 종료 */
if (!value) return;
/* 한 글자만 유지하도록 처리 후, 다음 입력칸으로 포커스 이동 */
if (i < inputs.length - 1) inputs[i + 1].focus();
}
(2-3) keydown 이벤트
- Delete, Backspace 키 클릭시 값을 제거하고, 화살표 키 클릭시 포커스를 이동시킨다.
function onKeyDown(input, e, i) {
switch(e.code) {
/* [a] Delete 키 처리: 기본 삭제 동작 차단 후 onDelete 실행 */
case 'Delete':
e.preventDefault();
onDelete(input, i);
break;
/* [b] Backspace 키 처리: 기본 삭제 동작 차단 후 onBackspace 실행 */
case 'Backspace':
e.preventDefault();
onBackspace(input, i);
break;
/* [c] ArrowLeft 키 처리: 이전 입력 칸으로 포커스 이동 */
case 'ArrowLeft':
if (i > 0) inputs[i - 1].focus();
break;
/* [d] ArrowRight 키 처리: 다음 입력 칸으로 포커스 이동 */
case 'ArrowRight':
if (i < inputs.length - 1) inputs[i + 1].focus();
break;
}
}
코드 번호 | 설명 |
---|---|
[a] Delete 키 처리 | - 사용자가 Delete 키를 누르면 onDelete() 를 실행한다. |
[b] Backspace 키 처리 | - 사용자가 Delete 키를 누르면 onBackspace() 를 실행한다. |
[c] ArrowLeft (왼쪽 화살표) 키 처리 | - inputs[i - 1].focus() 를 호출하여, 바로 이전 인풋 필드로 포커스를 이동시킨다. |
[d] ArrowRight (오른쪽 화살표) 키 처리 | - inputs[i + 1].focus() 를 호출하여, 바로 다음 인풋 필드로 포커스를 이동시킨다. |
- 단, Delete와 Backspace 동작을 처리할 때
preventDefault()
를 호출해 기본 동작을 막아야 한다. - 만약 기본 동작을 막지 않으면, 브라우저의 내장 삭제 기능이 실행되어 포커스 이동이나 값 삭제가 중복 발생할 수 있다.
(2-4) onDelete & onBackspace 함수
- Delete, Backspace키가 눌리면 현재 필드의 내용을 지운다.
- 단 Delete키의 경우 값 삭제 후 포커스가 다음 칸으로 이동하고, Backspace키는 이전 칸으로 이동한다.
function onDelete(input, i) {
/* 현재 입력 칸의 값 삭제 */
if (input.value) input.value = '';
/* Delete 키는 다음 칸으로 포커스 이동 */
inputs[i + 1]?.focus();
}
function onBackspace(input, i) {
/* 현재 입력 칸의 값 삭제 */
if (input.value) input.value = '';
/* Backspace 키는 이전 칸으로 포커스 이동 */
inputs[i - 1]?.focus();
}
(2-5) onPaste 함수
- 사용자가 붙여넣기를 하면, 붙여넣은 숫자들이 순서대로 각 입력 필드에 들어간다.
function onPaste(e, i) {
/* [a] 붙여넣기 이벤트 기본 동작 차단 및 붙여넣은 텍스트에서 숫자 외 문자를 제거 */
e.preventDefault();
const pasteText = (e.clipboardData || window.clipboardData).getData('text');
/* [b] 붙여넣은 숫자들을 순서대로 각 입력 필드에 채움 */
pasteText.split('').forEach((char, idx) => {
if (i + idx < inputs.length) {
inputs[i + idx].value = char;
}
});
/* [c] 붙여넣은 후, 마지막으로 채워진 입력 칸으로 포커스 이동 */
const nextIndex = Math.min(inputs.length - 1, i + pasteText.length);
inputs[nextIndex].focus();
}
코드 번호 | 설명 |
---|---|
[a] 클립보드 데이터 읽기 | - 클립보드에 있는 텍스트 데이터를 가져와 pasteText 에 저장한다. |
[b] 붙여넣은 텍스트를 각 입력 필드에 분배 | - 붙여넣기 이벤트가 발생한 필드부터 시작해 순서대로 각 입력 필드를 채운다. |
[c] 포커스 이동 | - 마지막으로 채워진 칸으로 포커스를 이동시켜 빠른 입력을 지원한다. |
(3) 구현 결과 보기
- 앞선 코드의 결과를 살펴보자!
4. 마치며
이번 글에서는 OTP 입력 UI에서 Delete와 Backspace 키를 제대로 처리하기 위해 oninput과 onkeydown 이벤트를 함께 사용하는 방법을 살펴보았다.
핵심 포인트를 정리하면:
- oninput 이벤트: 값 변경 후에 발생해, 문자 입력 시 입력값을 정제하고 다음 칸으로 자동 포커스 이동을 가능하게 한다. 단, 빈 칸에서는 이벤트가 발생하지 않는 한계가 있다.
- onkeydown 이벤트: 키를 누르는 순간 발생하여, 빈 입력 칸에서도 Delete/Backspace 키의 입력을 직접 감지하고 처리할 수 있다. 이때
preventDefault()
를 사용해 기본 동작을 차단하는 것이 중요하다. - onpaste 이벤트: 붙여넣기 시 전체 인증번호를 문자 단위로 분리해 각 칸에 채워 넣으며, 자동으로 포커스 이동을 지원한다.
이처럼 세부적인 구현과 기술적 고려를 통해, OTP 입력 UI의 UX를 더욱 완성도 있게 다듬을 수 있다.
Delete 키와 Backspace 키의 차이가 단순한 입력 삭제 이상의 의미를 가지며,
이 기능을 구현함으로써 사용자는 “버튼이 먹지 않는다”는 불편함 없이 일관된 입력 경험을 누릴 수 있게 된다.
그러니 삭제 기능을 구현할 때 Delete키의 존재를 잊지 말도록 하자 🙂
반응형
'개발 기술 > 사소하지만 놓치기 쉬운 개발 지식' 카테고리의 다른 글
CSS interpolate-size 기반 아코디언, JS보다 빠를까? 실측 성능 테스트 (4) | 2025.03.15 |
---|---|
에셋 이미지의 동적 로드와 빌드 최적화 (0) | 2025.02.14 |
Base64 인코딩과 Path 방식의 장단점 및 성능 비교(feat.에셋 로드 방식) (0) | 2025.02.09 |
[JS] Array 빌트인 함수는 정말 성능이 나쁠까? (forEach, map 등) (0) | 2025.01.24 |
[JS] setTimeout: 0ms delay에도 지연이 발생하는 이유 (4) | 2025.01.14 |
댓글