맥에서 다른 맥을 Jump Desktop으로 원격 제어할 때, 한글 입력이 이렇게 망가지는 경우가 있습니다.
기대: 잘되나 이렇게 뜨네 실제: ㅈㅏㄹㄷㅗㅣㄴㅏ ㅇㅣㄹㅓㅎㄱㅔㄸㅡㄴㅔ
그리고 더 성가신 문제가 있습니다. Jump 창 안에서는 Caps Lock이 원격에 닿지 않아서, 한/영을 바꾸려면 매번 원격 메뉴바를 마우스로 클릭해야 합니다. 바깥에서 한 번, 안에서 또 한 번. 이 글은 그 두 동작을 Caps Lock 한 번으로 줄이는 방법입니다.
먼저, 왜 이런 일이 생기나
한글 입력은 두 단계로 이뤄집니다. ① 자판을 누르면 자모(ㄱ, ㅏ…)가 모이고, ② 모인 자모가 한 글자로 합쳐집니다. 이 “아직 합쳐지는 중”인 상태를 조합 중간 상태라고 합니다.
문제는 Jump가 쓰는 전송 규격에 이 중간 상태를 담을 칸이 없다는 점입니다. 그래서 로컬 맥이 한글 상태면, Jump는 자모를 하나씩 이미 완성된 글자인 것처럼 따로따로 보냅니다. 받는 쪽에는 합칠 기회가 없습니다.
로컬이 한글일 때 → "ㄷ" "ㅗ" "ㅣ" 를 각각 따로 전송 → ㄷㅗㅣ ❌
로컬이 영문일 때 → 누른 키 그대로 d k s u 전송 → 안녕 ✅
(합치는 일은 원격 맥의 한글 IME가 한다)
실제로 키 전송을 들여다보면, 한글 상태에서는 키코드가 전부 49라는 가짜 값으로 찍힙니다. 진짜 자판이 아니라 “이 글자를 넣어라”는 지시를 흉내 낸 것이기 때문입니다. 영문 상태에서는 d, k 같은 원래 키코드가 그대로 전달됩니다.
그래서 지켜야 할 규칙은 딱 하나입니다.
Jump 창에 글을 칠 때는 로컬 맥이 영문, 원격 맥이 한글이어야 한다.
한글을 만드는 일은 원격에게 맡긴다.
이 글의 나머지는 전부 그 두 값을 Caps Lock 한 번으로 맞추는 방법입니다. 어렵게 느껴지는 부분이 나와도, 목적은 이 한 문장뿐이라고 생각하시면 됩니다.
결론: 키를 원격에 보내지 마세요
직관적인 해법은 “Caps Lock을 원격까지 전달해서 원격 IME를 전환시키자”입니다. 저도 그렇게 접근했고, F13으로 바꿔 보내는 방법까지 만들었습니다. 그 방향은 실패합니다. 키 전달이 간헐적이어서, 눌러도 원격이 반응하지 않는 경우가 계속 생깁니다.
실제로 작동한 구조는 이렇습니다. 원격에 키 대신 명령을 보냅니다.
Jump 안에서 Caps Lock → 스크립트가 로컬 입력 소스를 전환
→ SSH로 원격에 "같은 값으로 설정" 지시
Jump 밖에서 Caps Lock → Ctrl+Space (평소대로 로컬만)
Jump의 키보드 전달 경로를 아예 쓰지 않으므로, 그 경로가 불안정하든 말든 무관해집니다.
읽다가 막히면 — 그냥 AI에게 넘기세요
Swift 컴파일, SSH ControlMaster, Karabiner JSON… 낯선 것투성이라 부담스러우실 수 있습니다. 전부 이해하실 필요 없습니다.
이 글의 주소를 통째로 복사해서 쓰시는 AI 에이전트(Claude Code, Codex, Cursor 등)에게 이렇게 던지시면 됩니다.
이 글대로 내 맥에 설정해줘. 나는 Jump Desktop으로 다른 맥을 원격 제어하는 제어(뷰어) 쪽 맥이야. 먼저
systemextensionsctl list | grep pqrs로 Karabiner 승인 상태를 확인하고, 설정을 바꾸기 전에karabiner.json을 백업해줘. 원격 맥 주소와 계정은 물어봐줘.
이 글은 사람이 따라 하기 위한 안내서인 동시에, 에이전트가 그대로 실행할 수 있는 명세로 썼습니다. 경로·명령·검증 방법·실패 시 되돌리는 법이 전부 적혀 있어서, 맥락 없이 던져도 대부분 알아서 해냅니다.
다만 두 가지는 꼭 일러두세요. 어느 쪽 맥에서 작업하는지(제어 맥입니다. 원격 맥이 아닙니다) — 이걸 헷갈리면 엉뚱한 기계를 고치느라 시간을 버립니다. 그리고 화면 표시가 아니라 원격에 도착한 바이트로 검증하라고 하세요. 이 문제는 메뉴바 표시가 거짓말을 합니다.
준비물
- Tailscale — 두 맥을 같은 계정으로 연결. 포트 개방이나 공인 IP가 필요 없습니다
- 원격 맥의 원격 로그인(SSH) — 시스템 설정 → 일반 → 공유
- Karabiner-Elements — 제어 맥에만. Caps Lock을 가로채는 유일한 수단입니다
Karabiner는 DriverKit 승인이 되어 있어야 동작합니다. 승인이 안 되어 있으면 규칙을 아무리 만들어도 아무 일도 일어나지 않습니다.
systemextensionsctl list | grep pqrs → activated enabled 이어야 정상 → activated waiting for user 이면 시스템 설정 → 개인정보 보호 및 보안에서 허용
1단계: 입력 소스를 바꾸는 작은 도구
macOS에는 입력 소스를 CLI로 바꾸는 기본 명령이 없습니다. Swift 30줄이면 됩니다. 양쪽 맥에 모두 컴파일해 둡니다.
import Carbon
import Foundation
func currentID() -> String {
guard let s = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(),
let p = TISGetInputSourceProperty(s, kTISPropertyInputSourceID) else { return "?" }
return Unmanaged<CFString>.fromOpaque(p).takeUnretainedValue() as String
}
func select(_ target: String) -> Bool {
guard let list = TISCreateInputSourceList(nil, false)?.takeRetainedValue()
as? [TISInputSource] else { return false }
for s in list {
guard let p = TISGetInputSourceProperty(s, kTISPropertyInputSourceID) else { continue }
if (Unmanaged<CFString>.fromOpaque(p).takeUnretainedValue() as String) == target {
return TISSelectInputSource(s) == noErr
}
}
return false
}
let args = CommandLine.arguments
if args.count > 1 { _ = select(args[1]) }
print(currentID())
swiftc -O tis.swift -o ~/bin/tis
2단계: SSH 연결을 상시 유지
매번 SSH 핸드셰이크를 하면 1초 넘게 걸립니다. ControlMaster로 연결을 재사용하면 왕복 약 270ms로 줄어 체감이 달라집니다. 제어 맥의 ~/.ssh/config에 추가합니다.
Host jumpremote
HostName 100.x.x.x # Tailscale 주소
User <원격 계정>
IdentityFile ~/.ssh/id_ed25519
ControlMaster auto
ControlPath ~/.ssh/cm/%r@%h:%p
ControlPersist 10m
BatchMode yes
ServerAliveInterval 30
키 인증도 미리 걸어둡니다. 비밀번호 프롬프트가 뜨면 스크립트가 멈춥니다.
mkdir -p ~/.ssh/cm && chmod 700 ~/.ssh/cm ssh-copy-id -i ~/.ssh/id_ed25519.pub <원격 계정>@100.x.x.x
3단계: 동기화 스크립트
핵심은 토글이 아니라 설정이라는 점입니다. 원격을 “뒤집으면” 두 쪽이 어긋났을 때 어긋난 채로 남습니다. 로컬 값을 읽어 원격을 같은 값으로 맞추면 한 번 누르는 것만으로 정렬됩니다.
#!/bin/bash
# ~/bin/hanyeong-sync.sh
TIS=$HOME/bin/tis
ABC="com.apple.keylayout.ABC"
KOR="com.apple.inputmethod.Korean.2SetKorean"
cur=$("$TIS" | /usr/bin/sed -n 's/^current: //p')
case "$cur" in
*Korean*|*Hangul*) target="$ABC" ;;
*) target="$KOR" ;;
esac
"$TIS" "$target" >/dev/null # 로컬
/usr/bin/ssh jumpremote "~/bin/tis $target" # 원격을 같은 값으로
4단계: Karabiner 규칙
~/.config/karabiner/karabiner.json의 complex_modifications.rules에 넣습니다. Jump가 맨 앞일 때와 아닐 때를 나누는 것이 전부입니다.
{
"description": "Caps Lock = 한/영 (Jump 안: 로컬+원격 동시 / 밖: 로컬만)",
"manipulators": [
{
"type": "basic",
"from": { "key_code": "caps_lock" },
"to": [{ "shell_command": "/Users/<나>/bin/hanyeong-sync.sh" }],
"conditions": [{
"type": "frontmost_application_if",
"bundle_identifiers": ["^com\\.p5sys\\.jump\\.mac\\.viewer\\.web$"]
}]
},
{
"type": "basic",
"from": { "key_code": "caps_lock" },
"to": [{ "key_code": "spacebar", "modifiers": ["left_control"] }]
}
]
}
설정이 반영됐는지는 로그로 확인합니다. core_configuration is updated.가 찍히면 파싱 성공입니다.
tail -1 /var/log/karabiner/core_service.log
함정 넷 — 제가 전부 밟았습니다
1. Jump 안에서 Ctrl+Space는 로컬에 닿지 않는다
가장 오래 헤맨 함정입니다. Jump가 맨 앞이면 Ctrl+Space를 Jump가 가로채 원격으로 흘려버립니다. 그래서 로컬 입력 소스가 바뀌지 않습니다. 화면 표시만 바뀐 것처럼 보여서 더 헷갈립니다.
22:57:32 로컬=EN ← Caps Lock 눌렀는데 22:57:38 로컬=EN ← 두 번째도 안 바뀜
그래서 Jump 안에서는 Ctrl+Space를 쓰지 않고 TIS API로 직접 바꿉니다. Jump 밖에서는 Ctrl+Space가 정상 동작하니 그대로 씁니다.
2. 로컬을 영문으로 고정하면 한글 입력이 아예 막힌다
“로컬 IME가 개입하지 못하게 영문으로 못 박고, 조합은 원격 IME에 맡기자”는 설계가 그럴듯해 보입니다. 하지만 그러면 한글로 갈 경로 자체가 사라집니다. Jump 창으로 포커스가 갈 때마다 영문으로 되돌리는 감시자까지 붙이면, 영어는 깔끔하게 되고 한글은 영영 안 나옵니다.
저는 이 상태를 만들어 놓고 “Jump로 한글 입력은 구조적으로 불가능하다”고 결론 내렸습니다. 원래 되던 기능을 제가 막아놓고 불가능하다고 한 것입니다. 원래 되던 게 안 되면 자기 변경부터 의심해야 합니다.
3. 앱별 입력 소스 기억은 Jump에 적용되지 않는다
TextInputGlobalPropertyPerContextInput으로 앱마다 입력 소스를 기억시킬 수 있습니다. 그런데 Jump Desktop에는 붙지 않습니다. 앱을 재시작하고 값을 켠 뒤 확인해도 복원되지 않습니다.
이유는 Jump가 키보드를 통째로 캡처하는 앱이라 표준 텍스트 입력 컨텍스트를 만들지 않기 때문입니다. 붙을 대상이 없으니 켜든 끄든 무의미합니다. 참고로 이 값은 -g와 com.apple.HIToolbox 두 도메인에 각각 존재해서, 한쪽만 보면 오진합니다.
4. Hammerspoon으로 키를 대신 눌러줄 수 없다
hs.eventtap.keyStroke가 만드는 합성 이벤트는 macOS 시스템 단축키를 발동시키지 못합니다. Finder에서 합성 Ctrl+Space를 보내도 입력 소스가 바뀌지 않습니다. Karabiner는 가상 HID 장치로 주입하기 때문에 물리 키와 동일하게 취급되고, 이것이 유일하게 믿을 수 있는 경로입니다.
이 성질 때문에 “Jump가 Ctrl+Space를 가로챈다”는 결론을 합성 이벤트로 검증하려다 정반대로 판단한 적이 있습니다. 이 부류는 실제 타건 기록으로만 판정할 수 있습니다.
검증: 화면을 믿지 말고 바이트를 보세요
이 문제에서 가장 중요한 교훈입니다. 메뉴바 표시도, defaults read 값도 “한글”이라고 말하는데 실제로는 영문이 전송되는 상황이 반복됐습니다. 원격에 도착한 바이트만이 신뢰할 수 있는 신호입니다.
원격 터미널에서 cat > /tmp/typed.txt를 실행하고 rk를 입력한 뒤, SSH로 결과를 읽습니다.
hexdump -C /tmp/typed.txt ea b0 80 0a → 가 U+AC00 ✅ 성공 72 6b 0a → rk ❌ 원격이 영문 상태 ㄱㅏ → ❌ 자모 분리
터미널은 IME 조합을 가장 단순하게 받는 환경이라 GUI 앱보다 깨끗한 측정 대상입니다. 실제 사용 앱에서도 한 번 더 확인하세요.
계측을 붙여두면 훨씬 빨라집니다
스크립트 끝에 한 줄만 넣으면, 규칙이 실제로 발동했는지 · 로컬이 바뀌었는지 · 원격이 받았는지를 시각과 함께 볼 수 있습니다.
echo "$(date '+%H:%M:%S') 로컬=$after 원격=$out" >> ~/bin/hanyeong.log
앞서 나온 로컬=EN이 두 번 찍힌 기록이 바로 이것입니다. 이 한 줄이 없었으면 “Jump가 Ctrl+Space를 가로챈다”는 사실을 훨씬 늦게 알았을 것입니다.
알아둘 점
- Tailscale이 끊기면 원격 전환이 안 됩니다. 그때는 예전처럼 마우스로 원격 메뉴바를 클릭하면 되고, 연결이 돌아오면 자동 복구됩니다
- 독립 배포판 Tailscale은 GUI 앱이 곧 연결입니다. 별도 데몬이 없어서, 앱을 종료하면 SSH도 끊깁니다
- Karabiner의
shell_command는 환경변수가 거의 없는 상태로 실행됩니다. 반드시 절대 경로를 쓰세요 from에optional: ["any"]를 쓰지 마세요. 모디파이어를 함께 실어 보내 원격에서 전체선택·커서이동 같은 오작동을 일으킵니다- Karabiner-EventViewer를 켜둔 채 진단하지 마세요. 실행 중에는 Karabiner 변환이 일시 비활성화되어, 정상 상태에서도 문제가 있는 것처럼 보입니다
- 원격 맥이 잠들면 전부 멈춥니다. macOS는 전원이 연결돼 있어도 기본이 1분 뒤 잠자기라, 앞단 프로그램이 끝나면 그대로 잠듭니다. 원격 맥에서
sudo pmset -c sleep 0 disksleep 0으로 해제하세요(전원 연결 시에만 적용, 배터리는 그대로) - 원격 맥에 따로 설치할 서버는 없습니다. SSH(22)와 화면 공유(5900) 모두 macOS 내장 기능이고, 시스템 설정 → 일반 → 공유에서 켜면 됩니다
- Tailscale을 쓰면 공유기를 건드릴 필요가 없습니다. 포트포워딩+DDNS로도 되지만 공인 IP가 바뀌고 22번이 인터넷에 노출됩니다. Tailscale은 주소가 고정되고 두 기기 사이만 직접 연결됩니다(실측 왕복 약 16ms)
- Tailscale 위에 SSH 터널을 또 씌우지 마세요. 이미 전 구간이 암호화되어 중복입니다. 화면 공유는
100.x.x.x:5900으로 바로 붙이면 됩니다. 다만 원격에서127.0.0.1에만 열린 포트(개발 서버 등)는 Tailscale로 닿지 않아ssh -L이 필요합니다
덤: 앱이 이상하게 동작한다면
Jump Desktop을 ~/Downloads에서 바로 실행하고 있다면, macOS가 app translocation으로 매번 임의의 읽기 전용 경로에 띄웁니다. 경로가 계속 바뀌니 입력 모니터링·손쉬운 사용 권한이 유지되지 않습니다.
ps -Ao pid,comm | grep "Jump Desktop.app/Contents/MacOS" → 경로에 AppTranslocation 이 보이면 문제
/Applications로 옮기고 quarantine을 제거해야 합니다. 최신 macOS에서는 xattr -dr이 듣지 않는 경우가 있어, 확장속성 없는 복사본을 만들어 교체하는 편이 확실합니다.
ditto --noextattr --norsrc /Applications/"Jump Desktop.app" /Applications/"Jump Desktop-clean.app" codesign --verify --deep --strict /Applications/"Jump Desktop-clean.app" # 서명 확인 필수
나중에 또 안 될 때 — 위에서부터 순서대로
설정을 다 해두고도 어느 날 갑자기 안 되는 경우가 있습니다. 대부분 설정이 망가진 게 아니라 연결이 끊긴 것입니다. 아래 순서대로 확인하시면 됩니다. 위쪽일수록 실제로 자주 일어납니다.
- 원격 맥이 잠들었나? — 압도적 1순위입니다. 원격 맥의 화면을 깨우고,
sudo pmset -c sleep 0 disksleep 0을 아직 안 했다면 지금 하세요 - Tailscale 앱이 떠 있나? — 독립 배포판은 앱을 끄면 연결도 끊깁니다. 양쪽 맥 모두 확인
- SSH가 붙나? —
ssh jumpremote 'echo ok'가ok를 돌려주면 통로는 정상 - Karabiner가 살아 있나? —
systemextensionsctl list | grep pqrs가activated enabled여야 합니다. macOS 업데이트 후 풀리는 일이 있습니다 - 규칙이 발동은 하나? —
tail ~/bin/hanyeong.log. 아무 줄도 안 늘면 Karabiner 문제, 줄은 느는데로컬=EN만 반복되면 입력 소스 전환 문제 - 마지막은 바이트로 — 위 「검증」 절의
hexdump로 실제 도착 바이트를 확인
포트가 열렸는지 볼 때 lsof는 쓰지 마세요. sudo 없이 돌리면 열려 있는 포트를 “닫혔다”고 잘못 보고합니다. nc -z 100.x.x.x 22나 netstat -an -p tcp | grep LISTEN으로 판정하세요.
직접 하기 번거로우면, AI에게 이대로 던지세요
이 글 주소와 함께 아래 문장을 그대로 복사해 주시면 됩니다. 위 6단계를 순서대로 짚도록 쓰여 있습니다.
Jump Desktop으로 원격 맥에 한글을 입력하던 게 갑자기 안 돼. 이 글에 설정 방법과 재발 점검 순서가 있어. 나는 제어(뷰어) 쪽 맥이고, 이미 설정은 해둔 상태야.
글의 「나중에 또 안 될 때」 순서대로 1번부터 확인해줘. 원격 맥 잠자기 → Tailscale → SSH → Karabiner 승인 →~/bin/hanyeong.log→ 도착 바이트 순서로.
확인할 때 화면 표시나defaults read값은 믿지 말고, 원격에 실제로 도착한 바이트로 판정해줘. 포트 확인은lsof대신nc를 써줘. 설정 파일을 고치기 전에는 먼저 백업하고, 어느 단계에서 끊겼는지 알려줘.
고쳐야 할 게 나오면 그때 처음 설정할 때 쓴 프롬프트(맨 위 「읽다가 막히면」)로 넘기시면 됩니다. 두 프롬프트 모두 어느 쪽 맥인지와 바이트로 검증을 못 박아 두었는데, 이 두 가지를 빠뜨리면 엉뚱한 기계를 고치거나 고쳐지지도 않은 걸 고쳤다고 판단하기 때문입니다.
마무리
원격 데스크톱의 IME 문제는 “키를 어떻게 전달할까”로 접근하기 쉽습니다. 하지만 전달 경로가 불안정하면 그 위에 무엇을 쌓아도 간헐적으로 실패합니다. 두 기계 사이에 다른 통로가 이미 있다면, 키 대신 명령을 보내는 편이 훨씬 견고합니다.
그리고 진단할 때는 화면에 보이는 상태 표시를 믿지 마세요. 이 문제에서 표시와 실제가 어긋난 경우가 여러 번 있었고, 그때마다 엉뚱한 곳을 고쳤습니다.
댓글 남기기