SSRmeans the server renders the page and sends finished HTML, so it shows up before any JavaScript runs. Here the same app boots on the server without a browser, and the browser never renders it a second time. On top of that,RPC(calling a server function as if it were local) works with just an interface and a Symbol.
SSR(서버 사이드 렌더링)은 서버가 화면을 미리 그려서 완성된 HTML로 보내는 방식이에요. 그래서 자바스크립트가 돌기 전에 이미 화면이 보여요. 여기선 같은 앱이 브라우저 없이 서버에서 부팅되고, 브라우저는 그걸 두 번 그리지 않습니다. 그리고RPC(서버 함수를 내 함수처럼 부르기)를 인터페이스 하나와 Symbol만으로 합니다.
How a request becomes a page.
요청이 페이지가 되기까지.
The server draws it. That's it.
The browser never draws it twice.
서버가 그리면 끝.
브라우저는 두 번 안 그립니다.
The same app boots insidedom-parseron the server — same boot function, same components, no headless Chrome.@onConnected*render methods run there, every connected element is stampedswc-use-ssr="<id>",
and shadow roots are written out asDeclarative Shadow DOM.
같은 앱이 서버의dom-parser위에서 부팅됩니다 — 같은 부팅 함수, 같은 컴포넌트, 헤드리스 크롬 없이.@onConnected*렌더 메서드가 거기서 돌고, 연결된 엘리먼트마다swc-use-ssr="<id>"가 찍히고,
섀도우 루트는Declarative Shadow DOM으로 나갑니다.
<user-menuswc-use-ssr="s8k2j1x"><templateshadowrootmode="open"><style>button{ … }</style><buttonclass="me">Kim</button></template></user-menu><template shadowrootmode>— the page is paintedbefore any JavaScript.HTML 파서가 DOM을 만들고<template shadowrootmode>로 섀도우 루트까지 붙입니다 —자바스크립트 전에화면이 다 그려져요.customElements.define(); the existing elements upgrade in place.번들이customElements.define()을 부르면 이미 있는 엘리먼트가 그 자리에서 업그레이드됩니다.connectedCallbackseesswc-use-ssr: itskips the@onConnected*render, keeps the DOM and reuses the id.connectedCallback이swc-use-ssr를 보면@onConnected*렌더를 건너뛰고DOM과 id를 그대로 씁니다.@event, observers, message and route subscriptions, timers. Then the mark is removed, so later reconnects render normally.행위만 붙습니다 —@event, 옵저버, 메시지·라우트 구독, 타이머. 그리고 표시를 지워서 이후 재연결은 평소대로 렌더해요.No second render pass — so there isnothing to mismatch.두 번째 렌더가 없으니어긋날 것도 없습니다.
A value set on an elementbeforecustomElements.define()stays on it after the upgrade — that's the spec.
The server collects every@propertyfield and embeds a tiny script that sets them on the not-yet-upgraded elements.
When your component wakes up, the data isalready there: no fetch, no store, no keys.
customElements.define()전에엘리먼트에 넣은 값은 업그레이드 뒤에도 남습니다 — 스펙이에요.
서버가@property필드를 모아 작은 스크립트로 박고, 그 스크립트가 아직 업그레이드 전인 엘리먼트에 값을 넣습니다.
컴포넌트가 깨어나면 데이터는이미 와 있어요: fetch도 스토어도 키도 없이.
sequenceDiagram participant S as server (dom-parser) participant P as HTML parser participant E as user-menu participant J as your bundle S->>S: render · this.me = await auth.me() S->>P: HTML + inline hydration script P->>E: el.me = value (not upgraded yet) J->>E: customElements.define → upgrade Note over E: own property survives the upgrade E->>E: this.me ??= … → skipped, no request
<!-- embedded by the server, runs before your bundle --><script>window.__swc_hydration= [{sel:'[swc-use-ssr="s8k2j1x"]',prop:'me',value: {"name":"Kim"}
}];for(consthofwindow.__swc_hydration)document.querySelector(h.sel)[h.prop] = h.value;</script>@property// a hydration targetdeclareme:User|null;// declare: no initializeroverrideasynconSwcAppConnected(@inject(AuthService.SYMBOL)auth:AuthService,) {this.me??=awaitauth.me();// already here → skipped}@propertymarks the field맨@property로 필드 지정usedeclare— an initializer would overwrite itdeclare로 — 초기값을 주면 덮어씀JSON values only (functions skipped)JSON 값만 (함수는 제외)<escaped — no</script>injection<이스케이프 —</script>주입 차단Press it: we download the raw HTML this page was served as — the bytes before any script runs — and count what is already inside.눌러보세요: 이 페이지가 서빙된 원본 HTML — 스크립트가 돌기 전의 바이트 — 을 받아서 안에 이미 뭐가 들었는지 셉니다.
Zero API routes. Zero fetch code.
One interface, and you're done.
API 라우트 0개. fetch 코드 0줄.
인터페이스 하나면 끝.
Aninterfaceand aSymbollive in shared code. The browser and the server eachimport the same fileand implement it their own way — the browser with a one-line proxy (SymbolIntentApiServiceProxy), the server with the real logic.
Components only ever see the interface.
공유 코드에interface와Symbol하나. 브라우저와 서버가같은 파일을 import해서
각자 구현합니다 — 브라우저는 한 줄짜리 프록시(SymbolIntentApiServiceProxy), 서버는 진짜 로직. 컴포넌트는 인터페이스만 봅니다.
exportinterfaceAuthService{me(...args:any[]):Promise<User|null>;
}exportnamespaceAuthService{// Symbol.for → same key in both bundlesexportconstSYMBOL=Symbol.for('AuthService');
}@Sim({symbol:AuthService.SYMBOL,proxy: intentProxy,// the HTTP bridge})classAuthFrontimplementsAuthService{me(send) {returnsend(); }
}@Sim({symbol:AuthService.SYMBOL})classAuthBackimplementsAuthService{asyncme(rr:RequestResponse) {/* session cookie → User */}
}@onConnectedAfterasyncload(@inject(AuthService.SYMBOL)auth:AuthService,) {this.me=awaitauth.me();
}auth.me()hits the proxy, which turns it intoPOST /mewith the Symbol in thex-simple-boot-ssr-intent-schemeheader.
The server reads the header, finds the@Simregistered underthe same Symboland calls itsme().
The method name is the path. The Symbol is the address.
auth.me()는 프록시를 거쳐x-simple-boot-ssr-intent-scheme헤더에 Symbol을 실은POST /me가 됩니다.
서버는 헤더를 읽고같은 Symbol로 등록된@Sim을 찾아me()를 부릅니다.
메서드 이름이 경로, Symbol이 주소.
sequenceDiagram participant C as user-menu participant F as AuthFront (proxy) participant S as server · IntentSchemeFilter participant B as AuthBack C->>F: auth.me() F->>S: POST /me Note over F,S: header → Symbol.for(AuthService) S->>B: same Symbol → me(rr) B-->>S: User S-->>F: 200 · JSON F-->>C: Promise resolves with User
The same component runs on the server. There, the container hands itAuthBackitself,
sothe very same linebecomes a plain function call — and the current request is appended asrr,
exactly like over HTTP. One signature on the server, either way.
같은 컴포넌트가 서버에서 돕니다. 거기선 컨테이너가AuthBack자체를 주입하니까똑같은 그 한 줄이 그냥 함수 호출이 됩니다 — 현재 요청은 HTTP 때와 똑같이rr로 붙고요.
서버 쪽 시그니처는 어느 길이든 하나.
sequenceDiagram participant C as user-menu (on the server) participant B as AuthBack C->>B: auth.me() Note over C,B: no HTTP · AuthBack itself, rr appended B-->>C: User
The pieces that run behind.
뒤에서 도는 부품들.
Browserless SSR that boots the same app on the server — and skips re-rendering on the client.브라우저 없는 SSR. 서버에서 같은 앱을 부팅하고, 클라이언트는 다시 그리지 않습니다.
An HTTP server that is its own DI container — with symbol RPC: one interface, one call.자체가 DI 컨테이너인 HTTP 서버 — Symbol RPC: 인터페이스 하나, 호출 한 번.
Symbol-based DI with AOP, an intent bus and a router — one object graph, browser or Node.Symbol 기반 DI에 AOP, 인텐트 버스, 라우터 — 브라우저든 Node든 하나의 객체 그래프.