Jump Desktop 맥→맥 한/영 전환, Caps Lock 하나로 끝내기

By

Published on

in

맥에서 다른 맥을 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.jsoncomplex_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가 키보드를 통째로 캡처하는 앱이라 표준 텍스트 입력 컨텍스트를 만들지 않기 때문입니다. 붙을 대상이 없으니 켜든 끄든 무의미합니다. 참고로 이 값은 -gcom.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는 환경변수가 거의 없는 상태로 실행됩니다. 반드시 절대 경로를 쓰세요
  • fromoptional: ["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. 원격 맥이 잠들었나? — 압도적 1순위입니다. 원격 맥의 화면을 깨우고, sudo pmset -c sleep 0 disksleep 0을 아직 안 했다면 지금 하세요
  2. Tailscale 앱이 떠 있나? — 독립 배포판은 앱을 끄면 연결도 끊깁니다. 양쪽 맥 모두 확인
  3. SSH가 붙나?ssh jumpremote 'echo ok'ok를 돌려주면 통로는 정상
  4. Karabiner가 살아 있나?systemextensionsctl list | grep pqrsactivated enabled여야 합니다. macOS 업데이트 후 풀리는 일이 있습니다
  5. 규칙이 발동은 하나?tail ~/bin/hanyeong.log. 아무 줄도 안 늘면 Karabiner 문제, 줄은 느는데 로컬=EN만 반복되면 입력 소스 전환 문제
  6. 마지막은 바이트로 — 위 「검증」 절의 hexdump로 실제 도착 바이트를 확인

포트가 열렸는지 볼 때 lsof는 쓰지 마세요. sudo 없이 돌리면 열려 있는 포트를 “닫혔다”고 잘못 보고합니다. nc -z 100.x.x.x 22netstat -an -p tcp | grep LISTEN으로 판정하세요.

직접 하기 번거로우면, AI에게 이대로 던지세요

이 글 주소와 함께 아래 문장을 그대로 복사해 주시면 됩니다. 위 6단계를 순서대로 짚도록 쓰여 있습니다.

Jump Desktop으로 원격 맥에 한글을 입력하던 게 갑자기 안 돼. 이 글에 설정 방법과 재발 점검 순서가 있어. 나는 제어(뷰어) 쪽 맥이고, 이미 설정은 해둔 상태야.

글의 「나중에 또 안 될 때」 순서대로 1번부터 확인해줘. 원격 맥 잠자기 → Tailscale → SSH → Karabiner 승인 → ~/bin/hanyeong.log → 도착 바이트 순서로.

확인할 때 화면 표시나 defaults read 값은 믿지 말고, 원격에 실제로 도착한 바이트로 판정해줘. 포트 확인은 lsof 대신 nc를 써줘. 설정 파일을 고치기 전에는 먼저 백업하고, 어느 단계에서 끊겼는지 알려줘.

고쳐야 할 게 나오면 그때 처음 설정할 때 쓴 프롬프트(맨 위 「읽다가 막히면」)로 넘기시면 됩니다. 두 프롬프트 모두 어느 쪽 맥인지바이트로 검증을 못 박아 두었는데, 이 두 가지를 빠뜨리면 엉뚱한 기계를 고치거나 고쳐지지도 않은 걸 고쳤다고 판단하기 때문입니다.

마무리

원격 데스크톱의 IME 문제는 “키를 어떻게 전달할까”로 접근하기 쉽습니다. 하지만 전달 경로가 불안정하면 그 위에 무엇을 쌓아도 간헐적으로 실패합니다. 두 기계 사이에 다른 통로가 이미 있다면, 키 대신 명령을 보내는 편이 훨씬 견고합니다.

그리고 진단할 때는 화면에 보이는 상태 표시를 믿지 마세요. 이 문제에서 표시와 실제가 어긋난 경우가 여러 번 있었고, 그때마다 엉뚱한 곳을 고쳤습니다.

댓글 남기기


Hey!

Hey there, fellow Robloxian! Whether you’re here to discover hidden gem games, level up your building skills, or just stay in the loop with the latest events, you’re in the right place. This blog is all about sharing the coolest things in the Roblox universe—from developer tips to epic game reviews. So grab your Bloxy Cola, hit that follow button, and let’s explore the world of Roblox together! 🚀


Join the Club

Stay updated with our latest tips and other news by joining our newsletter.


Categories

CS&NEWS에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기