분할 입력 인풋(split code input) 구현하기

김민서·2025년 2월 26일

DOKBARO

목록 보기
5/6
post-thumbnail

들어가기 앞서

이 포스팅은 DOKBARO를 개발하면서 경험한 것을 기반으로 제작하였습니다.

DOKBARO란 ?

자기개발과 성장을 위해 독서와 스터디를 활용하는 개발자들을 위한 퀴즈 학습 플랫폼, DOKBARO입니다.

개발 서적을 즐겨 읽지만, 매번 내용을 제대로 이해했는지 확인하기 어렵지 않으셨나요? 혹은 이해 부족으로 인해 독서 스터디가 소수만 적극적으로 참여하는 형태로 변질되는 경험을 하셨을지도 모릅니다.

그래서, DOKBARO는

📚 퀴즈 출제 및 풀이 기능으로 도서 내용을 재미있고 효과적으로 이해하도록 도와드려요.

💡 스터디 리포트 기능으로 스터디원들이 책에 대해 자유롭게 의견을 나누고, 서로의 학습 현황을 확인할 수 있어요.

DOKBARO와 함께라면 도서 이해도를 높이고, 스터디 활동을 보다 풍성하고 활발하게 만들어 이상적인 독서 환경을 경험하실 수 있습니다. ✌️

현재 베타 오픈중이니 아래 링크를 통해 이용해보실 수 있어요!
https://dokbaro.com


개요

독바로 서비스 회원가입 시 이메일로 전송받은 6자리 인증 코드를 입력하는 인풋과 스터디 그룹 초대코드를 입력하는 인풋의 디자인은 다음과 같다.

동작 확인

이렇게 쪼갈라진 인풋의 동작은 다음과 같다.

1. 인풋 하나에 문자를 입력하면 입력 커서가 바로 다음 인풋으로 넘어가서 바로 다음 문자를 이어서 입력할 수 있다.
2. 지울 때도 마찬가지로, backspace를 누르면 해당 인풋의 문자가 하나 지워지며, 다시 backspace를 누르면 바로 이전 인풋으로 커서가 이동한다.
3. 코드 붙여넣기를 할 수 있다.

구현해보자.

구현 방법

1. 입력 필드 UI 컴포넌트 구현

먼저, CodeInput 이라는 6개의 입력 필드를 하나로 묶은 UI 컴포넌트를 만들었다.
(css 관련 - code-input-container div는 column 방향의 flex 레이아웃으로 설정하고, 일정한 gap을 설정하고, 3번째와 4번째 인풋 사이의 간격은 nth-child를 사용해 더 넓게 설정했다.)

/* 코드에서 불필요한 부분은 임의생략함. */
interface Props {
  codeList: string[];
  isMatch: boolean;
  onCodeChange: (e: React.ChangeEvent<HTMLInputElement>, i: number) => void;
  onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>, i: number) => void;
  onPaste: (e: React.ClipboardEvent<HTMLInputElement>) => void;
}

export default function CodeInput({
  codeList,
  isMatch,
  onCodeChange,
  onKeyDown,
  onPaste,
}: Props) {
  return (
     {/* 각각의 인풋 6개를 감싸는 컨테이너 */}
     <div className={styles["code-input-container"]}>
       {codeList.map((digit, i) => (
         <Input
           key={i}
           id={`code-input-${i}`}
           value={digit}
           onChange={(e) => onCodeChange(e, i)}
           onKeyDown={(e) => onKeyDown(e, i)}
           onPaste={onPaste}
           maxLength={1}
           isError={!isMatch}
           className={styles["code-input"]}
         />
       ))}
     </div>
  );
}

이 컴포넌트는 codeList라는 string[] 타입의 값을 전달 받는데, 해당 값은 코드 스트링이다. 예)"DOKBARO"
이 값을 map을 이용해 순차적으로 처리하며, "DOKBARO"의 구성 문자인 "D", "O", "K", ... , "O"를 Input 컴포넌트(커스텀 컴포넌트, input 태그와 동작 유사함)의 value에 각각 할당한다. 여기서 각각의 Input에는 당연히 한 문자만 들어가기 때문에 maxLength를 1로 지정해준다.

2. 코드 입력을 관리하는 커스텀 훅 구현

이제 CodeInput 컴포넌트의 Props로 전달할 인풋 동작에 필요한 로직을 관리하는 useCodeInput이라는 커스텀 훅을 작성해보자.

훅의 기본 틀은 다음과 같다.

const useCodeInput = () => {
  // 배열 ex) ["D", "O", "K", "B", "A", "R", "O"]
  const [codeList, setCodeList] = useState<string[]>(Array(6).fill(""));
  
  // "DOKBARO"
  const combinedCode: string = codeList.join(""); 
  
  /* 관련 로직들... */
  const handleCodechange = () => { /* ... */ }
  const handleKeyDown = () => { /* ... */ }
  const handlePaste = () => { /* ... */ }

  return {
    handleCodeChange,
    handleKeyDown,
    handlePaste,
    combinedCode,
    codeList,
  };
};
export default useCodeInput;

인풋의 입력값을 배열(codeList)로 관리하고 이를 문자열(combinedCode)로 합친 값을 반환한다.
codeList 배열은 ["", "", "", "", "", ""] 로 초기화되어 있다.
또한 코드 입력 관련 함수들(handleCodeChange, handleKeyDown, handlePaste)을 반환한다.

관련 함수를 하나씩 작성해보자.

1. 코드 입력 시 동작 (handleCodeChange)

const handleCodeChange = (
  e: React.ChangeEvent<HTMLInputElement>,
  index: number,
) => {
  let { value } = e.target;
  value = value.toUpperCase(); // 항상 대문자로 입력받음
    
  // codeList 업데이트
  const newCodeList = [...codeList];
  newCodeList[index] = value;
  setCodeList(newCodeList);
};

handleCodeChange 함수에서는 각 입력값을 받아 (대문자로 변환하여) value에 저장하고, 해당 index를 사용해 codeList 배열을 업데이트한다. 이를 통해 사용자가 입력한 값이 배열에 반영되도록 한다.

codeList 배열은 다음과 같이 업데이트 된다.codeList 배열 업데이트 로깅

그런데 여기까지만 하면 사용자가 인풋에 값을 하나 입력한 뒤, 다음 인풋을 클릭하여 직접 커서를 옮겨야 하는 불편함이 있다.
따라서 커서가 다음 인풋으로 자동 포커싱되는 로직을 함께 구현해야 한다.

setCodeList(newCodeList)codeList를 업데이트하는 로직 아래에 다음과 같이 작성한다.

// 다음 칸으로 포커스 이동
  if (index < 5) {
    requestAnimationFrame(() => {
      const nextInput = document.getElementById(`code-input-${index + 1}`);       
      if (nextInput) {
        nextInput.focus();
      }
    });
  }

이렇게 getElementById를 사용해 다음 인풋의 id 값을 통해 다음 인풋 필드의 DOM을 가져와 포커스를 설정한다.

requestAnimationFrame 사용하는 이유
화면이 렌더링된 후 포커스를 설정하기 위해서이다. 이는 시각적으로 자연스럽고 부드럽게 포커스가 이동하도록 보장하며, 렌더링 주기와 동기화되어 예상치 못한 동작을 방지한다.

2. 코드 삭제(backspace) 시 동작 (handleKeyDown)

이번엔 코드 삭제 시 동작을 구현하기 위해 handleKeyDown 함수를 작성해보자.

  const handleKeyDown = (
    e: React.KeyboardEvent<HTMLInputElement>,
    index: number,
  ) => {
    if (e.key === "Backspace") {
      const newCodeList = [...codeList];

      if (newCodeList[index] === "") {
        // 현재 칸이 비어있으면 이전 칸도 같이 삭제
        if (index > 0) {
          newCodeList[index - 1] = "";
          setCodeList(newCodeList);

          // 이전 칸 포커싱
          requestAnimationFrame(() => {
            const prevInput = document.getElementById(
              `code-input-${index - 1}`,
            );
            if (prevInput) {
              prevInput.focus();
            }
          });
        }
      } else {
        // 현재 칸만 삭제
        newCodeList[index] = "";
        setCodeList(newCodeList);
      }
    }
  };

사용자가 backspace를 눌러 코드를 지울 때, codeList(의 복사본)의 현재 인풋 값이 비어있으면 이전 인풋 값을 삭제함과 동시에 이전 칸으로 포커싱한다.
물론 현재 인풋이 첫번째 칸이라면 현재 인풋 값만 삭제하면 된다.

3. 코드 붙여넣기 (handlePaste)

코드 붙여넣기는 나중에 요구사항이 추가되면서 추가로 구현하였다.

외부에서 "DOKBARO" 라는 스트링 형식의 코드를 복사하여 인풋에 붙여넣기를 하면 각 문자가 개별 입력 필드에 들어가도록 해야 한다.

처음엔 어려울 거 같아서 겁냈는데, 이미 구현해놨던 로직을 보면서 생각보다 어렵지 않게 구현할 수 있었다.

const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
    // 클립보드에서 복사된 텍스트(코드)
    const pasteCode = e.clipboardData.getData("text");
    const newCodeList = [...codeList];

    const pasteArr = pasteCode.slice(0, 6).split("");
  
    pasteArr.forEach((char, i) => {
      newCodeList[i] = char;
    });
    setCodeList(newCodeList);

    // 마지막 칸 포커싱
    requestAnimationFrame(() => {
      const nextInput = document.getElementById(
        `code-input-${pasteArr.length - 1}`,
      );
      if (nextInput) {
        nextInput.focus();
      }
    });

    // 기본 붙여넣기 동작 방지
    e.preventDefault();
  };

e.clipboardData.getData("text")를 사용하여 클립보드에서 복사된 텍스트를 pasteCode에 저장한 후, 이를 한 문자씩 나누어 pasteArr 배열에 저장한다.
그 다음, pasteArr의 각 문자를 codeList(의 복사본)에 순서대로 할당하여 업데이트한다.

그리고 마지막 칸에 커서를 두어 코드를 지울 수 있도록 해야하기 때문에 마지막 칸에 포커싱을 한다.

또한 붙여넣기 동작을 직접 구현했기 때문에, 브라우저가 기본적으로 실행하는 기본 붙여넣기 동작은 방지해주기 위해 e.preventDefault(); 코드를 작성해준다.

마무리

이런 형태의 인풋을 처음 만들어 보다 보니 생소한 부분이 많았다. 이렇게 구현하는 것이 최선의 방법은 아닐 수 있지만, 그래도 나름 시행착오 끝에 원하는 동작을 구현하긴 했으니, 과정과 방법을 정리해 두는 것이 좋을 것 같아 정리해 봤다. 앞으로 더 좋은 방법을 찾게 되면 개선할 예정이다.

1개의 댓글

comment-user-thumbnail
2025년 2월 28일

와 ~~ 자세한 설명 감사합니당 최고최고!!

답글 달기