텍스트 인풋으로 날짜 입력 받기
웹에서 날짜 입력을 커스텀하려면 <input type="date"> 대신 <input type="text">를 사용하게 된다. 브라우저 기본 날짜 인풋은 스타일링과 동작을 제어하기 어렵기 때문이다.
텍스트 인풋을 쓰면 자유도는 높아지지만, 그만큼 직접 처리해야 할 것들이 생긴다:
- 숫자 외 문자 입력 제한
- 구분자(
.,-등) 자동 삽입 - 삭제 시 포맷이 깨지지 않도록 처리
이 글에서는 yyyy.MM.dd 형식의 날짜 입력을 정규 표현식으로 다루는 방법을 정리한다.
<input type="text" />
<script src="index.js"></script>
숫자만 입력 받기
날짜 입력의 기본은 숫자다. keydown 이벤트에서 숫자가 아닌 키 입력을 차단한다.
const input = document.querySelector('input');
input.addEventListener('keydown', (e) => {
if (!e.key.match(/\d/)) {
e.preventDefault();
}
});
\d는 [0-9]와 같은 의미의 메타 문자로, 숫자 한 자리에 매칭된다.
하지만 이렇게 하면 방향키, 백스페이스, 엔터까지 막혀버린다. 허용할 키를 화이트리스트로 정의한다.
input.addEventListener('keydown', (e) => {
const whitelist = /(\d|ArrowLeft|ArrowRight|Backspace|Enter)/;
if (!whitelist.test(e.key)) {
e.preventDefault();
}
});
test()는 문자열이 정규식 패턴에 매칭되는지를 boolean으로 반환한다. match()가 매칭 결과 배열을 반환하는 것과 달리, 단순히 매칭 여부만 확인할 때 적합하다.
자동 포맷팅: 캡처링 그룹과 replace
숫자만 입력받으므로 구분자 .은 자동으로 삽입해야 한다. 아이디어는 이렇다:
- 입력값에서 숫자만 추출한다
- 캡처링 그룹으로 연·월·일을 분리한다
replace()의 치환 패턴$n으로 구분자를 끼워 넣는다
input.addEventListener('input', (e) => {
let date = e.target.value;
if (e.inputType === 'insertText') {
date = date
.replace(/[^0-9]/g, '')
.replace(/^(\d{0,4})(\d{0,2})(\d{0,2})$/g, '$1.$2.$3')
.replace(/(\.{2})/g, '');
}
input.value = date;
});
각 단계를 풀어보면:
/[^0-9]/g— 숫자가 아닌 문자를 모두 제거한다. 이전 포맷팅에서 삽입된.도 여기서 정리된다./^(\d{0,4})(\d{0,2})(\d{0,2})$/g— 숫자열을 4자리, 2자리, 2자리 그룹으로 나눈다.{0,4}처럼 최솟값을 0으로 두면 입력 중간에도 포맷팅이 동작한다. 치환 문자열'$1.$2.$3'에서$1,$2,$3은 각 캡처링 그룹에 매칭된 값으로 대체된다./(\.{2})/g— 아직 입력이 짧을 때2025.뒤에 빈 그룹이.을 만들어2025..이 되는 것을 방지한다.
e.inputType === 'insertText' 분기는 중요하다. 백스페이스로 삭제할 때도 input 이벤트가 발생하는데, 이때 포맷팅이 다시 실행되면 구분자가 재삽입되어 삭제가 동작하지 않는다. 텍스트 입력 시에만 포맷팅을 적용한다.
날짜 범위로 확장하기
단일 날짜가 아닌 yyyy.MM.dd - yyyy.MM.dd 형식의 기간을 입력받아야 한다면, 캡처링 그룹을 6개로 늘린다.
const regexPatterns = [
{ regex: /[^0-9]/g, replaceWith: '' },
{
regex: /^(\d{0,4})(\d{0,2})(\d{0,2})(\d{0,4})(\d{0,2})(\d{0,2})$/g,
replaceWith: '$1.$2.$3 - $4.$5.$6',
},
{ regex: /(\.{2})/g, replaceWith: '' },
{ regex: /\s*-\s*(?!\s*\d)/g, replaceWith: '' },
];
input.addEventListener('input', (e) => {
let date = e.target.value;
if (e.inputType === 'insertText') {
date = regexPatterns.reduce(
(value, { regex, replaceWith }) => value.replace(regex, replaceWith),
date
);
}
input.value = date;
});
추가된 정규식 하나만 살펴보면:
/\s*-\s*(?!\s*\d)/g — 시작 날짜만 입력한 상태에서 하이픈이 홀로 남는 것을 제거한다. (?!\s*\d)는 부정 전방 탐색(negative lookahead)으로, 하이픈 뒤에 숫자가 오지 않는 경우에만 매칭한다. 즉 종료 날짜 입력이 시작되면 하이픈은 유지된다.
한계
정규식만으로 날짜 포맷팅을 다루면 커서 위치를 고려할 수 없다는 근본적 한계가 있다. 사용자가 순서대로 입력하고 순서대로 지울 때는 잘 동작하지만, 커서를 중간으로 옮겨 구분자를 지우거나 여러 자리를 한꺼번에 삭제하면 포맷이 깨진다.
lookbehind 등 복잡한 정규식을 추가해 엣지 케이스를 하나씩 막을 수는 있지만, 정규식이 늘어날수록 유지보수가 어려워지고 여전히 커버하지 못하는 케이스가 남는다.
더 견고한 접근이 필요하다면 입력 영역을 연·월·일로 분리하거나, 각 섹션의 커서 위치와 선택 범위를 직접 관리하는 방식을 고려해야 한다.