
생년월일 입력 UI를 구현하면서,
네이티브 input[type="date"]와 커스텀 텍스트 인풋을 함께 사용하는 패턴을 적용했다.
함수명, 변수명, 컴포넌트명은 모두 예시용 가명으로 작성되어 있다.
[UI 구조]
생년월일 필드는 다음과 같은 구조로 구성되어 있었다.
[텍스트 인풋]
포맷팅된 날짜를 보여주는 용도
예: 1995년 5월 10일
[날짜 인풋 (input[type="date"])]
실제로 네이티브 날짜 피커를 띄우는 역할
투명하게 겹쳐서 사용
function DateOfBirthField() {
const extraInputRef = useRef<HTMLInputElement | null>(null);
const dateInputRef = useRef<HTMLInputElement | null>(null);
const [rawDate, setRawDate] = useState('');
const formattedDate = formatDate(rawDate); // YYYY-MM-DD → "1995년 5월 10일"
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setRawDate(e.target.value);
};
const openPicker = () => {
if (!dateInputRef.current) return;
// 날짜 인풋에 포커스
dateInputRef.current.focus();
// 네이티브 피커 열기
if (typeof dateInputRef.current.showPicker === 'function') {
try {
dateInputRef.current.showPicker();
} catch {
dateInputRef.current.click();
}
} else {
dateInputRef.current.click();
}
// 날짜 선택 후 텍스트 인풋으로 포커스 이동
extInputRef.current?.focus();
};
return (
<DateFieldWrapper onClick={openPicker}>
<FormattedTextInput ref={extraInputRef} value={formattedDate} readOnly />
<HiddenDateInput
ref={dateInputRef}
type="date"
value={rawDate}
onChange={handleChange}
/>
</DateFieldWrapper>
);
}
[UX를 위한 포커스 이동]
UX 측면에서는 다음과 같은 흐름을 의도했다.
1. 사용자가 생년월일 영역을 탭하면 네이티브 달력이 열린다.
2. 날짜를 선택하면 포맷팅된 값이 텍스트 인풋에 표시된다.
3. 선택 이후에는 텍스트 인풋에 커서가 있는 상태가 자연스럽다고 판단했다.
이를 위해 래퍼에 클릭 핸들러를 두고, 피커를 여는 동시에 인풋 포커스를 조정하는 로직을 추가했다.
[iOS에서만 발생한 현상]
문제는 이 코드가 iOS에서만 다음과 같이 동작했다는 점이다.
생년월일 필드를 처음 탭했을 때
[같은 필드를 다시 탭하면]
정리하면 다음과 같다.
초기 클릭에서만 발생 (“오늘 날짜 자동 선택 + 피커 즉시 닫힘”)
=> 내부 상태와 실제 UX가 어긋나는 문제
문제를 재현하면서 몇 가지 공통점을 확인했다.
이를 바탕으로 다음 요소들을 하나씩 제거하며 테스트했다.
실험 결과, 결정적인 원인은 포커스 이동이었다.
포커스 이동 제거
가장 먼저, 피커를 띄운 직후 텍스트 인풋으로 포커스를 옮기던 부분을 제거했다. 이 변경만으로 다음 문제가 모두 사라졌다.
데스크톱과 안드로이드 환경에서도 기존 동작은 그대로 유지됐다.
function DateOfBirthField() {
const dateInputRef = useRef<HTMLInputElement | null>(null);
const [rawDate, setRawDate] = useState('');
const formattedDate = formatDate(rawDate);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setRawDate(e.target.value);
};
const openPicker = () => {
if (!dateInputRef.current) return;
if (typeof dateInputRef.current.showPicker === 'function') {
try {
dateInputRef.current.showPicker();
return;
} catch {}
}
dateInputRef.current.click();
};
return (
<DateFieldWrapper onClick={openPicker}>
<FormattedTextInput value={formattedDate} readOnly />
<HiddenDateInput
ref={dateInputRef}
type="date"
value={rawDate}
onChange={handleChange}
/>
</DateFieldWrapper>
);
}
선택 이후에도 텍스트 인풋에 커서가 있는 상태를 만들고 싶다는 UX 판단 자체는 합리적이었다. 하지만 iOS에서 input[type="date"]는 포커스와 피커가 결합돼 있었고, 그 위에 추가한 포커스 이동 로직이 오히려 부작용을 만들고 있었다.
해결책은 단순했다.
피커를 띄운 뒤에는 포커스를 조작하지 않고
브라우저의 기본 동작을 존중한다.
UX 의도를 유지하려다 생긴 문제였고,
한 줄의 포커스 제거로 디자인과 안정성을 모두 지킬 수 있었다.