Wails로 데스크톱 앱 만들기 #5 실전 기능 — 설정 저장과 에러 처리
“Wails로 데스크톱 앱 만들기 #4 시스템 통합 — 다이얼로그·메뉴·창 제어"까지 만든 할 일 앱에는 치명적인 공백이 하나 있습니다. 앱을 끄면 데이터가 사라집니다. 목록이 Go 구조체의 메모리에만 있기 때문입니다. 이번 글은 이 앱을 실제로 쓸 수 있는 수준으로 끌어올리는 실전 기능 세 가지, 곧 데이터 영속화, 에러 처리, 로그를 다룹니다.
총 8편(본편 6편 + 심화 2편)입니다.
- #1 Wails란 — Go로 만드는 가벼운 데스크톱 앱
- #2 프로젝트 구조와 개발 루프 — wails dev와 바인딩
- #3 Go와 프론트엔드 연결 — 메서드 바인딩과 이벤트
- #4 시스템 통합 — 다이얼로그·메뉴·창 제어
- #5 실전 기능 — 설정 저장과 에러 처리 ← 이번 글
- #6 빌드와 배포 — 플랫폼별 패키징
어디에 저장하는가 — os.UserConfigDir #
첫 질문은 저장 위치입니다. 실행 파일 옆에 파일을 쓰는 방식은 설치 경로에 쓰기 권한이 없는 경우(Windows의 Program Files, macOS의 Applications)가 많아 배포 후 곧바로 깨집니다. 각 OS가 앱 데이터용으로 정해 둔 경로를 써야 하고, Go 표준 라이브러리의 os.UserConfigDir가 그 경로를 돌려줍니다.
| OS | os.UserConfigDir() 반환 경로 |
|---|---|
| macOS | ~/Library/Application Support |
| Windows | %AppData% (C:\Users\<이름>\AppData\Roaming) |
| Linux | ~/.config (XDG_CONFIG_HOME) |
이 아래에 앱 이름으로 디렉터리를 만들어 그 안에 저장합니다. 플랫폼 분기 코드 없이 세 OS 에서 같은 코드가 올바른 위치를 가리킵니다.
Store — JSON 저장과 로드 #
할 일 목록을 파일로 다루는 작은 저장소 타입을 만듭니다. 형식은 JSON이면 충분합니다.
type Store struct {
path string
}
func NewStore(appName string) (*Store, error) {
base, err := os.UserConfigDir()
if err != nil {
return nil, err
}
dir := filepath.Join(base, appName)
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
return &Store{path: filepath.Join(dir, "todos.json")}, nil
}
func (s *Store) Load() ([]Todo, error) {
data, err := os.ReadFile(s.path)
if errors.Is(err, os.ErrNotExist) {
return []Todo{}, nil // 첫 실행 — 빈 목록으로 시작
}
if err != nil {
return nil, err
}
var todos []Todo
if err := json.Unmarshal(data, &todos); err != nil {
return nil, err
}
return todos, nil
}
func (s *Store) Save(todos []Todo) error {
data, err := json.MarshalIndent(todos, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.path, data, 0o644)
}첫 실행에서 파일이 없는 것은 오류가 아니라 정상 상태이므로, os.ErrNotExist를 구분해 빈 목록을 돌려줍니다. 이 구분이 없으면 첫 실행마다 오류 알림이 뜨는 앱이 됩니다.
startup과 shutdown — 수명 주기에 연결 #
저장소를 앱의 수명 주기에 연결합니다. #3에서 본 startup 훅에서 로드하고, 종료 훅에서 저장합니다.
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
store, err := NewStore("wails-todo")
if err == nil {
a.store = store
a.todos, _ = store.Load()
}
}
func (a *App) shutdown(ctx context.Context) {
if a.store != nil {
a.store.Save(a.todos)
}
}shutdown은 options.App의 OnShutdown에 등록합니다. 다만 강제 종료나 크래시에서는 shutdown 이 실행되지 않을 수 있으므로, 항목 추가·삭제처럼 데이터가 바뀌는 시점마다 저장하는 편이 실전에서는 더 안전합니다. 할 일 목록 수준의 데이터라면 매번 저장해도 비용이 체감되지 않습니다.
에러 처리 — Go의 error가 알림이 되기까지 #
바인딩된 Go 메서드가 error 를 반환하면, 프론트엔드에서는 거부된 Promise가 됩니다. 이 연결이 에러 처리 설계의 기반입니다.
func (a *App) AddTodo(title string) ([]Todo, error) {
title = strings.TrimSpace(title)
if title == "" {
return nil, fmt.Errorf("할 일 내용을 입력해 주세요")
}
a.todos = append(a.todos, Todo{Title: title})
if err := a.store.Save(a.todos); err != nil {
return nil, fmt.Errorf("저장에 실패했습니다: %w", err)
}
return a.todos, nil
}try {
const todos = await AddTodo(input.value);
render(todos);
} catch (err) {
showToast(String(err)); // Go 가 반환한 에러 메시지
}여기서 오류를 두 부류로 나누는 기준을 세워 두면 코드가 정리됩니다.
- 예상 가능한 오류 — 빈 입력, 잘못된 형식처럼 사용자가 고칠 수 있는 문제입니다. 사용자에게 보여 줄 문장으로 메시지를 만들어 반환합니다.
- 예상 밖 오류 — 디스크 쓰기 실패, 손상된 파일처럼 사용자가 어쩔 수 없는 문제입니다. 사용자에게는 짧은 안내만 보여 주고, 상세 내용은 로그로 남깁니다.
로그 — 배포 후를 위한 준비 #
개발 중에는 터미널이 보이지만, 배포된 앱에는 터미널이 없습니다. 문제가 생겼을 때 들여다볼 기록을 파일로 남겨 둡니다. Wails 는 로거를 교체할 수 있는 옵션을 제공합니다.
import "github.com/wailsapp/wails/v2/pkg/logger"
err := wails.Run(&options.App{
Title: "todo",
Logger: logger.NewFileLogger(logPath), // 설정 디렉터리 아래 경로 권장
// ...
})앱 코드에서는 runtime.LogInfo(ctx, ...), runtime.LogError(ctx, ...) 계열 함수로 기록합니다. 예상 밖 오류를 만나는 지점마다 LogError 한 줄을 남겨 두면, 사용자에게 로그 파일 하나만 받아도 원인 파악이 가능해집니다.
외부 API 호출은 Go 쪽에서 #
날씨, 환율처럼 외부 API 를 부르는 기능이 필요하다면 프론트엔드의 fetch 대신 Go의 net/http 로 호출해 바인딩 메서드로 노출하는 구성을 권합니다. 이유는 두 가지입니다.
- CORS 가 없습니다. WebView 의 fetch 는 브라우저와 같은 출처 제약을 받지만, Go 의 HTTP 클라이언트는 일반 프로그램의 네트워크 요청이라 제약이 없습니다.
- 키가 숨겨집니다. API 키를 프론트엔드 코드에 두면 배포 파일에서 그대로 노출됩니다. Go 쪽에 두면 바이너리 안에 있고, 요청 조립도 서버 코드처럼 다룰 수 있습니다.
마무리 #
이번 글의 핵심은 세 가지입니다.
- 데이터는
os.UserConfigDir아래 앱 디렉터리에 저장합니다. 세 OS 에서 같은 코드가 올바른 경로를 가리킵니다. - Go 메서드의 error 는 프론트엔드의 거부된 Promise 가 됩니다. 사용자가 고칠 수 있는 오류는 문장으로, 예상 밖 오류는 로그로 보냅니다.
- 외부 API 호출은 Go 쪽에 두면 CORS 와 키 노출 문제가 줄어듭니다.
이제 앱이 기능적으로 완성됐습니다. 다음 글인 “Wails로 데스크톱 앱 만들기 #6 빌드와 배포 — 플랫폼별 패키징"에서는 이 앱을 실행 파일로 만들어 다른 사람의 컴퓨터에 전달하는 마지막 단계를 다루고 시리즈를 마치겠습니다.