Wails로 데스크톱 앱 만들기 #4 시스템 통합 — 다이얼로그·메뉴·창 제어
“Wails로 데스크톱 앱 만들기 #3 Go와 프론트엔드 연결 — 메서드 바인딩과 이벤트"까지의 작업은 전부 창 안에서 끝났습니다. 버튼을 누르면 Go 메서드가 실행되고 결과가 화면에 반영되는 흐름은 갖췄지만, 이대로라면 브라우저 탭에서 돌아가는 웹 앱과 차이가 없습니다. 데스크톱 앱을 데스크톱 앱답게 만드는 것은 OS와의 통합입니다. 파일을 여는 네이티브 다이얼로그, 화면 상단의 메뉴바, 창 크기와 위치의 제어가 이번 글의 주제입니다.
총 8편(본편 6편 + 심화 2편)입니다.
- #1 Wails란 — Go로 만드는 가벼운 데스크톱 앱
- #2 프로젝트 구조와 개발 루프 — wails dev와 바인딩
- #3 Go와 프론트엔드 연결 — 메서드 바인딩과 이벤트
- #4 시스템 통합 — 다이얼로그·메뉴·창 제어 ← 이번 글
- #5 실전 기능 — 설정 저장과 에러 처리
- #6 빌드와 배포 — 플랫폼별 패키징
이번 글에서 다루는 기능은 모두 github.com/wailsapp/wails/v2/pkg/runtime 패키지에 모여 있습니다. 공통 규칙은 하나입니다. 모든 런타임 함수는 첫 인자로 context 를 받습니다. #3에서 startup 훅으로 받아 둔 ctx를 구조체에 보관해 두었다면 준비는 끝난 상태입니다.
네이티브 다이얼로그 — 파일 열기와 저장 #
웹에서 파일을 열려면 <input type="file">을 쓰지만, Wails 앱에서는 OS의 네이티브 다이얼로그를 직접 띄울 수 있습니다. 사용자에게 익숙한 모양이 그대로 나오고, 경로 문자열을 받아 Go에서 바로 파일을 다룰 수 있습니다.
func (a *App) OpenTodoFile() (string, error) {
path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "할 일 파일 열기",
Filters: []runtime.FileFilter{
{DisplayName: "JSON 파일 (*.json)", Pattern: "*.json"},
},
})
if err != nil || path == "" {
return "", err // 사용자가 취소하면 path 가 빈 문자열
}
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(data), nil
}저장은 SaveFileDialog가 담당합니다. 기본 파일명을 제안할 수 있다는 점만 다릅니다.
path, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "내보내기",
DefaultFilename: "todos.json",
})사용자에게 무언가를 묻거나 알릴 때는 MessageDialog를 씁니다. 타입에 따라 아이콘과 버튼 구성이 달라집니다.
choice, err := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog,
Title: "삭제 확인",
Message: "완료된 항목을 모두 삭제할까요?",
})
// macOS 기준 choice 는 "Yes" 또는 "No" 문자열로 돌아옵니다OpenFileDialog는 취소 시 빈 문자열과 nil 에러를 돌려주므로, 빈 문자열 검사를 빠뜨리면 취소가 “빈 경로를 여는 시도"로 이어집니다. 취소를 정상 흐름으로 처리하는 습관이 필요합니다.애플리케이션 메뉴 — 메뉴바와 단축키 #
메뉴바는 menu 패키지로 구성해 앱 옵션에 연결합니다. 메뉴 항목마다 단축키와 클릭 콜백을 지정할 수 있습니다.
import (
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
)
appMenu := menu.NewMenu()
fileMenu := appMenu.AddSubmenu("File")
fileMenu.AddText("Open", keys.CmdOrCtrl("o"), func(_ *menu.CallbackData) {
runtime.EventsEmit(app.ctx, "menu:open")
})
fileMenu.AddSeparator()
fileMenu.AddText("Quit", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
runtime.Quit(app.ctx)
})
err := wails.Run(&options.App{
Title: "todo",
Menu: appMenu,
// ...
})keys.CmdOrCtrl은 macOS에서 Cmd, Windows와 Linux에서 Ctrl로 자동 매핑됩니다. 플랫폼별 분기를 직접 쓸 필요가 없습니다. 메뉴 콜백에서 프론트엔드 동작을 일으키고 싶다면 위처럼 #3에서 다룬 이벤트를 발행하고, 프론트엔드에서 EventsOn으로 받아 처리하는 구성이 깔끔합니다.
menu.AppMenu()와 menu.EditMenu()를 appMenu.Append로 추가해 두면 표준 항목이 채워집니다.창 제어 — 크기, 위치, 상태 #
창에 대한 조작도 런타임 함수로 처리합니다. 자주 쓰는 것을 추리면 다음과 같습니다.
| 함수 | 동작 |
|---|---|
WindowSetTitle(ctx, t) | 타이틀바 텍스트 변경 |
WindowSetSize(ctx, w, h) | 창 크기 변경 |
WindowSetMinSize / WindowSetMaxSize | 크기 제한 |
WindowCenter(ctx) | 화면 중앙 정렬 |
WindowFullscreen / WindowUnfullscreen | 전체 화면 전환 |
WindowGetSize / WindowGetPosition | 현재 크기·위치 조회 |
초기 크기와 최소 크기는 options.App의 Width, Height, MinWidth, MinHeight로 정하는 편이 안정적이고, 런타임 함수는 실행 중 동적으로 바꿀 때 사용합니다. 문서 편집 중이라면 WindowSetTitle로 제목에 수정 표시를 붙이는 식입니다.
창을 닫을 때 마지막 크기와 위치를 저장해 두었다가 다음 실행에서 복원하는 패턴도 이 함수들의 조합으로 만듭니다. WindowGetSize와 WindowGetPosition으로 읽어 설정 파일에 저장하고, startup에서 WindowSetSize와 WindowSetPosition으로 되돌립니다. 저장 자체는 다음 글의 주제입니다.
클립보드 #
클립보드 접근은 두 함수로 끝납니다.
func (a *App) CopyResult(text string) error {
return runtime.ClipboardSetText(a.ctx, text)
}
func (a *App) PasteText() (string, error) {
return runtime.ClipboardGetText(a.ctx)
}브라우저의 클립보드 API가 권한 프롬프트와 보안 컨텍스트 제약을 요구하는 것과 달리, 데스크톱 앱은 OS 수준에서 곧바로 접근합니다. “복사됨” 피드백 같은 UI 처리만 프론트엔드에 남습니다.
기본 브라우저로 열기와 보안 경계 #
앱 안에서 외부 링크를 눌렀을 때 WebView 안에서 페이지가 열리는 것은 대부분 원하는 동작이 아닙니다. 시스템 기본 브라우저로 넘깁니다.
runtime.BrowserOpenURL(a.ctx, "https://wails.io")이 지점에서 Wails 앱의 보안 구조를 한 번 정리해 둘 필요가 있습니다. 프론트엔드는 WebView 안에서 동작하므로 임의로 로컬 파일을 읽거나 프로세스를 실행할 수 없습니다. OS 자원에 닿는 모든 동작은 Go 쪽 메서드를 거칩니다. 무엇을 노출할지는 개발자가 바인딩으로 결정하므로, 파일 경로를 받아 그대로 여는 메서드처럼 범위가 넓은 창구를 만들 때는 검증을 함께 두는 것이 안전합니다.
마무리 #
이번 글의 핵심은 세 가지입니다.
- OS 통합 기능은
runtime패키지에 모여 있고, 모든 함수가 context 를 첫 인자로 받습니다. - 다이얼로그는 취소(빈 문자열)를 정상 흐름으로 처리하고, 메뉴는 이벤트 발행으로 프론트엔드와 연결합니다.
- 로컬 자원 접근은 전부 Go 메서드를 거치므로, 바인딩 설계가 곧 앱의 보안 경계가 됩니다.
다음 글인 “Wails로 데스크톱 앱 만들기 #5 실전 기능 — 설정 저장과 에러 처리"에서는 할 일 데이터를 앱을 껐다 켜도 남게 만들고, Go의 error 가 프론트엔드의 알림까지 이어지는 처리 흐름을 정리하겠습니다.