ㄱ은 보이는데 ㅏ를 누르는 순간 글자가 사라진다안녕을 쳤는데 ㅇㅏㄴㄴㅕㅇ 으로 풀려서 들어간다저는 SSH Term이라는 SSH 앱을 혼자 만들고 있습니다. 원래는 안드로이드 SSH 앱에서 일본어 입력이 안 돼서 만든 앱인데, 같은 문제를 한글로 겪는 분이 훨씬 많다는 걸 알고 한글 쪽을 계속 파고 있습니다.
위의 증상들, 겉보기는 비슷해도 원인이 전부 다릅니다. 하나씩 보겠습니다.
PC에서 claude를 실행하면 구조는 이렇습니다.
키보드 → OS의 IME → 터미널 앱(iTerm2, Windows Terminal 등) → pty → Claude Code
IME가 ㅎ → 하 → 한을 조합하는 동안, 그 과정을 아는 건 터미널 앱까지입니다. 터미널 앱은 조합 중인 글자를 자기 화면 위에 겹쳐서 그려 줄 뿐이고, 조합이 끝나야 비로소 한의 UTF-8 바이트(ed 95 9c)를 pty에 씁니다.
pty는 그냥 바이트가 흐르는 파이프입니다. "지금 이 글자는 조합 중"이라고 알려 주는 표준 시퀀스가 없습니다. 그래서 Claude Code 같은 TUI 입장에서는 도착한 바이트가 전부입니다.
여기서 중요한 건 클라이언트가 한글을 어떤 바이트로 보내느냐입니다. 한을 칠 때, 클라이언트에 따라 3가지로 갈립니다.
| 방식 | 보내는 바이트 | TUI가 받는 것 |
|---|---|---|
| ① 완성된 글자만 보낸다 | ed 95 9c | 한 |
| ② 지우고 다시 쓴다 | e3 85 8e 7f ed 95 98 7f ed 95 9c | ㅎ → 지우기 → 하 → 지우기 → 한 |
| ③ 조합하지 않고 낱자로 보낸다 | e3 85 8e e3 85 8f e3 84 b4 | ㅎㅏㄴ |
7f는 DEL(백스페이스)입니다. ②는 조합 중인 글자를 서버에 미리 보내 놓고, 바뀔 때마다 지우고 새 글자를 다시 보내는 방식입니다. 모바일 SSH 앱에서 자주 보입니다.
PC의 터미널 앱은 대부분 ①입니다. 문제는 대부분 ②와 ③, 그리고 ①에서 "조합 중인 글자를 어디에 그리느냐"에서 생깁니다.
터미널 앱은 조합 중인 글자를 진짜 커서 위치에 겹쳐 그립니다. 일반 셸이라면 커서가 입력 위치에 있으니 문제가 없습니다.
그런데 Claude Code 같은 TUI는 화면 전체를 자기가 다시 그립니다. 다 그린 뒤에 진짜 커서를 화면 아래쪽 같은 곳에 두면, 터미널은 그 자리에 조합 중인 글자를 그립니다. 그래서 조합 중인 한이 왼쪽 아래에 둥둥 떠 있는 증상이 납니다.
Claude Code는 이걸 여러 번에 걸쳐 고쳐 왔습니다. CHANGELOG에서 찾아보면 이렇습니다.
claude agents 화면(2.1.160), Windows의 백그라운드 세션(2.1.153), /skills 검색창(2.1.282) 같은 화면별로 하나씩 고쳐짐제가 2.1.289에서 확인해 보니, 입력란에 가를 넣으면 진짜 커서가 정확히 가 바로 뒤 칸에 있었습니다.
다만 아직 열려 있는 보고도 있습니다.
claude update로 최신 버전을 쓴다. 조합 위치 문제의 대부분은 이미 고쳐진 버전이 있습니다workbench.experimental.modernUI를 false 로 둔다 (#97389의 우회책)Ctrl+G로 외부 편집기를 열어서 거기서 쓴다. Claude Code 입력란 대신 평소 쓰는 편집기에서 한글을 다루게 됩니다2025년 12월에 이런 보고가 올라왔습니다 (#15705, 당시 Claude Code 2.0.76). iOS의 SSH 앱(Termius)으로 서버에 붙어서 Claude Code에 가를 치면, ㅏ를 누르는 순간 글자가 사라진다는 내용입니다.
보고자가 서버에서 실제로 받은 바이트를 찍어 봤더니 이랬습니다.
e3 84 b1 7f ea b0 80
ㄱ DEL 가
위 표의 ② 방식입니다. 같은 앱의 Android판에서는 재현되지 않았고, 같은 iOS 환경에서도 Gemini CLI나 Codex CLI는 문제가 없었다고 합니다.
원인은 이슈 댓글에서 커뮤니티가 분석했습니다. ㄱ, DEL, 가가 한 덩어리로 도착하면, 입력 처리 코드가 이 덩어리를 "백스페이스가 섞인 입력"으로 보고 DEL만 처리한 뒤 나머지 글자(가)를 버리고 끝나 버린다는 것입니다. 그래서 ㄱ은 지워지고 가는 안 들어가서, 결과적으로 아무것도 안 남습니다.
pty로 claude를 띄우고, 입력란에 아래 바이트를 그대로 써 넣은 뒤 화면을 읽었습니다.
| 보낸 것 | 입력란 결과 |
|---|---|
ㄱ DEL 가 를 한 덩어리로 | 가 ✅ |
ㄱ / DEL / 가 를 0.3초 간격으로 따로 | 가 ✅ |
ㅎ DEL 하 DEL 한 을 한 덩어리로 | 한 ✅ |
ㄱ DEL 가 DEL 간 DEL 가나 (받침이 다음 글자로 넘어가는 경우) | 가나 ✅ |
ㅎ DEL 하 DEL 한 ㄱ DEL 그 DEL 글 을 한 덩어리로 | 한글 ✅ |
ab 입력 → 왼쪽 화살표 → ㄱ DEL 가 (문장 중간) | a가b ✅ |
2.1.289에서는 전부 제대로 들어갔습니다. CHANGELOG에서 이 수정에 해당하는 항목은 찾지 못했지만, 적어도 이 버전에서는 ② 방식이 깨지지 않습니다.
claude update). 휴대폰 앱이 아니라 SSH로 붙는 서버 쪽 버전이 중요합니다같은 #15705에 다른 iOS 앱으로 시험한 댓글이 있습니다. 안녕을 치면 ㅇㅏㄴㄴㅕㅇ 으로 들어간다는 것입니다. 위 표의 ③ 방식입니다.
이건 서버에서는 고칠 수 없습니다. 클라이언트가 이미 낱자로 보내 버렸기 때문에, Claude Code든 bash든 받은 그대로 ㅇㅏㄴㄴㅕㅇ을 보여 줄 수밖에 없습니다.
#15705 보고자가 쓴 방법을 참고해서, 한 번에 도착한 바이트를 한 줄로 보여 주도록 바꿨습니다. 서버에 이 파일을 만들고, 문제가 나는 그 클라이언트로 SSH 접속한 상태에서 실행합니다.
# keylog.py — 도착한 바이트를 덩어리 단위로 보여 줍니다
import os, sys, termios, tty
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
tty.setraw(fd)
try:
print("키를 입력해 보세요. 끝내려면 Ctrl+C\r")
while True:
chunk = os.read(fd, 1024) # 한 번에 도착한 바이트 = 한 줄
if not chunk or chunk == b"\x03":
break
print(chunk.hex(" ") + " " + repr(chunk.decode("utf-8", "replace")) + "\r")
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
python3 keylog.py
가, 한을 쳐 보면 이런 식으로 나옵니다.
e3 84 b1 7f ea b0 80 'ㄱ\x7f가'
ed 95 9c '한'
읽는 법은 간단합니다.
| 보이는 것 | 방식 | 해당하는 곳 |
|---|---|---|
ea~ed로 시작하는 3바이트만 (완성된 글자) | ① | 깨지는 곳 1 |
중간에 7f가 섞여 있다 | ② | 깨지는 곳 2 |
e3 84 / e3 85 / e3 86으로 시작하는 낱자만 계속 나온다 | ③ | 깨지는 곳 3 |
참고로 e3 84 b1~e3 86 8e가 한글 낱자(ㄱ, ㅏ 같은 호환 자모), ea b0 80~ed 9e a3가 완성된 글자(가~힣)입니다.
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 조합 중인 글자가 왼쪽 아래 / 엉뚱한 곳에 보인다 | 진짜 커서가 입력 위치에 없다 | claude update. VS Code는 workbench.experimental.modernUI 끄기 |
| 문장 중간에서 조합하면 뒤 글자가 안 보인다 | 터미널의 겹쳐 그리기와 TUI 다시 그리기의 충돌 (열린 이슈) | 조합이 끝나면 돌아온다. 긴 수정은 Ctrl+G 외부 편집기 |
| 모바일에서 글자가 사라진다 | ② 지우고 다시 쓰기를 예전 버전이 잘못 읽음 | 서버의 Claude Code를 최신으로 |
ㅇㅏㄴㄴㅕㅇ처럼 풀린다 | ③ 클라이언트가 조합하지 않음 | 클라이언트 또는 키보드 설정을 바꾸기 |
여기까지 보면 원칙은 하나입니다. TUI가 조합 과정을 볼 일이 없게 만들면 깨질 곳이 줄어듭니다.
SSH Term은 이 원칙대로 만들었습니다.
Ctrl+E로 입력 모드를 켜고 끕니다Claude Code에 긴 지시를 한국어로 쓰는 일이 많다면, 입력 모드에서 다 쓰고 한 번에 보내는 쪽을 권합니다.
그리고 한 줄만 덧붙이면, SSH Term은 연결 설정에서 「호스트에서 셸 유지」를 켜면 셸을 호스트에 남겨 두기 때문에 Mosh 없이도 끊기지 않습니다. 지하철 터널에서 연결이 끊겨도 에이전트는 호스트에서 계속 돌고, 다시 붙으면 놓친 출력까지 이어서 보여 줍니다.
그래도 Mosh가 꼭 필요한 환경이라면 댓글로 알려 주세요. 다음에 무엇을 만들지 정할 때 참고합니다.
구독 없이 1회 구매입니다.
한글이 깨질 때 "Claude Code가 한글을 못 한다"로 뭉뚱그리면 고칠 곳을 못 찾습니다. 조합은 누가 하고, 무슨 바이트가 오가는지를 나눠 보면 대부분은 버전 업데이트나 설정 하나로 끝납니다.
keylog.py로 찍어 본 결과가 위 표 어디에도 안 맞는다면, 댓글로 결과를 붙여 주세요. 같이 보겠습니다.