GODRICH

입력창 디자인 9가지 — 복붙하면 되는 폼 위젯

입력창 디자인은 전화번호, 카드 번호, 수량, 태그처럼 회원가입·결제 폼에서 자주 보이지만 순수 HTML만으로는 제대로 동작하지 않는 입력 위젯을 말합니다.

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

아홉 개는 인기 순이 아니라 폼을 실제로 채우는 순서로 놓았습니다. 연락처와 결제 정보를 입력하고(01~03), 수량과 관심 태그를 고르고(04~05), 메모를 적고 파일을 첨부하고(06~07), 마지막으로 본인인증과 제출 전 검증을 거치는(08~09) 흐름입니다.

번호 이름 언제 쓰나
01 전화번호 자동 하이픈 회원가입·배송지 연락처
02 카드 번호 4자리 자동 포맷 결제 폼 카드 번호
03 금액 콤마 자동 입력 후원·견적 금액
04 수량 +/- 스테퍼 장바구니·예약 인원
05 태그 입력창 키워드·수신자 다중 등록
06 자동 늘어나는 textarea 후기·문의 내용
07 파일 드래그 앤 드롭 업로드 프로필 사진·첨부파일
08 인증번호 재전송 타이머 버튼 휴대폰·이메일 재인증
09 실시간 인풋 검증 표시 이메일·비밀번호 검증

01전화번호 자동 하이픈

숫자만 입력해도 010-1234-5678처럼 하이픈이 자동으로 들어갑니다. 붙여넣기로 11자리를 한 번에 넣어도 같은 규칙으로 다시 포맷됩니다.

010 패턴자동 하이픈숫자만
function format(raw) {
  const d = raw.replace(/\D/g, "").slice(0, 11);
  if (d.length < 4) return d;
  if (d.length < 8) return d.slice(0, 3) + "-" + d.slice(3);
  return d.slice(0, 3) + "-" + d.slice(3, 7) + "-" + d.slice(7);
}
input.addEventListener("input", () => {
  input.value = format(input.value);
});

02카드 번호 4자리 자동 포맷

카드 번호를 입력하면 네 자리마다 공백이 자동으로 들어가 실제 카드처럼 읽힙니다. 앞자리로 VISA·Mastercard·AMEX 여부도 함께 표시합니다.

4자리 그룹16자리 제한숫자만
function format(raw) {
  const d = raw.replace(/\D/g, "").slice(0, 16);
  const groups = d.match(/.{1,4}/g) || [];
  return groups.join(" ");
}
function brandOf(d) {
  if (d[0] === "4") return "VISA";
  if (/^5[1-5]/.test(d)) return "Mastercard";
  return "";
}

03금액 콤마 자동 입력

원화 기호가 앞에 붙은 입력창에 숫자를 넣으면 천 단위마다 콤마가 자동으로 붙습니다. 앞자리의 불필요한 0도 함께 지워집니다.

₩ 접두어천 단위 콤마우측 정렬
function format(raw) {
  const d = raw.replace(/\D/g, "").replace(/^0+(?=\d)/, "").slice(0, 12);
  return d ? Number(d).toLocaleString("ko-KR") : "";
}
input.addEventListener("input", () => {
  input.value = format(input.value);
});

04수량 +/- 스테퍼

버튼을 누를 때마다 가운데 숫자가 1씩 바뀌고, 최솟값(1)에서는 − 버튼이 자동으로 흐려지고 눌리지 않습니다.

최소/최대 제한키보드 지원숫자 직접 입력
const MIN = 1, MAX = 99;
function set(v) {
  n = Math.min(MAX, Math.max(MIN, v));
  value.value = n;
  minus.disabled = n <= MIN;
  plus.disabled = n >= MAX;
}
minus.addEventListener("click", () => set(n - 1));
plus.addEventListener("click", () => set(n + 1));

05태그 입력창

글자를 쓰고 Enter를 누르면 알약 모양 태그로 바뀌고, 빈 입력창에서 백스페이스를 누르면 마지막 태그가 지워집니다.

Enter로 추가×로 삭제백스페이스 삭제
input.addEventListener("keydown", (e) => {
  if (e.key === "Enter" || e.key === ",") {
    e.preventDefault();
    if (input.value.trim()) tags.push(input.value.trim());
    input.value = "";
    render();
  } else if (e.key === "Backspace" && !input.value && tags.length) {
    tags.pop();
    render();
  }
});

06자동 늘어나는 textarea

줄이 늘어날 때마다 스크롤 대신 입력창 높이 자체가 늘어나고, 정해 둔 최대 높이(132px)를 넘으면 그때부터 스크롤이 생깁니다.

scrollHeight 기반최대 높이 제한리사이즈 불가
function resize() {
  // 먼저 auto로 되돌려야 scrollHeight가 줄어든 만큼도 다시 잰다
  area.style.height = "auto";
  const next = Math.min(area.scrollHeight, 132);
  area.style.height = next + "px";
}
area.addEventListener("input", resize);

07파일 드래그 앤 드롭 업로드

파일을 끌어다 놓으면 영역 테두리가 굵어지고, 놓는 순간 파일 이름이 칩으로 바뀝니다. 클릭하면 일반 파일 선택창도 그대로 열립니다.

dragover 표시클릭해서 찾아보기키보드 접근
zone.addEventListener("dragover", (e) => {
  e.preventDefault();
  zone.classList.add("is-over");
});
zone.addEventListener("drop", (e) => {
  e.preventDefault();
  zone.classList.remove("is-over");
  const file = e.dataTransfer.files[0];
  if (file) filename.textContent = "선택됨: " + file.name;
});

08인증번호 재전송 타이머 버튼

재전송 버튼을 누르면 60초 동안 비활성화되며, 남은 초가 버튼 글자 위에 실시간으로 줄어듭니다. 0초가 되면 다시 눌립니다.

60초 카운트다운중복 클릭 방지aria-live 안내
function cooldown(seconds) {
  let left = seconds;
  btn.disabled = true;
  btn.textContent = left + "초 후 재전송";
  const t = setInterval(() => {
    left--;
    if (left <= 0) {
      clearInterval(t);
      btn.disabled = false;
      btn.textContent = "재전송";
    } else {
      btn.textContent = left + "초 후 재전송";
    }
  }, 1000);
}

09실시간 인풋 검증 표시

입력하는 동안 이메일 형식이 맞으면 초록 체크가, 틀리면 빨간 문구가 그 자리에서 바로 나타납니다. 값이 비어 있으면 아무 표시도 하지 않습니다.

실시간 검증role="status" 알림성공/실패 아이콘
const RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
input.addEventListener("input", () => {
  const v = input.value;
  frame.classList.remove("is-ok", "is-bad");
  if (!v) { icon.textContent = ""; msg.textContent = ""; return; }
  if (RE.test(v)) { frame.classList.add("is-ok"); icon.textContent = "✓"; }
  else { frame.classList.add("is-bad"); icon.textContent = "!"; msg.textContent = "이메일 형식을 확인해 주세요"; }
});

어디서 깨지는가 — 함정 하나

01·02·03의 자동 포맷 입력창은 전부 같은 함정을 안고 있습니다. input.value = format(value)처럼 값을 통째로 다시 쓰면, 브라우저는 커서(캐럿) 위치를 항상 문자열 맨 끝으로 되돌립니다. 가운데 자릿수를 고치려고 입력창 중간을 클릭해도, 다음 글자를 치는 순간 커서가 끝으로 튀어버립니다. 짧은 전화번호·카드 번호 입력에서는 크게 거슬리지 않지만, 길게 수정하는 입력창이라면 MDN의 HTMLInputElement.setSelectionRange 문서를 참고해 재포맷 전 커서 위치를 저장했다가 되돌려 주는 처리가 필요합니다. 이 함정을 포함해 아홉 개 파일 전부에 실제로 동작하는 바닐라 JS와 React 버전을 담은 압축 파일의 비밀번호는 puc8udvv이고, 아래 버튼으로 받을 수 있습니다.

07의 드래그 앤 드롭도 비슷하게 놓치기 쉬운 부분이 있습니다. dragover 이벤트에서 preventDefault()를 호출하지 않으면 브라우저가 기본 동작(파일을 새 탭으로 여는 것)을 실행해 버려 drop 이벤트 자체가 발생하지 않습니다. 위 코드 조각에도 이 처리가 들어 있는 이유입니다.

접근성

아홉 개 모두 라벨과 입력창을 label·for로 연결했고, 04의 수량 버튼은 화면에 보이지 않는 aria-live 영역으로 "수량 3개"처럼 변경값을 스크린 리더에 알립니다. 08의 재전송 버튼은 aria-live="polite"인 상태 문구로 재전송 완료를 알리고, 09의 검증 문구는 role="status"로 값이 바뀔 때만 조용히 갱신됩니다. 07의 파일 입력은 화면에서는 숨겨져 있지만 Tab으로 포커스가 가고 Enter·Space로 파일 선택창이 열려, 마우스 없이도 끝까지 쓸 수 있습니다. 색·테두리 전환에는 전부 MDN의 prefers-reduced-motion 문서에 따라 모션을 줄이는 사용자를 위한 처리를 넣었습니다.

@media (prefers-reduced-motion: reduce) {
  .field__input, .drop__zone, .valid__frame { transition: none; }
}

다른 폼·인풋 관련 편은 인증번호 입력 UI 9종에서 이어 볼 수 있고, 이 사이트가 무엇을 다루는지는 소개에 정리돼 있습니다.

FAQ

이 아홉 개를 리액트 프로젝트에도 그대로 쓸 수 있습니까?

네. 압축 파일 안 react/ 폴더에 항목마다 .tsx.module.scss가 따로 들어 있어, vanilla/의 순수 JS 버전과 같은 동작을 훅으로 옮겨 뒀습니다. props로 색·라벨 문구 정도만 바꿔 쓰면 됩니다.

카드 번호 자동 포맷이 결제 보안에도 영향을 줍니까?

아닙니다. 여기서 다루는 건 화면에 숫자를 보기 좋게 4자리씩 나눠 보여주는 표시 방식일 뿐이고, 카드 정보를 실제로 처리·저장하는 건 결제대행사(PG)의 몫입니다. 자동 포맷 자체는 카드 데이터를 서버로 보내지 않습니다.

전화번호·카드 번호 입력창에서 모바일 숫자 키패드가 자동으로 뜨나요?

네, inputmode="numeric"을 넣어 뒀기 때문에 대부분의 모바일 브라우저가 문자 키패드 대신 숫자 키패드를 먼저 보여줍니다. 다만 이는 힌트일 뿐이라 브라우저마다 미세하게 다르게 동작할 수 있습니다.

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

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