개발 기술/사소하지만 놓치기 쉬운 개발 지식

[JS] Delete vs Backspace: 키 이벤트 처리 시 주의사항 (with. OTP 입력기)

by GicoMomg 2025. 3. 29.

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)을 구현할 때,
deleteContentBackwarddeleteContentForward 타입을 사용하면 되겠군!

  • 아쉽게도 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 입력창에서는 inputkeydown 두 이벤트를 조합해야 입력과 삭제를 모두 구현할 수 있다.
  • 그럼 이제 실제로 어떻게 구현했는지 코드 단위로 살펴보자!

(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키의 존재를 잊지 말도록 하자 🙂

반응형

댓글