5.5 KiB
Contract: 브릿지 계약 → 환경별 전달 수단 매핑
Created: 2026-08-11 | Updated: 2026-09-11
이 문서의 위치
메시지 계약의 진실원천은 여기가 아니다. 원본은 아래 둘이고, 이 문서는 같은 메시지를 Tauri·참고용 legacy·브라우저가 어떻게 나르는지만 대응시킨다.
specs/001-snippet-palette/contracts/bridge-messages.md— window.hide / paste.code / navigate / snippets.*specs/002-capture-to-chat/contracts/bridge-capture.md— capture.image
메시지의 의미·필드·에러 규칙을 바꿔야 한다면 원본을 먼저 고친다. Tauri 사정만으로 여기서 다른 계약을 만들지 않는다.
1. JS → 데스크톱 앱 (요청)
| 계약 메시지 | 참고용 legacy (.NET) | 기준 앱 (Tauri) |
|---|---|---|
{type:"window.hide"} |
postMessage → type 스위치 |
invoke("window_hide") |
{type:"window.drag"} |
postMessage → type 스위치 |
invoke("window_drag") |
{type:"window.snippetLayout", stage} |
전송하지 않음 | invoke("window_snippet_layout", { stage }) |
{type:"paste.code", text} |
postMessage → type 스위치 |
invoke("paste_code", { text }) |
{type:"route.changed", path} |
postMessage → type 스위치 |
invoke("report_route", { path }) |
{type:"snippets.list", reqId} |
postMessage + reqId 대기 |
invoke("snippets_list") |
{type:"snippets.create", reqId, snippet} |
〃 | invoke("snippets_create", { snippet }) |
{type:"snippets.update", reqId, snippet} |
〃 | invoke("snippets_update", { snippet }) |
{type:"snippets.delete", reqId, name} |
〃 | invoke("snippets_delete", { name }) |
{type:"snippets.recordUse", reqId, name} |
〃 | invoke("snippets_record_use", { name }) |
커맨드 이름 규칙
계약의 type 을 점 → 밑줄, camelCase → snake_case 로 바꾼 것. 기계적이라 외울 게 없다.
snippets.recordUse → snippets_record_use
route.changed → report_route ← 예외 1건
route.changed 만 예외인 이유: 이건 "무엇이 바뀌었다"는 통보라 route_changed 로 하면 커맨드가 아니라 이벤트처럼 읽힌다. 껍데기가 하는 일(보고 받기)을 이름에 담았다.
window.snippetLayout의 단계·크기·복원 계약은 스니펫 원본 계약의 Tauri 절을 따름.
reqId는 Tauri에 없다
legacy는 요청·응답을 reqId로 짝맞춘다. invoke는 Promise를 돌려주므로 Tauri 경로에는 reqId가 실리지 않는다.
그래도 프론트 표면은 같다 — request(type, payload) 시그니처가 유지되므로 snippets.api.ts는 실행 환경을 모른다.
2. 데스크톱 앱 → JS (푸시)
| 계약 메시지 | 참고용 legacy (.NET) | 기준 앱 (Tauri) |
|---|---|---|
{type:"navigate", path} |
PostWebMessageAsJson |
emit("bridge", …) |
{type:"paste.target", name, app} |
〃 | 〃 |
{type:"capture.image", dataUrl} |
〃 | 〃 |
채널은 하나, 페이로드는 동일
// .NET 이 보내는 것
{ "type": "navigate", "path": "/snippet" }
// Tauri 가 보내는 것 — 이벤트 이름만 "bridge", 페이로드는 글자 그대로 같음
{ "type": "navigate", "path": "/snippet" }
메시지 종류마다 이벤트 이름을 나누지 않는다. Tauri 관용으로는 bridge:navigate처럼 쪼개는 게 자연스럽지만, 그러면 프론트 bridgeNavigate.ts의 type 분기를 다시 써야 한다. 한 채널로 몰면 그 분기와 CustomEvent dispatch를 그대로 재사용할 수 있다.
필드 이름은 JS 쪽 표기를 따른다
capture.image 의 dataUrl 은 camelCase 그대로 나간다. Rust 내부 변수명이 무엇이든 직렬화 결과가 계약과 같아야 한다.
3. 오류 표현
| 상황 | 참고용 legacy | Tauri | 프론트가 받는 것 |
|---|---|---|---|
| 정상 | {ok:true, data} |
invoke resolve |
값 |
| 실패 (중복 이름 등) | {ok:false, error:"한글 메시지"} |
invoke reject (한글 메시지) |
Error |
| 아직 안 만듦 | 해당 없음 | invoke reject (한글 "아직 안 됨") |
Error |
세 경우 모두 프론트에서는 Error로 도착해 react-query onError → sonner 토스트로 흐른다. 실행 환경별 분기가 화면에 생기지 않는다.
미구현은 성공을 흉내내지 않는다
Tauri에서 아직 안 만든 기능은 빈 값([], null)으로 답하지 않는다. 구현 중 빠진 기능을 정상 결과와 구분할 수 있어야 한다.
4. 껍데기 없음 (브라우저)
기존 동작 그대로. 계약상 변화 없음.
| 방향 | 동작 |
|---|---|
| JS → 껍데기 (단방향) | 조용히 no-op |
| JS → 껍데기 (응답 필요) | 즉시 reject — "데스크톱 전용" |
| 껍데기 → JS | 안 옴 |
npm run dev 로 브라우저에서 화면만 열어도 안 깨져야 한다 (FR-003).
5. 검증
이 매핑이 맞는지는 아래로 확인한다. 자동화 대상과 수동 대상이 갈린다.
| 항목 | 방법 |
|---|---|
| 호스트 3종 판별 | 자동 (vitest, mock) |
| 브라우저에서 no-op / reject | 자동 (vitest) |
| legacy adapter 동작 | 자동 (기존 브릿지 테스트 3개) |
| Tauri에서 실제 왕복 | 수동 (quickstart.md) |
| Tauri와 브라우저가 같은 화면 결과물 사용 | 빌드 설정 + 실제 실행 확인 |