Wails로 데스크톱 앱 만들기 #3 Go와 프론트엔드 연결 — 메서드 바인딩과 이벤트

5 분 소요

지난 글에서 app.go의 메서드가 wailsjs의 자바스크립트 함수로 노출되는 흐름을 확인했습니다. 이번 글은 그 연결을 본격적으로 씁니다. 바인딩의 규칙을 정확히 정리하고, 할 일 목록 백엔드를 만들어 React 프론트엔드와 연결한 뒤, 반대 방향 통신인 이벤트까지 다룹니다. 예제는 react-ts 템플릿 기준입니다.

총 8편(본편 6편 + 심화 2편)입니다.

  • #1 Wails란 — Go로 만드는 가벼운 데스크톱 앱
  • #2 프로젝트 구조와 개발 루프 — wails dev와 바인딩
  • #3 Go와 프론트엔드 연결 — 메서드 바인딩과 이벤트 ← 이번 글
  • #4 시스템 통합 — 다이얼로그·메뉴·창 제어
  • #5 실전 기능 — 설정 저장과 에러 처리
  • #6 빌드와 배포 — 플랫폼별 패키징

바인딩 규칙 — 무엇이 어떻게 노출되는가 #

Bind에 등록한 구조체에서 프론트엔드로 노출되는 것은 공개 메서드(대문자로 시작)뿐입니다. Go 쪽 시그니처가 자바스크립트 쪽에서 어떻게 보이는지 규칙을 표로 정리합니다.

Go 메서드자바스크립트에서
func (a *App) Do()Do() — Promise<void>
func (a *App) Do() stringDo() — Promise<string> 로 resolve
func (a *App) Do() (string, error)error 가 nil 이면 resolve, 아니면 reject
func (a *App) Do(n int, s string)Do(n, s) — 인자 순서 그대로
소문자 시작 메서드노출되지 않음

핵심은 error의 매핑입니다. Go의 관례대로 마지막 반환값을 error로 두면, 프론트엔드에서는 Promise의 reject로 넘어와 try/catch로 처리할 수 있습니다. 양쪽 언어의 에러 처리 관례가 자연스럽게 이어지는 설계입니다.

실습 — 할 일 목록 백엔드 #

메모리에 할 일을 보관하는 백엔드를 만듭니다. app.go에 타입과 메서드를 추가합니다.

app.go — 할 일 목록 백엔드
type Todo struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

type App struct {
	ctx    context.Context
	todos  []Todo
	nextID int
	mu     sync.Mutex
}

func (a *App) AddTodo(title string) (Todo, error) {
	if strings.TrimSpace(title) == "" {
		return Todo{}, errors.New("제목이 비어 있습니다")
	}
	a.mu.Lock()
	defer a.mu.Unlock()
	a.nextID++
	todo := Todo{ID: a.nextID, Title: title}
	a.todos = append(a.todos, todo)
	return todo, nil
}

func (a *App) ListTodos() []Todo {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.todos
}

func (a *App) DeleteTodo(id int) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.todos = slices.DeleteFunc(a.todos, func(t Todo) bool {
		return t.ID == id
	})
}

일반적인 Go 코드입니다. 데스크톱 앱이라고 특별한 것은 없고, 고루틴에서 접근될 수 있으므로 뮤텍스로 보호한 정도가 전부입니다. 저장이 메모리라 앱을 끄면 사라지는데, 파일로 남기는 것은 #5에서 해결합니다.

프론트엔드에서 호출 #

wails dev를 실행하면 wailsjs에 세 함수가 생성됩니다. React 컴포넌트에서 그대로 가져다 씁니다.

frontend/src/App.tsx — 핵심 부분
import { useEffect, useState } from 'react';
import { AddTodo, ListTodos, DeleteTodo } from '../wailsjs/go/main/App';
import { main } from '../wailsjs/go/models';

function App() {
    const [todos, setTodos] = useState<main.Todo[]>([]);
    const [title, setTitle] = useState('');

    useEffect(() => {
        ListTodos().then(setTodos);
    }, []);

    async function handleAdd() {
        try {
            await AddTodo(title);
            setTitle('');
            setTodos(await ListTodos());
        } catch (err) {
            alert(err);   // Go 가 돌려준 error 메시지
        }
    }
    // ...입력란과 목록 렌더링
}

AddTodo가 빈 제목으로 호출되면 Go 쪽에서 error를 반환하고, 프론트엔드에서는 catch로 잡힙니다. 서버 없이도 백엔드와 프론트엔드가 한 프로세스 안에서 함수 호출로 이어지는 것이 Wails 개발의 감각입니다.

구조체와 TypeScript 모델 #

위 코드에서 main.Todo 타입을 import 한 부분이 눈에 띕니다. Wails는 바인딩된 메서드가 주고받는 구조체를 분석해 TypeScript 모델을 자동 생성합니다(wailsjs/go/models.ts). Go 구조체의 json 태그가 필드 이름을 결정하므로, 태그를 붙여 두면 프론트엔드에서 소문자 필드로 자연스럽게 다룰 수 있습니다. Go 타입을 바꾸면 모델도 다음 빌드에서 갱신되어, 양쪽 타입이 어긋나는 사고를 컴파일 단계에서 잡아 줍니다.

이벤트 — Go에서 프론트엔드로 밀어 보내기 #

바인딩 호출은 항상 프론트엔드가 시작합니다. 반대로 Go 쪽에서 먼저 알려야 하는 상황이 있습니다. 오래 걸리는 작업의 진행률이 대표적입니다. 이때 이벤트 시스템을 씁니다.

app.go — 진행 상황을 이벤트로 전송
import "github.com/wailsapp/wails/v2/pkg/runtime"

func (a *App) ProcessFiles(paths []string) {
	go func() {
		for i, path := range paths {
			process(path)
			runtime.EventsEmit(a.ctx, "progress", map[string]any{
				"done":  i + 1,
				"total": len(paths),
			})
		}
	}()
}
React — 이벤트 수신
import { EventsOn } from '../wailsjs/runtime/runtime';

useEffect(() => {
    const off = EventsOn('progress', (data) => {
        setProgress(data);
    });
    return off;   // 컴포넌트 정리 시 구독 해제
}, []);

EventsEmit의 첫 인자로 쓰인 a.ctx가 #2에서 본 startup의 context입니다. Wails 런타임 함수들은 이 context를 통해 앱과 연결되므로, startup에서 context를 보관하는 관례는 앞으로 계속 등장합니다. 바인딩 호출이 고루틴 안에서 EventsEmit을 부르는 위 구조는 무거운 작업으로 UI가 멈추지 않게 하는 기본 패턴입니다.

노트
이벤트는 프론트엔드에서 Go 로도(EventsEmit), 프론트엔드끼리도 쓸 수 있는 양방향 통로입니다. 다만 요청과 응답이 짝을 이루는 통신은 바인딩 호출이 명확하므로, 이벤트는 “요청 없이 밀어 보내야 하는 알림"에 한정해 쓰는 것이 구조를 단순하게 유지합니다.

마무리 #

이번 글의 핵심은 세 가지입니다.

  • 바인딩은 공개 메서드만 노출하고, 반환값은 Promise resolve로, error는 reject로 매핑됩니다.
  • 구조체는 json 태그 기준으로 TypeScript 모델이 자동 생성되어 양쪽 타입이 동기화됩니다.
  • Go에서 먼저 알려야 하는 정보는 EventsEmit과 EventsOn의 이벤트로 전달하고, 무거운 작업은 고루틴에서 돌리며 진행률을 이벤트로 보고합니다.

다음 글인 “Wails로 데스크톱 앱 만들기 #4 시스템 통합 — 다이얼로그·메뉴·창 제어"에서는 데스크톱 앱다운 기능들을 붙입니다. 파일 열기 다이얼로그, 네이티브 메뉴, 창 제어처럼 브라우저에서는 못 하던 일들을 Wails 런타임으로 처리하겠습니다.

X