Chrome에서 한글을 입력하다가 공백을 누르면 갑자기 입력한 글자가 보이지 않는 경험, 해보신 적 있나요? 커서는 분명 텍스트 끝에 있는데 마지막 글자들이 input 필드 밖으로 밀려나서 안 보이는 현상이에요. 이 글에서는 이 문제의 원인을 파헤치고, 근본적인 해결 방법을 알아볼게요.
문제 발견: 영어는 괜찮은데 한글만 이상해요
처음에는 CSS나 레이아웃 문제인 줄 알았어요. 하지만 영어로 타이핑할 때는 전혀 문제가 없었어요. 아무리 길게 입력해도 커서가 항상 보이는 위치에 있었죠.
그런데 한글로 입력하면 이야기가 달라졌어요:


이 차이점에서 힌트를 얻었어요. 영어와 한글 입력의 근본적인 차이는 바로 IME(Input Method Editor) 사용 여부였거든요. 영어는 키보드의 각 키가 바로 하나의 문자로 입력되지만, 한글은 ‘ㄱ’ + ‘ㅏ’ + ‘ㄴ’ = ‘간’ 처럼 여러 키를 조합해서 하나의 글자를 만들어야 해요.
실제로 이 문제가 어떻게 나타나는지 직접 확인해보세요. 아래 데모에서 Chrome 브라우저로 한글을 입력하고 공백을 눌러보면 차이를 확인할 수 있어요:
IME 스크롤 버그 데모
Chrome에서 한글 입력 후 공백을 눌러 차이점을 확인해보세요
문제 상황 (일반 Input)
해결된 상황 (IME Safe Input)
테스트 방법
- 1. 위 두 input 필드에 긴 한글 텍스트를 입력하세요
- 2. 텍스트가 인풋 너비를 넘어갈 때까지 입력하세요
- 3. 한글 조합 중에 공백을 눌러보세요
- 4. 첫 번째는 커서가 사라지고, 두 번째는 항상 보입니다
브라우저별 테스트: Chromium만의 특별한 문제
여러 브라우저에서 동일한 상황을 테스트해봤어요:
- Chrome, Edge: 버그 발생
- Firefox, Safari: 정상 동작
Chromium 기반 브라우저에서만 발생하는 문제였어요. 관련 이슈를 찾아보니 실제로 ProseMirror에서도 비슷한 문제가 보고되었고, 여러 텍스트 에디터에서 한글 IME 관련 커서 위치 문제가 지속적으로 제기되고 있었어요.
원인 분석: Chrome의 IME 처리 방식
IME 조합 과정과 스크롤 타이밍 문제
문제의 핵심은 Chrome이 IME 조합을 처리하는 방식에 있었어요. 한글 입력 과정을 자세히 살펴보면:
- 조합 시작 (
compositionstart): ‘ㄱ’ 입력 - 조합 업데이트 (
compositionupdate): ‘ㄱ’ → ‘가’ → ‘간’ - 공백 입력: 조합 완료 + 공백 추가
- 조합 종료 (
compositionend): 최종 텍스트 확정
이때 Chrome은 3번 단계에서 텍스트 길이가 갑자기 늘어나는 상황을 제대로 처리하지 못해요. 다른 브라우저들은 텍스트가 길어져서 스크롤이 필요할 때 자동으로 스크롤 위치를 조정해서 커서를 보이게 하는데, Chrome은 IME 조합 과정에서 이 스크롤 업데이트가 누락돼요.
브라우저 렌더링 사이클의 이해
이 문제를 해결하려면 브라우저가 어떻게 화면을 그리는지 알아야 해요. 브라우저의 렌더링 사이클은 매우 대략적으로 이런 순서예요:
JavaScript 실행 → 스타일 계산 → 레이아웃 → 페인트 → 컴포지트 requestAnimationFrame은 이 사이클에서 다음 페인트가 시작되기 직전에 실행되도록 예약하는 함수예요. 보통 60Hz 모니터에서는 16.67ms마다 이 사이클이 반복되죠.
Chrome의 IME 버그는 조합 종료 시점과 스크롤 업데이트 타이밍이 엇갈리면서 발생해요. Chrome이 IME 조합을 완료한 직후에는 아직 레이아웃 계산이 완료되지 않은 상태라서, 이때 스크롤 위치를 조정해봤자 소용이 없어요.
시도: 렌더링 사이클에 맞춘 스크롤 보정
해결책의 핵심은 Chrome의 렌더링 타이밍에 맞춰서 스크롤 위치를 보정하는 거예요.
1단계: IME 상태 추적하기
먼저 현재 IME 조합이 진행 중인지, 그리고 조합 중에 공백이 입력되었는지 추적해야 해요:
let isComposing = false;
let hasPendingSpace = false;
// IME 조합 시작 감지
inputElement.addEventListener('compositionstart', () => {
isComposing = true;
hasPendingSpace = false;
});
// 조합 중 공백 입력 감지
inputElement.addEventListener('keydown', (e) => {
if (isComposing && e.code === 'Space') {
hasPendingSpace = true;
}
});2단계: 렌더링 사이클 완료 후 스크롤 보정
IME 조합이 완료된 후, 브라우저가 레이아웃을 다시 계산하고 화면을 그리는 작업이 끝나기를 기다려야 해요. 이때 requestAnimationFrame을 사용하면 다음 렌더링 사이클에서 정확한 스크롤 위치를 계산할 수 있어요.
inputElement.addEventListener('compositionend', () => {
isComposing = false;
if (isChromeBrowser() && hasPendingSpace) {
hasPendingSpace = false;
}
// 브라우저 렌더링 완료 후 스크롤 위치 보정
requestAnimationFrame(() => {
ensureCaretVisible();
});
});3단계: 스크롤 위치 보정하기
마지막으로 커서가 보이도록 스크롤 위치를 조정하는 함수예요:
function ensureCaretVisible() {
const selectionStart = inputElement.selectionStart;
if (selectionStart === inputElement.value.length) {
// 커서가 텍스트 끝에 있으면 스크롤을 끝까지
inputElement.scrollLeft = inputElement.scrollWidth;
} else {
// 그렇지 않으면 현재 커서 위치로 스크롤
inputElement.setSelectionRange(selectionStart, selectionStart);
}
}완성된 해결책
이 모든 로직을 합쳐보면 다음과 같아요:
function useImeSafeScroll(inputRef: React.RefObject<HTMLInputElement>, value: string) {
const isComposingRef = useRef(false);
const hasPendingSpaceRef = useRef(false);
const ensureCaretVisible = useCallback(() => {
if (!inputRef.current) return;
const element = inputRef.current;
const selectionStart = element.selectionStart;
if (selectionStart === null) return;
if (selectionStart === element.value.length) {
element.scrollLeft = element.scrollWidth;
} else {
element.setSelectionRange(selectionStart, selectionStart);
}
}, [inputRef]);
const onCompositionStart = () => {
isComposingRef.current = true;
hasPendingSpaceRef.current = false;
};
const onCompositionEnd = () => {
isComposingRef.current = false;
if (isChromeBrowser() && hasPendingSpaceRef.current) {
hasPendingSpaceRef.current = false;
}
// 모든 브라우저에서 일관되게 한 번의 requestAnimationFrame 사용
requestAnimationFrame(ensureCaretVisible);
};
const onKeyDown = (e: React.KeyboardEvent) => {
if (isComposingRef.current && e.code === 'Space') {
hasPendingSpaceRef.current = true;
}
};
const onInput = () => {
if (!isComposingRef.current) {
requestAnimationFrame(ensureCaretVisible);
}
};
// 외부 값 변경 시에도 스크롤 보정
useEffect(() => {
if (!isComposingRef.current) {
ensureCaretVisible();
}
}, [value, ensureCaretVisible]);
return { onCompositionStart, onCompositionEnd, onKeyDown, onInput };
}적용 결과와 얻은 점
이 해결책을 적용한 결과, Chrome에서도 한글 입력 중에 텍스트가 가려지는 문제가 완전히 해결되었어요. 사용자들이 더 이상 화살표 키를 눌러서 커서를 찾을 필요가 없어졌죠.
웹 표준이 존재해도 브라우저마다 세부 구현 방식이 다를 수 있어요. 특히 IME처럼 운영체제와 깊게 연결된 복잡한 기능일수록 차이가 두드러지더라고요.
이 작업에서는requestAnimationFrame을 활용해서 브라우저의 렌더링 타이밍에 맞춰 DOM을 조작하는 것이 중요했어요. IME 조합이 완료된 직후가 아니라 브라우저가 레이아웃 계산을 마친 후에 스크롤 위치를 보정해야 정확한 결과를 얻을 수 있었거든요.
국제화 개발의 중요성
이번 경험으로 국제화 개발이 얼마나 중요한지 다시 한번 깨달았어요. 영어권 개발자들은 이런 IME 문제를 경험하기 어려워서, 한글, 일본어, 중국어 사용자들의 불편함이 간과되는 경우가 많아요.
실제로 초기 Arc 브라우저에서도 비슷한 문제가 있었어요. 검색할 때 한글을 입력하고 Enter를 누르면 마지막 문자가 사라지거나 제대로 표시되지 않는 버그였죠. 이런 작은 문제 하나 때문에 아무리 다른 기능이 훌륭해도 사용을 포기하게 되는 경우가 많아요.
웹 개발에서는 이런 세심한 사용성 문제들을 하나씩 해결해나가는 과정이 정말 중요한 것 같아요. 사용자가 자연스럽게 텍스트를 입력할 수 있는 것, 그런 기본적인 경험을 만드는 것이야말로 좋은 프론트엔드 개발의 출발점이라고 생각해요.