본문 바로가기
C.W.K.
Stream
Lesson 02 of 04 · published

한국어 IME 앞에서 에디터가 무너진다

~12 min · ime, composition, korean, war-story

Level 0식은 초고
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"IME 는 확정 전에 키들을 한 글자로 조립해. 키마다 반응하는 에디터는 그 조립을 도중에 깨뜨리고."

IME 가 실제로 하는 일

한국어를 칠 때 키보드가 보내는 건 완성된 글자가 아니야. IME 가 눈앞에서 실시간으로 음절 블록을 조립하는 자모들이지. 치고 치고 치면 이 되는데, 조합이 확정된 다음에야 그래. 일본어 가나-한자 변환이나 중국어 병음도 똑같아. 키는 여럿, 확정되는 글자는 하나, 그리고 그 사이에 조립 중인 상태가 있어.

순진한 에디터가 왜 깨뜨리나

input 이벤트가 올 때마다 문서를 다시 쓰는 에디터는 조합 한가운데로 걸어 들어가 그걸 짓밟아. 자모가 떨어져 나가거나, 글자가 중복으로 찍히거나, 반쪽짜리 음절이 확정돼버려. 제대로 된 처리는 조합 라이프사이클을 존중하는 거야. 조합이 언제 시작하는지 알고, 갱신되는 동안에는 조립 중인 텍스트를 건드리지 않고, 끝났을 때만 편집을 적용해.

el.addEventListener("compositionstart", () => { composing = true; });
el.addEventListener("compositionupdate", () => { /* 자모 조립 중: 커밋 금지 */ });
el.addEventListener("compositionend", (e) => { composing = false; apply(e.data); });

CodeMirror 6 은 이 라이프사이클 전체를 내부에서 처리해. 그 위에 선다는 건 저 세 줄을 영영 안 써도 된다는 뜻이고, 브라우저별로 디버깅하며 보낼 일주일도 안 쓴다는 뜻이야. Rekindle 이 코어를 빌리는 제일 큰 이유 중 하나가 이거야. 손수 짠 에디터는 대개 IME 앞에서 무너지는데, 아빠는 매일 한국어로 쓰거든.

소스 파일에서도 바이트가 문제를 일으켜

non-ASCII 때문에 겪는 고생은 편집 화면에서 안 끝나고 소스 파일까지 따라와. Rekindle 에서 실제로 있었던 일이야. 소스 모듈 안에 들어 있던 한국어, 예를 들면 voice-rule 정규식 같은 게 프로덕션 빌드와 Chromium 에선 멀쩡한데 tauri dev 에서만 조용히 깨졌어. 원인은 로직이 아니라 바이트 디코딩이었고, charset=utf-8 헤더를 붙여도 안 고쳐졌어. 모듈 스크립트는 Content-Type 이 뭐든 스펙상 항상 UTF-8 이거든.

증상:  소스 모듈 안 한국어가 프로덕션 + Chromium 에선 되는데,
       `tauri dev` 에선 소리 없이 깨짐.
원인:  Vite 가 ES 모듈을 HTTP 로 서빙 → WKWebView 가 그 트랜스포트에서
       raw non-ASCII 바이트를 잘못 디코딩.
수정:  Vite 플러그인이 서빙된 출력의 non-ASCII 를 \uXXXX 로 이스케이프.
       불변 조건: 어떤 webview 도 \u 로 이스케이프된 ASCII 는 못 깸.

해결책은 소스 파일은 읽을 수 있는 한국어 그대로 두고 서빙되는 트랜스포트만 ASCII 로 만드는 거였어. 교훈은 여기서만 통하는 게 아니야. non-ASCII 텍스트가 프로젝트에서 진짜 비중을 갖는 순간, 아빠한텐 늘 그렇지만, 인코딩 버그 한 부류를 통째로 물려받게 돼. 빌릴 수 있는 레이어는 전부 빌리고 네가 감당할 표면적을 작게 유지해야 할 이유가 하나 더 생기는 거지.

Code

IME 조합 라이프사이클 존중 (CM6 이 너 대신 해줌)·javascript
// IME 가 깨지는 방식: input 이벤트마다 문서를 건드리면 조합과 부딪혀.
el.addEventListener("input", () => apply(el.value));

// 올바른 방식: 조합 라이프사이클을 존중해.
let composing = false;
el.addEventListener("compositionstart", () => { composing = true; });
el.addEventListener("compositionupdate", () => { /* 자모 조립 중이니 확정 금지 */ });
el.addEventListener("compositionend", (e) => { composing = false; apply(e.data); });

// CodeMirror 6 이 이 라이프사이클 전체를 내부에서 맡아.
// 코어를 빌린다는 건 이 줄을 안 쓰고, 디버깅도 안 한다는 뜻이야.
실제 사례: 서빙된 모듈 안의 non-ASCII 가 tauri dev 에서 깨진 일·text
증상:  소스 모듈 안 한국어가 프로덕션 빌드 + Chromium 에선 멀쩡한데,
       `tauri dev` 에서만 조용히 깨짐.
원인:  Vite 가 ES 모듈을 HTTP 로 서빙 → WKWebView 가 그 트랜스포트에서
       raw non-ASCII 바이트를 잘못 디코딩. charset=utf-8 헤더도 소용없음
       (모듈 스크립트는 스펙상 항상 UTF-8).
수정:  Vite 플러그인이 서빙된 모듈 출력의 non-ASCII 를 \uXXXX 로 이스케이프.
       불변 조건: 어떤 webview 도 \u 로 이스케이프된 ASCII 는 못 깸.

# 소스 파일은 읽을 수 있는 한국어 그대로, 서빙 트랜스포트만 ASCII 로.

External links

Exercise

한국어나 일본어, 중국어를 칠 줄 알면 아무 웹 텍스트 상자나 열고 단어 하나를 천천히 쳐봐. 확정되기 전에 글자가 조립되는 걸 지켜보는 거야. 화면에는 보이는데 아직 확정은 안 된 그 중간 상태가 조합 라이프사이클이야. 이제 키를 칠 때마다 디스크에 저장하는 에디터를 상상해봐. 어느 순간에 네 단어가 깨질까?
Hint
compositionupdate 도중에 깨져. 자모가 아직 조립 중이라 확정된 게 하나도 없는 상태거든. 그 중간의 키 입력을 확정된 편집으로 취급하는 에디터는 IME 와 부딪혀서 음절을 뭉개버려.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.