멀티윈도우
This content is not available in your language yet.
Suji는 하나의 프로세스에서 여러 브라우저 창을 띄울 수 있음. WindowManager가 창
라이프사이클을 관리하고, 프론트엔드가 __suji__.core IPC로 조작.
창 생성
섹션 제목: “창 생성”권장: @suji/api의 windows API
섹션 제목: “권장: @suji/api의 windows API”import { windows } from '@suji/api';
const { windowId } = await windows.create({ title: 'Second Window', url: 'http://localhost:12300/secondary',});suji.json windows[]와 동일한 옵션 셋을 받는다 — frame/transparent/parent/x/y/alwaysOnTop/resizable/min·maxWidth/min·maxHeight/fullscreen/backgroundColor/titleBarStyle 모두 런타임 지정 가능.
Raw IPC (저수준)
섹션 제목: “Raw IPC (저수준)”@suji/api를 못 쓰는 환경(예: 레거시/테스트)에서는 직접 호출:
const w = await window.__suji__.core(JSON.stringify({ cmd: 'create_window', title: 'Second Window', url: 'http://localhost:12300/secondary',}));// w = { from: "zig-core", cmd: "create_window", windowId: 2 }명명된 창
섹션 제목: “명명된 창”await windows.create({ title: 'Settings', name: 'settings', // WM 등록 이름 — 이후 event.window.name으로 식별 url: 'http://localhost:12300/settings',});같은 name으로 재호출하면 기존 창이 반환됨 (singleton 패턴).
전체 옵션 (런타임)
섹션 제목: “전체 옵션 (런타임)”await windows.create({ title: 'HUD', url: 'http://localhost:12300/overlay', frame: false, // frameless transparent: true, // 투명 배경 (HTML body도 transparent여야) parent: 'main', // 부모 창 이름 — native parent/child attach alwaysOnTop: true, resizable: false, minWidth: 200, maxWidth: 600, backgroundColor: '#00000000', titleBarStyle: 'hiddenInset',});창 제어
섹션 제목: “창 제어”windows.setTitle
섹션 제목: “windows.setTitle”import { windows } from '@suji/api';await windows.setTitle(2, 'New Title');// { from: "zig-core", cmd: "set_title", windowId: 2, ok: true }windows.setBounds
섹션 제목: “windows.setBounds”await windows.setBounds(2, { x: 100, y: 100, width: 1200, height: 800 });- macOS: Cocoa 좌표 자동 변환 (화면 좌상단 기준 Y 입력)
- CEF Views 경로: macOS / Linux / Windows 모두
CefWindow.set_bounds로 적용 - macOS legacy fallback: Cocoa 좌표 변환 후 NSWindow bounds 적용
webContents — 네비게이션 / JS 실행
섹션 제목: “webContents — 네비게이션 / JS 실행”Electron webContents.loadURL/reload/executeJavaScript/getURL/isLoading 대응. 모두 windowId 기반 — 어느 창이든 조작 가능.
windows.loadURL(id, url) — 새 페이지 로드
섹션 제목: “windows.loadURL(id, url) — 새 페이지 로드”import { windows } from '@suji/api';await windows.loadURL(2, 'https://example.com/');windows.reload(id, ignoreCache?) — 현재 페이지 reload
섹션 제목: “windows.reload(id, ignoreCache?) — 현재 페이지 reload”await windows.reload(2); // 일반 reloadawait windows.reload(2, true); // disk 캐시 무시 (Electron reloadIgnoringCache)windows.executeJavaScript(id, code) — 렌더러에서 임의 JS 실행
섹션 제목: “windows.executeJavaScript(id, code) — 렌더러에서 임의 JS 실행”await windows.executeJavaScript(2, 'document.title = "Hello"');fire-and-forget — 응답에 결과(eval value)는 없음, ok만 반환. 결과가 필요하면 JS 측에서 suji.send(channel, value)로 회신.
windows.getURL(id) — 현재 main frame URL
섹션 제목: “windows.getURL(id) — 현재 main frame URL”const r = await windows.getURL(2);// r = { ok: true, url: "https://example.com/" } 또는// { ok: false, url: null } (캐시 미스 — OnAddressChange 갱신 전)windows.isLoading(id) — 로딩 중 여부
섹션 제목: “windows.isLoading(id) — 로딩 중 여부”const r = await windows.isLoading(2);// r = { ok: true, loading: true | false }백엔드 SDK 미러
섹션 제목: “백엔드 SDK 미러”같은 4-A API가 백엔드 4언어 SDK에도 노출되어 있어, handler 안에서 다른 창을 조작할 수 있다 (예: 메인 창의 핸들러가 settings 창을 reload).
// Zig_ = suji.windows.loadURL(2, "https://example.com/");_ = suji.windows.executeJavaScript(2, "console.log('hi')");// Rustsuji::windows::load_url(2, "https://example.com/");suji::windows::execute_javascript(2, "console.log('hi')");// Goimport "github.com/ohah/suji-go/windows"windows.LoadURL(2, "https://example.com/")windows.ExecuteJavaScript(2, "console.log('hi')")// Nodeimport { windows } from '@suji/node';await windows.loadURL(2, 'https://example.com/');await windows.executeJavaScript(2, "console.log('hi')");응답은 모두 {from:"zig-core", cmd, windowId, ok} 형태 (get_url/is_loading은 추가 필드). caller가 JSON 파싱.
Cross-platform: CEF Views 경로에서는 macOS/Linux/Windows 모두 같은 windowId 기반
webContents API를 사용한다. 일부 외형 옵션은 플랫폼별 native window 지원 범위가 다르며,
세부 사항은 아래 외형/속성 표를 따른다.
Electron webContents.{setZoomLevel, getZoomLevel, setZoomFactor, getZoomFactor} 대응. CEF는 zoom_level만 노출 — factor는 코어가 pow(1.2, level) 변환 (Electron 호환).
import { windows } from '@suji/api';await windows.setZoomLevel(2, 1.5); // logarithmic — 0=100%, 1=120%, -1=1/1.2await windows.setZoomFactor(2, 1.2); // linear — 1.0=100%, 1.5=150%const r = await windows.getZoomLevel(2); // { ok, level }const f = await windows.getZoomFactor(2); // { ok, factor }_ = suji.windows.setZoomFactor(2, 1.5);_ = suji.windows.getZoomLevel(2);참고: CEF set_zoom_level은 navigation 시점에 적용 deferred — 즉시 호출한 getZoomLevel이 cache된 0 반환 가능.
DevTools
섹션 제목: “DevTools”Electron webContents.{openDevTools, closeDevTools, isDevToolsOpened, toggleDevTools} 대응.
await windows.openDevTools(2); // 이미 열려있으면 멱등 no-opawait windows.closeDevTools(2);const r = await windows.isDevToolsOpened(2); // { ok, opened }await windows.toggleDevTools(2); // F12 단축키와 동일_ = suji.windows.toggleDevTools(2);F12 / Cmd+Shift+I 단축키: 누른 창의 DevTools만 토글 (멀티 윈도우 sender 식별).
DevTools 안에서 reload 키 (F5 / Cmd+R / Cmd+Shift+R): inspectee(원래 창)을 reload. Electron 호환 — DevTools front-end self-reload 대신 개발자가 진짜 reload하고 싶은 페이지 대상으로. 멀티 윈도우 동시 DevTools도 정확히 매핑 (각 DevTools → 자기 inspectee).
편집 / 검색
섹션 제목: “편집 / 검색”Electron webContents.{undo, redo, cut, copy, paste, selectAll, findInPage, stopFindInPage} 대응. 편집 6은 main frame에 위임, find는 cef_browser_host_t.
await windows.undo(2);await windows.copy(2);await windows.paste(2);await windows.selectAll(2);
// 텍스트 검색 — 첫 호출은 findNext=false, 이후 같은 단어는 true.await windows.findInPage(2, 'hello', { forward: true, matchCase: false, findNext: false });await windows.stopFindInPage(2, /* clearSelection */ true);_ = suji.windows.copy(2);_ = suji.windows.findInPage(2, "hello", .{ .forward = true });find 결과 보고: cef_find_handler_t.OnFindResult를 window:find-result
이벤트({windowId, identifier, count, activeMatchOrdinal})로 발신한다. 현재는
finalUpdate=true 결과만 노출한다.
미노출: set_user_agent 동적 변경은 CEF가 창 settings에 한 번만 노출하므로 현재 미지원.
Electron webContents.printToPDF 대응. CEF는 콜백 기반 async — 코어가 즉시 ok 응답하고 완료 시 window:pdf-print-finished 이벤트({path, success}) 발화. SDK가 path 매칭으로 Promise resolve.
import { windows } from '@suji/api';const { success } = await windows.printToPDF(2, '/tmp/report.pdf');if (success) console.log('saved');// Zig — fire-and-forget 후 listener_ = suji.windows.printToPDF(2, "/tmp/report.pdf");// 별도 핸들러에서 `on("window:pdf-print-finished", ...)` 등록let _ = suji::windows::print_to_pdf(2, "/tmp/report.pdf");windows.PrintToPDF(2, "/tmp/report.pdf")capturePage는 CEF 직접 미노출 → CDP Page.captureScreenshot + dev-tools observer 로 구현 완료(file-path 방식, window:page-captured 이벤트).
Linux 주의: PDF 인쇄용 cef_print_handler_t.get_pdf_paper_size(U.S. Letter) 등록 완료. macOS/Windows는 네이티브 인쇄라 무영향. Linux에서도 실제 PDF 파일 생성이 동작한다.
어느 창에서 호출됐는지 식별 — __window wire 자동 태깅
섹션 제목: “어느 창에서 호출됐는지 식별 — __window wire 자동 태깅”프론트엔드가 __suji__.invoke('save', data)를 호출하면 수신 코어가 sender browser를
식별해 request JSON에 자동으로 두 필드를 주입:
{ "cmd": "save", "content": "...", "__window": 2, // 항상 — sender 창의 WM id "__window_name": "settings", // 창에 name이 설정된 경우만 "__window_url": "http://localhost:12300/", // sender 창의 main frame URL (있을 때만) "__window_main_frame": true // sender frame이 main인지 (false면 iframe)}백엔드는 이를 통해 “어느 창에서 호출됐는지”, “어느 페이지에서 호출됐는지” 분기 가능.
__window_url은 Electron event.sender.url과 대응. 4개 SDK 모두
event.window.url 필드로 노출 (Zig: ?[]const u8, Rust: Option<String>, Go: string(빈 = 없음), Node: string | null).
Zig SDK — InvokeEvent
섹션 제목: “Zig SDK — InvokeEvent”fn save(req: suji.Request, event: suji.InvokeEvent) suji.Response { std.debug.print("save from id={d} name={s} url={s}\n", .{ event.window.id, event.window.name orelse "(anonymous)", event.window.url orelse "(none)", }); if (event.window.name) |n| { if (std.mem.eql(u8, n, "settings")) { // settings 창 전용 분기 } } return req.ok(.{});}Rust / Go / Node SDK — InvokeEvent 편의 래퍼
섹션 제목: “Rust / Go / Node SDK — InvokeEvent 편의 래퍼”4개 언어 SDK 모두에 InvokeEvent 자동 주입이 지원된다.
// Rust — proc macro가 파라미터 타입이 InvokeEvent면 wire에서 자동 파생#[suji::handle]fn save(filename: String, event: suji::InvokeEvent) -> serde_json::Value { serde_json::json!({ "from_window": event.window.id })}// Go — reflect 경로가 *suji.InvokeEvent 타입을 감지func (a *App) Save(text string, event *suji.InvokeEvent) suji.M { return suji.M{ "from_window": event.Window.ID }}// Node — handler.length >= 2 이면 event 전달const { handle } = require('@suji/node');handle('save', (data, event) => ({ from_window: event.window.id,}));wire로 직접 파싱도 여전히 가능 (req.__window / req.__window_name). 편의 래퍼와 동등.
이벤트 발신 타겟팅
섹션 제목: “이벤트 발신 타겟팅”기본: suji.send('event', data) 는 모든 창으로 브로드캐스트.
특정 창 하나에만 보내려면 세 번째 옵션 {to: winId} — Electron webContents.send 대응:
// Frontendsuji.send('toast', { text: 'saved' }, { to: 2 });// Zig 백엔드suji.sendTo(2, "toast", "{\"text\":\"saved\"}");// Rustsuji::send_to(event.window.id, "toast", r#"{"text":"saved"}"#);// Gosuji.SendTo(event.Window.ID, "toast", `{"text":"saved"}`)// Nodeconst { sendTo } = require('@suji/node');sendTo(event.window.id, 'toast', { text: 'saved' });to가 생략되거나 0이면 broadcast.- 대상 창이 이미 닫혔으면 silent no-op (Electron과 동일).
- 백엔드 리스너는 target 무관하게 항상 수신 — 필터링은 JS dispatch 레이어에서만. 이 정책 덕분에 로깅/감사 플러그인이 모든 send 이벤트를 놓치지 않음.
전체 창 목록
섹션 제목: “전체 창 목록”현재 별도 API 없음. suji.windows.all() 또는 core({cmd:"list_windows"}) 추가 예정.
창 이벤트
섹션 제목: “창 이벤트”// 창 생성됨window.__suji__.on('window:created', (data) => { // data = { id: 2, name: "settings" }});
// 창 닫힘 직전 (cancelable)window.__suji__.on('window:close', (data) => { // preventDefault는 아직 renderer 측 미지원 — 백엔드에서만 가능});
// 창 닫힘 완료window.__suji__.on('window:closed', (data) => { // data = { id: 2 }});
// 모든 창 닫힘window.__suji__.on('window:all-closed', () => { // Electron canonical: macOS 제외하면 quit});외형 / 속성
섹션 제목: “외형 / 속성”선언적 옵션 (시작 시점 결정, 런타임 변경 X):
{ "windows": [ { "name": "main", "title": "App", "width": 1024, "height": 768 }, { "name": "panel", "title": "Floating Panel", "width": 320, "height": 480, "frame": false, "transparent": true, "parent": "main" } ]}| 옵션 | 의미 | 플랫폼 |
|---|---|---|
frame: false | 타이틀바/리사이즈 핸들 없음 (Electron frame: false). 사용자가 커스텀 타이틀바 구현 필요 | macOS, Linux |
transparent: true | 네이티브 창/View와 CEF browser background를 투명으로 설정. HTML body도 background: transparent 필요 | macOS, Linux |
parent: "<name>" | 부모 창과 시각 관계를 맺음. macOS는 NSWindow child, Linux는 CEF Views parent Window 사용. 부모 close는 자식을 재귀 close하지 않음 | macOS, Linux |
x: <number> / y: <number> | 명시 위치. 둘 다 0이면 OS cascade 자동 분산 (cascadeTopLeftFromPoint:) | macOS, Linux |
alwaysOnTop: true | 항상 다른 일반 창 위. 위젯/툴 팔레트용 | macOS, Linux |
resizable: false | 사용자가 창 크기 조절 불가. 고정 크기 다이얼로그/팔레트 | macOS, Linux |
minWidth / minHeight / maxWidth / maxHeight | 콘텐츠 영역 크기 제한 (0이면 제한 없음) | macOS, Linux |
fullscreen: true | 시작 시 전체화면 진입 | macOS, Linux |
backgroundColor: "#RRGGBB" 또는 "#RRGGBBAA" | 16진수 색. transparent와 함께 쓰면 transparent 우선 | macOS, Linux |
titleBarStyle: "hidden" 또는 "hiddenInset" | 타이틀바 hide + traffic light 유지 (Electron 호환). content view가 타이틀바 영역까지 확장 | macOS |
parent 순서 주의: 부모는 windows[] 배열에서 자식보다 앞에 와야 함 (wm.fromName lookup 시점에 이미 생성돼 있어야). 잘못된 이름이면 stderr에 warn 로그 + parent_id=null로 폴백 (자식은 정상 생성, 시각 관계만 빠짐).
Linux/Windows: Linux는 CEF Views 경로에서 frame:false, drag/no-drag region, transparent, parent, alwaysOnTop, resizable, min/max, fullscreen, backgroundColor를 지원한다. Windows frameless parity는 향후 추가 예정이다.
frameless 사용 시 — drag region (-webkit-app-region)
섹션 제목: “frameless 사용 시 — drag region (-webkit-app-region)”타이틀바가 없으면 창 이동 불가. HTML 측에 drag region을 명시해야 함.
Electron/Tauri와 동일한 표준 — Chromium의 -webkit-app-region CSS 속성.
기본 패턴:
/* 드래그 가능 영역 (마우스 down + 이동으로 창 이동) */.titlebar { -webkit-app-region: drag; app-region: drag; /* 표준 spec, 향후 호환 */}
/* drag 영역 안의 버튼/입력은 클릭 가능하게 — drag 상속 차단 */.titlebar button,.titlebar input { -webkit-app-region: no-drag; app-region: no-drag;}커스텀 타이틀바 + 종료 버튼 풀 데모:
<div class="titlebar"> <div class="title">⌘ My Frameless App</div> <div class="controls"> <button id="quit">×</button> </div></div>
<style> html, body { margin: 0; padding: 0; height: 100%; }
.titlebar { -webkit-app-region: drag; display: flex; align-items: center; justify-content: space-between; height: 32px; padding: 0 8px; background: #2c2c2e; border-bottom: 1px solid #3a3a3c; }
.title { font-size: 12px; font-weight: 600; padding-left: 4px; color: #f5f5f7; }
.controls button { -webkit-app-region: no-drag; width: 24px; height: 22px; border: none; border-radius: 3px; background: #3a3a3c; color: #f5f5f7; cursor: pointer; font-size: 13px; } .controls button:hover { background: #48484a; }</style>
<script> document.getElementById('quit').addEventListener('click', () => { window.__suji__.quit(); });</script>주의:
- drag region은 마우스 down 시점의 hit-test 결과로 결정. 동적으로 z-index를 바꿔서 drag가 가려져도 OS는 원래 영역을 쓴다 → CSS 클래스 토글로 drag/no-drag 전환은 즉시 반영 안 될 수 있음.
<input>,<select>,<a>,<button>은 drag region 안에 있어도 자동으로 no-drag가 되지는 않음 — 명시 필수.- macOS 신호등(min/max/close) 흉내가 필요하면 직접 SVG 그리거나
traffic lightHTML 컴포넌트.
전체 동작 예제: examples/window-styles/ — 메인/frameless panel/transparent HUD 3창이 함께 떠 있는 데모. cd examples/window-styles && suji dev로 실행.
transparent 사용 시 — 주의
섹션 제목: “transparent 사용 시 — 주의”- HTML
body { background: transparent }로 받쳐야 OS 윈도우까지 비침. 안 그러면 페이지 배경색이 보인다. - 컴포지터 비용 증가 — 큰 영역 투명은 GPU 사용량/배터리에 영향.
- macOS에서 투명 영역 클릭은 OS가 hit-test 통과 → 뒤 창에 전달. 마우스 이벤트가 필요한 영역은 불투명 색이라도 채워야.
- focus ring/그림자가 사라지므로 active/inactive 시각 피드백을 사용자가 직접 처리.
안전성 invariants
섹션 제목: “안전성 invariants”- id는 monotonic + non-reusing — 창을 닫아도 id 재사용 안 됨
- JSON-unsafe name은 wire 주입 skip —
",\, control char 포함된 name은__window_name이 생략됨 (id만 주입). JSON 파서 깨짐 방지 - Cross-hop 보존 — 이미
"__window"있는 request는 재주입 안 함 (백엔드-백엔드 호출 시 원 sender 정보 유지) - 부모-자식 재귀 close X — 부모 close해도 자식은 살아있음. orphan 정리는 destroyAll만