Wails 실전 강좌 #4 트레이 상주와 전역 단축키 — 백그라운드에서 빠르게 캡처
지금까지의 노트 앱은 창을 닫으면 프로세스가 끝납니다. 메모 도구의 진짜 가치는 필요한 순간 즉시 떠오르는 데 있으므로, 이번 글은 앱을 트레이에 상주시키고 전역 단축키로 어디서든 새 노트를 캡처하는 백그라운드 앱으로 바꿉니다. 여기서 Wails v2의 한 가지 제약을 정직하게 다루고 넘어갑니다.
총 10편(2부 구성) 중 네 번째 글입니다.
- #1 실전 프로젝트 설계 · #2 SQLite 로컬 데이터베이스 · #3 전문 검색과 데이터 흐름
- #4 트레이 상주와 전역 단축키 — 백그라운드에서 빠르게 캡처 ← 이번 글
- #5 서명과 공증 — 배포된 앱이 신뢰받는 법
- #6 CI/CD 자동 릴리스 — GitHub Actions로 세 플랫폼 배포
제약 먼저: v2에는 내장 트레이가 없다 #
솔직하게 시작합니다. Wails v2에는 시스템 트레이 API가 내장되어 있지 않습니다. 이 기능은 #1에서 언급한 대로 v3에서 정식으로 들어옵니다. v2에서 트레이를 쓰려면 외부 Go 라이브러리를 붙여야 하고, 가장 널리 쓰이는 것이 getlantern/systray입니다. 창 관리는 Wails 런타임이, 트레이 아이콘·메뉴는 systray가 맡는 조합입니다.
이 제약을 아는 것이 중요한 이유는, 오래된 튜토리얼이나 AI가 “Wails에 트레이를 붙이자"며 v2에 없는 API를 호출하는 코드를 내놓는 경우가 있기 때문입니다. v2에서는 외부 라이브러리, v3에서는 내장 API — 이 구분을 기준으로 삼습니다.
창 닫기를 종료가 아니라 숨김으로 #
백그라운드 앱의 첫 조건은 창을 닫아도 프로세스가 살아 있는 것입니다. Wails의 OnBeforeClose 훅에서 종료를 막고 창을 숨기면 됩니다.
// main.go의 options.App에 연결: OnBeforeClose: app.beforeClose
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
runtime.WindowHide(ctx) // 창을 숨기고
return true // 실제 종료는 막는다
}true를 돌려주면 Wails가 종료를 취소합니다. 이제 창의 닫기 버튼은 앱을 끄는 대신 트레이로 집어넣는 동작이 됩니다. 실제 종료는 트레이 메뉴의 “종료” 항목에서 runtime.Quit(ctx)로 따로 제공합니다. 이 구분이 없으면 사용자가 앱을 완전히 끌 방법이 사라지므로 반드시 함께 둡니다.
트레이: 아이콘과 메뉴 #
systray는 자체 이벤트 루프를 돌리므로, Wails가 시작된 뒤 별도 고루틴에서 띄웁니다. 트레이 메뉴에는 최소한 “새 노트”, “창 열기”, “종료"를 둡니다.
func (a *App) startTray() {
systray.Run(func() {
systray.SetIcon(trayIcon) // //go:embed로 넣은 아이콘 바이트
systray.SetTooltip("노트")
mNew := systray.AddMenuItem("새 노트", "빠른 캡처 창 열기")
mShow := systray.AddMenuItem("창 열기", "메인 창 보이기")
systray.AddSeparator()
mQuit := systray.AddMenuItem("종료", "앱 종료")
for {
select {
case <-mNew.ClickedCh:
a.openQuickCapture()
case <-mShow.ClickedCh:
runtime.WindowShow(a.ctx)
case <-mQuit.ClickedCh:
runtime.Quit(a.ctx)
return
}
}
}, nil)
}메뉴 항목의 클릭은 각 항목의 ClickedCh 채널로 들어오므로, select로 받아 해당 동작을 실행합니다. 창을 다시 보이는 것은 입문 #4에서 다룬 runtime.WindowShow입니다.
전역 단축키: 앱 바깥에서 캡처 #
메모 도구의 핵심 경험은 다른 앱을 쓰다가도 단축키 한 번으로 즉시 캡처하는 것입니다. 앱이 포커스를 갖지 않은 상태에서도 동작해야 하므로 전역(global) 단축키가 필요한데, 이것도 v2 런타임에 없어 외부 라이브러리(golang.design/x/hotkey 등)를 씁니다.
func (a *App) registerHotkey() {
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyN)
if err := hk.Register(); err != nil {
runtime.LogError(a.ctx, "단축키 등록 실패: "+err.Error())
return
}
go func() {
for range hk.Keydown() {
a.openQuickCapture() // Ctrl+Shift+N 이 눌리면 캡처 창
}
}()
}openQuickCapture는 작은 캡처 창을 앞으로 가져오는 함수입니다. 창을 보이고 포커스를 주면, 사용자는 제목을 입력하고 저장하는 것만으로 노트를 남깁니다. 저장은 #3에서 만든 CreateNote를 그대로 부르고, 이때 발행되는 notes:changed 이벤트가 메인 창의 목록까지 자동으로 갱신합니다. 앱 바깥에서 추가해도 모든 창이 최신 상태가 되는 이 흐름이, #3에서 데이터의 진실을 Go에 둔 설계의 보상입니다.
전역 단축키에는 두 가지 주의가 있습니다. 하나는 다른 앱이 이미 쓰는 조합과 충돌할 수 있다는 것이고(가능하면 사용자가 바꿀 수 있게 둡니다), 다른 하나는 macOS에서 손쉬운 사용(입력 모니터링) 권한을 요구할 수 있다는 것입니다. 첫 실행 시 권한 안내를 준비해 둡니다.
v3에서는 어떻게 달라지나 #
Wails v3는 시스템 트레이를 런타임에 내장하고 멀티 윈도우를 정식 지원합니다. 즉 v3에서는 systray 같은 외부 라이브러리 없이 트레이 아이콘·메뉴를 Wails API로 직접 만들고, 캡처 창도 별도 윈도우로 깔끔하게 띄울 수 있습니다. 이번 글의 개념(닫기를 숨김으로, 트레이 메뉴 구성, 전역 단축키로 캡처, 이벤트로 갱신)은 v3에서도 그대로이고, 달라지는 것은 트레이를 외부 라이브러리 대신 내장 API로 만든다는 점입니다.
정리 #
- Wails v2에는 시스템 트레이와 전역 단축키가 내장되어 있지 않습니다.
getlantern/systray,golang.design/x/hotkey같은 외부 라이브러리로 붙입니다. v3는 트레이를 내장합니다. - 백그라운드 앱의 첫 조건은
OnBeforeClose에서true를 돌려줘 창 닫기를 종료가 아니라 숨김으로 바꾸는 것입니다. 실제 종료는 트레이 메뉴에서 따로 제공합니다. - 트레이 메뉴 항목의 클릭은
ClickedCh채널로 받아select로 처리합니다. 최소 “새 노트·창 열기·종료"를 둡니다. - 전역 단축키는 앱이 포커스가 없어도 동작합니다. 저장은 기존
CreateNote를 그대로 부르고,notes:changed이벤트가 모든 창을 갱신합니다. - 다음 글에서 이 앱을 서명·공증해 다른 사람의 기기에서 경고 없이 실행되게 만듭니다.