
「Enter로 전송」에 지쳐서, macOS의 Enter 키 동작을 OS 레벨에서 통일하는 앱을 만들었다
요약
macOS에서 앱마다 제각각인 Enter 키 동작을 OS 레벨에서 통일해주는 앱 'Entruce' 개발기를 다룹니다. CGEventTap을 활용해 Enter는 줄바꿈으로, Cmd+Enter는 전송으로 동작하도록 키 이벤트를 가로채 재정의하는 구현 방식을 설명합니다.
핵심 포인트
- 앱마다 다른 Enter 키 동작 문제를 OS 레벨에서 해결
- CGEventTap을 사용하여 키 이벤트를 가로채고 재정의
- 순수 함수로 로직을 분리하여 단위 테스트 가능하도록 설계
- 최상단 앱의 Bundle ID를 확인하여 특정 앱에서만 동작하도록 구현
줄바꿈을 하려다, 전송되어 버린다
채팅 앱에서 줄바꿈을 하려고 Enter를 눌렀더니, 쓰다 만 메시지가 그대로 전송되었다. 아마 많은 사람이 겪어본 경험일 것이다.
일본어 입력의 경우 더욱 까다로운데, 앱에 따라서는 변환 확정(Conversion Confirmation)을 위한 Enter가 그대로 전송이 되기도 한다. 문장 중간에 날아가 버린 메시지에 "죄송합니다, 오전송입니다"라고 덧붙이는 것은 몇 번을 해도 어색하다.
나도 Teams나 LINE에서 이를 반복하다 보니, 이제는 근본적으로 해결하고 싶어졌다.
범인은 「앱마다 제각각인 Enter」
냉정하게 생각하면 잘못된 것은 내 손가락이 아니다. Enter 키의 의미가 앱마다 다른 것이 문제다.
에디터(Editor)에서 Enter는 당연히 줄바꿈이다. 반면, 많은 채팅 앱에서 Enter는 전송이며, 줄바꿈을 위해서는 Shift+Enter와 같은 수식 키(Modifier Key)가 필요하다. 앱에 따라 설정에서 전송 키를 바꿀 수 있기도 하고, 반대로 Enter가 줄바꿈이고 Shift+Enter가 전송인 경우도 있다.
하루 동안 에디터와 채팅 앱을 오갈 때마다, 뇌 속의 「Enter의 의미」를 전환해야 한다. 그리고 전환에 실패한 횟수만큼 오전송이 발생한다.
앱 측 설정으로 전송 키를 바꿀 수 있는 것도 있지만, 모든 앱에 있는 것은 아니다. 브라우저의 Web 버전 채팅이라면 확장 기능(Extension)으로 통일할 수 있지만, Claude나 ChatGPT, Teams를 데스크톱 앱으로 사용하고 있다면 확장 기능이 닿지 않는다.
내가 원하는 것은 이것뿐이다.
Enter는 항상 줄바꿈
전송하고 싶을 때만 Cmd+Enter
어떤 앱에서도. 예외 없이.
OS 레벨에서 Enter를 다시 쓰다
macOS에는 CGEventTap이라는 메커니즘이 있다. 접근성(Accessibility) 권한을 얻으면, 시스템을 흐르는 키 이벤트(Key Event)를 앱에 전달되기 전에 가로채서(Intercept) 다시 쓸 수 있다.
이를 사용하면, 대상 앱이 최상단(Foreground)에 있을 때만 다음과 같은 규칙으로 앱 측에 전혀 손을 대지 않고 동작을 통일할 수 있다.
- Enter (수식 키 없음) → Shift+Enter로 변경 (많은 앱에서 Shift+Enter는 줄바꿈)
- Cmd+Enter → Cmd를 제거한 순수 Enter로 변경 (많은 앱에서 Enter는 전송)
- Shift+Enter → 그대로 통과
이렇게 만든 것이 Entruce다. Enter + truce(휴전). Enter 키와의 싸움에 휴전 협정을 맺는다는 의미를 담았다.

구현의 핵심
중심은 두 가지뿐이다. 변경 규칙을 결정하는 순수 로직(Pure Logic)과 CGEventTap의 콜백(Callback)이다.
규칙 부분은 CGEvent에 의존하지 않는 순수 함수(Pure Function)로 분리했다. 이로써 단위 테스트(Unit Test)를 작성할 수 있다.
/// Pure logic that determines the rewrite result from the key code and modifier keys.
public enum RewriteEngine {
public static func decide(_ input: KeyInput) -> RewriteDecision {
...
콜백 측은 최상단 앱의 bundle ID가 유효 리스트에 들어있을 때만 flags를 변경한다. 키 이벤트는 모든 타건 시 발생하므로, 콜백 내부의 처리는 최소한으로 제한한다.
let flags = event.flags
switch RewriteEngine.decide(input) {
case .passThrough:
...
이벤트를 새로 생성하거나 재전송하지 않는다. 흘러 들어온 이벤트의 수식 키 플래그(Modifier Key Flag)를 변경하여 그대로 통과시킬 뿐이다. 이것이 부작용(Side Effect)이 가장 적다.
운용상의 주의점 하나로, CGEventTap은 콜백 처리가 늦어지면 시스템에 의해 무효화될 수 있다 (tapDisabledByTimeout). 이 이벤트를 감지했을 때 즉시 재활성화하는 처리를 넣어두지 않으면, "어느샌가 작동하지 않는" 앱이 된다.
최대의 장벽: 일본어 IME의 변환 확정
이런 종류의 도구에서 가장 무서운 것은 변환 확정을 위한 Enter까지 바꿔버리는 것이다.
결론부터 말하자면, 대상으로 삼은 채팅 계열 앱에서는 문제가 없었다. 변환 중인 Enter는 IME가 먼저 소비하기 때문에, 앱에 도달하는 단계에서는 일반적인 입력 이벤트로 처리되어 변경의 영향을 받지 않는다.
하지만 터미널은 달랐다. Ghostty나 iTerm2에서는 변환 확정 시의 Enter가 바뀌어 버려, 입력 중인 텍스트가 사라진다. 이는 단순한 오전송을 넘어 데이터 파괴에 가깝다.
그래서 Entruce는 터미널 계열 앱(Ghostty / iTerm2 / Terminal / Alacritty / WezTerm / kitty / Warp)을 블랙리스트(Blocklist)로 코드에 내장하여, 대상에 추가하는 것 자체를 거부하도록 설계했다. "지원하지 않음"이 아니라 "추가할 수 없음"이다. 안전과 직결된 부분은 사용자의 조작 실수로 인해 망가지지 않을 수준까지 철저히 대응하고 싶었다.
검증 과정에서 발견한 함정: 키 할당이 정반대인 앱
실기 검증 과정에서 또 하나의 흥미로운 함정을 발견했다. 바로 Chatwork다.
Chatwork의 기본 설정은 Enter=줄바꿈 / Shift+Enter=전송이다. 즉, Entruce의 변경 규칙과 정반대이며, 만약 이를 활성화하면:
- Enter → Shift+Enter로 변경되어,
줄을 바꾸려던 것이 전송되어 버림 - 변환 확정 시에도 전송이 발생함
오전송을 방지하기 위한 도구가 오전송 제조기가 되는 것이다.
이 경험을 바탕으로 Entruce는 "동작 확인이 완료된 앱만을 프리셋(Preset)으로 동봉하고, 그 외에는 대상으로 삼지 않는다"는 방침을 세웠다. 현재 프리셋은 Claude / ChatGPT / Gemini / Copilot / Manus / Teams / LINE / Discord / Telegram / WhatsApp 등 10개의 앱이다. 모두 실기에서 "Enter 줄바꿈 / Cmd+Enter 전송 / 변환 확정이 깨지지 않음"의 3가지 사항을 확인했다.
사소한 부분이지만, 동일한 제품이라도 bundle ID가 여러 개인 경우도 있었다 (ChatGPT는 com.openai.chat과 com.openai.codex 두 가지). 제품 단위가 아니라 bundle ID 단위로 관리하지 않으면 놓치는 부분이 생긴다.
"키로거(Keylogger) 아닌가?"라는 의구심을 피하기 위해
키 이벤트를 가로채는 도구는 구조상 키로거와 같은 위치에 서 있게 된다. 그렇기에 신뢰할 수 있는 설계는 코드만큼이나 중요하다고 생각했다.
- 네트워크 통신을 일절 하지 않음. 텔레메트리(Telemetry)도, 자동 업데이트 체크도 없다.
- 권한은 손쉬운 사용(Accessibility) 권한만 사용 - 키 입력을 기록하거나 저장하지 않는다. 내용을 변경한 뒤에는 버릴 뿐이다.
- 소스 코드를 전면 공개 (MIT 라이선스) - Developer ID 서명 + Apple 공증(Notarization) 완료
"통신하지 않는다"는 말로만 해서는 증명하기 어렵지만, 소스가 공개되어 있다면 grep으로 확인할 수 있다. 네트워크 관련 API는 코드 어디에도 등장하지 않는다.
완성된 결과물
메뉴 바에 상주하며, 패널을 통해 전체 ON/OFF 및 로그인 시 자동 실행을 설정할 수 있다. 앱별 활성화/비활성화는 상세 설정에서 전환 가능하다. UI는 일본어/영어에 대응한다 (시스템 언어에 따름).
설치는 Homebrew 또는 GitHub Releases를 통해 가능하다:
brew install --cask jncch/tap/entruce
최초 실행 시 손쉬운 사용 권한을 허용하면 즉시 작동하기 시작한다. macOS 14 이상 및 Apple Silicon을 지원한다.
마치며
"Enter로 전송할 것인가, 줄바꿈을 할 것인가"는 작은 문제처럼 보이지만, 매일 확실하게 발생하는 마찰이다. 그리고 오전송은 보낸 본인이 가장 큰 피해를 입는다.
같은 스트레스를 겪고 있는 분들이 사용해 주신다면 기쁘겠다. 지원 앱 추가 요청은 GitHub의 issue를 통해 받고 있다 (실기 검증을 거친 후 추가하는 방침이므로 시간이 다소 걸릴 수 있다).
당신의 Enter 키에도 휴전 협정을.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기