Safari에서 일본어 입력 시 Enter가 form submit 되는 이슈 해결기
웹 프로젝트에서 입력 폼을 만들다 보면 다양한 언어 환경과 브라우저 동작을 고려해야 합니다. 최근 일본어를 입력하는 Safari 유저에게서 다음과 같은 문제가 발생했습니다.
문제: 일본어 입력 도중 Enter 키를 누르면 한자 변환 확정이 아니라 폼이 제출(submit) 되어버리는 현상
이 문제는 Chrome, Firefox 등 다른 브라우저에서는 발생하지 않았기 때문에 원인을 파악하고 해결하는 데 꽤 시간이 걸렸습니다. 이 글에서는 문제 상황, 원인 분석, 그리고 해결 과정까지 모두 공유해보겠습니다.
문제 상황 재현
⚙️ 환경
- 브라우저: Safari (macOS, iOS)
- 입력 언어: 일본어
<form onSubmit={handleSubmit}>
<textarea onKeyDown={handleChangeHeight} />
<button type="submit">Send</button>
</form>
⛔ 문제가 되는 행동
Safari에서 일본어를 입력할 때, 예를 들어 こんにちは를 한자로 바꾸기 위해 입력 후 Enter를 누르면…
- Chrome, Firefox: IME 입력이 확정되고, submit은 되지 않음
- Safari: IME 입력이 끝나기도 전에 form이 submit 됨
즉, 사용자는 아직 입력 중인데 메시지가 전송돼버리는 UX 문제가 발생합니다.
🔍 원인 분석
Safari에서는 onKeyDown 이벤트의 e.nativeEvent.isComposing 값이 기대한 것보다 빨리 false가 되거나,
onCompositionEnd가 IME 입력 확정 전에 먼저 호출되는 경우가 있었습니다.
이는 IME(입력기) 동작의 타이밍 차이에서 비롯되며, 특히 Safari에서 더 자주 발생합니다.
Safari는 변환 중인데도 compositionend가 너무 빨리 오는 경우 있습니다.
(크롬은 문제 없이 잘 동작...)
=> isComposing 상태를 추적해서 Enter를 무시해야 함
‼️ IME란?
IME (Input Method Editor)는 키보드로 직접 입력할 수 없는 문자나 언어를 입력할 수 있게 도와주는 도구
[ IME의 동작 흐름 ]
1. Composition Start (입력 시작)
예: konnichiha를 타이핑하면 아직 "未確定" 상태의 문자로 처리됨
2. Composition Update (입력 중)
계속해서 입력이 바뀌는 중. 사용자는 아직 확정하지 않았고 IME가 후보를 보여줌.
3. Composition End (입력 확정)
사용자가 Enter 등을 눌러 후보를 선택하면, 확정된 문자가 입력 필드에 반영됨.
💡 원래 시도했던 방식 (실패)
const handleKeyDown = (e: KeyboardEvent) => {
if (e.nativeEvent.isComposing) return; // 입력 중이면 무시
if (e.key === 'Enter') {
e.preventDefault(); // submit 방지
handleSubmit();
}
};
이 방법은 Chrome에서는 잘 작동했지만, Safari에서는 isComposing === false인데 여전히 변환 중인 상태라 submit이 발생했습니다.
✅ 해결 방법
- isComposing 상태를 useRef로 직접 관리
- onCompositionStart / onCompositionEnd으로 정확한 입력 상태 감지
- Safari의 빠른 onCompositionEnd를 보완하기 위해 setTimeout 사용
최종 코드
const isComposingRef = useRef(false);
const handleCompositionStart = () => {
isComposingRef.current = true;
};
const handleCompositionEnd = () => {
setTimeout(() => {
isComposingRef.current = false;
}, 10); // Safari 대응
};
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
if (isComposingRef.current) {
e.preventDefault();
return;
}
e.preventDefault();
handleSubmit(e as unknown as FormEvent);
}
};
<Textarea
ref={textareaRef}
onKeyDown={handleKeyDown}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
/>