dev.to

Isomorphic hydration in Fitz: first paint on the server, then WASM adopts the DOM

Fitz의 아이소모픽 하이드레이션 — 서버가 첫 화면을 그리고 WASM이 DOM을 그대로 인계받는다

FitzLiveViews 시리즈 8편으로, .fitzv 컴포넌트 하나가 서버에서는 HTML로 렌더링되고 클라이언트에서는 WASM 앱으로 부팅해 서버가 그린 DOM 노드를 재생성 없이 그대로 '입양(adopt)'하는 하이드레이션 구현을 다룹니다. 가상 DOM 재조정이 없어 hydration mismatch 자체가 성립하지 않고, 프레임워크 런타임을 클라이언트에 싣지 않는다는 점이 차별점입니다.

본문

이 글은 .fitzv 컴포넌트를 서버 HTML과 독립 WASM 앱 두 방식으로 컴파일하는 언어/프레임워크 Fitz의 하이드레이션 구현을 설명합니다. 시리즈 7편이 @rpc 서버 함수 호출까지 타입이 이어지는 풀스택 루프를 완성하며 약속했던 SSR 하이드레이션의 후속편입니다.

왜 '재조정'이 아니라 '입양'인가

SSR은 빠른 첫 페인트, SEO, JS 비활성 환경 동작이라는 장점을 주지만, 이후 인터랙티브 런타임이 페이지를 인계받는 과정이 문제입니다. naive한 방식(새로 마운트)은 서버가 그린 DOM을 버리고 다시 만들어 깜빡임과 중복 작업을 유발하고, 프레임워크 방식(가상 DOM 재조정)은 hydration mismatch 경고의 근원이 되며 런타임 전체를 클라이언트에 실어야 합니다. Fitz는 둘 다 하지 않고, WASM 런타임이 서버가 그린 노드를 그대로 입양합니다.

opt-in 마커와 서버 측 산출물

하이드레이션은 루트에 hydrate 마커 하나를 붙이는 opt-in 방식입니다. LiveViews의 WS-takeover 경로는 루트에 <script>를 허용하지 않기 때문에, 마커를 opt-in으로 두어 그 경로의 출력이 바이트 단위로 동일하게 유지되도록 했습니다. 마커가 있으면 SSR 이미터는 ▲컴포넌트 템플릿이 생성하는 그대로의 HTML, ▲<script type="application/json" id="__flv_state_App">{"name":"Ada"}</script> 형태의 상태 페이로드, ▲클라이언트가 순회할 수 있는 입양 마커(혼합 텍스트의 보간 주변 <!--fi-->…<!--/fi-->, {#if}/{#for} 영역 주변 <!--fr-->…<!--/fr-->, 자식 컴포넌트를 감싸는 <div class="__fitz-child-Card"> 래퍼, 부모 스코프에 인라인된 슬롯 콘텐츠)를 남깁니다. 이는 손으로 작성한 index.html이 아니라 서버가 App_render(App { name: "Ada" })를 실제로 계산한 문자열 렌더 결과입니다.

hydrate()의 동작 순서

하이드레이션 가능한 컴포넌트는 루트를 지우고 새로 만드는 mount() 대신 hydrate()를 실행합니다. 루트에 이미 콘텐츠가 있음을 확인한 뒤, <script> 페이로드에서 직렬화된 상태를 복원하고(프리미티브뿐 아니라 List<T>, Map<Str,V>, nullable, 임포트된 nominal 타입도 JSON을 통해 라운드트립), 커서로 기존 DOM을 깊이 우선 순회하며 각 element/text/comment 노드를 빌드 경로가 만들었을 것과 동일한 keep-node 핸들에 매핑합니다 — create_element 없이, wipe 없이 진행됩니다. 이후 @input/@click 리스너를 입양된 노드에 연결하고, 상태 변경 시에는 keep-node 재조정으로 제자리 패치가 일어나므로 <input>의 캐럿 위치가 유지됩니다.

검증과 비교

실제 Chrome에서 Puppeteer로 엔드투엔드 검증했으며, 결정적인 테스트는 init() 실행 전에 서버가 그린 노드에 JS 프로퍼티를 심어두는 것입니다. 하이드레이션 후에도 그 프로퍼티가 남아 있다는 것은 노드가 재생성이 아니라 입양됐다는 증거입니다. 인사말은 템플릿 기본값 "world"가 아닌 서버 상태 "Ada"를 표시하고, 타이핑 시 텍스트가 제자리 패치되며 페이지 오류는 0건입니다. 예제는 hydrate(기본), hydrate-mixed(정적+보간 혼합 텍스트), hydrate-regions({#if}/{#for}), hydrate-composition(<Child /> + 슬롯) 네 가지 형태를 다룹니다.

저자는 하이드레이션 자체는 JS 세계에서 해결된 문제이며 핵심은 비용이라고 말합니다. React/Next.js는 프레임워크 런타임을 싣고 재조정 패스를 돌리며 mismatch가 경고 카테고리가 되고, Astro 아일랜드도 각 아일랜드가 여전히 JS 프레임워크 런타임이며, Phoenix LiveView/Hotwire는 클라이언트가 얇은 패치 레이어에 불과합니다. 반면 Fitz의 클라이언트는 자체 상태를 가진 진짜 컴파일된 WASM 앱이 서버 DOM 전체를 인계받습니다.

한계와 다음 계획

범용 자동 하이드레이션은 향후 과제이고, JSON을 라운드트립하지 못하는 타입(Str이 아닌 키의 Map, 튜플, 함수)은 복원 시 기본값으로 리셋됩니다. SSR 이미터의 named slot과 {#if}/{#for} 내부 동적 컴포지션도 이후 작업으로 남겨뒀습니다. 다음 편에서는 플래그십 관리자 패널의 기반이 되는 약 40개 컴포넌트 규모의 동반 UI 라이브러리를 다룰 예정입니다.

dev.to 반응

아직 댓글이 없습니다(커뮤니티 점수 0, 댓글 0).

원문: dev.to / 번역·요약: Trawling