# 04. 폴백 전략 비교

> HTML-in-Canvas를 못 쓰는 브라우저(Firefox, Safari, 그리고 플래그/OT 없는 Chrome Stable 전부)에서 **비슷한 인상**을 내는 방법들.
> 핵심 질문 3개로 갈린다: **① 상호작용을 유지하는가 ② 매 프레임 갱신되는가 ③ 렌더된 픽셀을 셰이더가 읽을 수 있는가**

---

## 1. 한눈에 보는 비교표

| 방식 | 원리 | 상호작용<br>유지 | 매 프레임<br>실시간 | 픽셀을<br>셰이더로 | CSS 충실도 | 접근성 | 브라우저 | 비용 |
|---|---|:---:|:---:|:---:|---|---|---|---|
| **A. 네이티브 HTML-in-Canvas** | 브라우저가 요소 렌더링을 캔버스/텍스처로 직접 복사 | ✅ 완전 | ✅ | ✅ | 100% (브라우저 본체) | ✅ 자동 (DOM 그대로) | Chrome 148+ OT / 플래그만 | 낮음 (GPU 경로) |
| **B. `three-html-render` 폴리필** | 네이티브 있으면 fast path, 없으면 `foreignObject` 래스터화 + DOM 오버레이 | ✅ (오버레이가 이벤트 수신) | △ 무효화마다 재래스터 | ✅ | 높음 (브라우저 렌더러 재사용) | ✅ (실 DOM 유지) | 전 브라우저 | 중 (직렬화·이미지 디코드) |
| **C. SnapDOM** | DOM → 인라인 SVG `foreignObject` → 래스터화 | ❌ 정지 이미지 | ❌ | ✅ | **매우 높음** (그라디언트·필터·블렌드·transform 포함) | ❌ 별도 대체 필요 | 전 모던 브라우저 | 낮음 (html2canvas 대비 2~16배 빠름) |
| **D. `foreignObject` 직접 구현** | 직접 SVG 직렬화 → `<img>` → `drawImage` | ❌ | ❌ | ✅ | 높음 (단 폰트·이미지 인라인 직접 처리) | ❌ | 전 모던 브라우저<br>(Safari 제약 있음) | 낮음, 단 구현 부담 |
| **E. html2canvas** | 브라우저 렌더러를 **JS로 재구현**해 DOM을 다시 그림 | ❌ | ❌ | ✅ | **낮음** — 미지원 CSS 목록이 길다 | ❌ | 전 브라우저 | 높음 (느림) |
| **F. CSS3DRenderer (three.js)** | 실제 DOM 요소를 `matrix3d`로 3D 배치 | ✅ 완전 | ✅ | **❌ 불가** | 100% (진짜 DOM) | ✅ | 전 브라우저 | 낮음 |
| **G. `backdrop-filter` + SVG `feDisplacementMap`** | 배경 레이어를 변위 맵으로 굴절 | ✅ (위 콘텐츠 그대로) | ✅ (CSS 합성) | ❌ (고정 필터 셋) | — | ✅ | **Chromium만.** Safari·Firefox는 `backdrop-filter: url()` 미지원 → 블러로 degrade | 낮음~중 (GPU) |
| **H. View Transitions API** | 전/후 **스냅샷 2장**을 CSS로 크로스페이드/클립 | 전환 중 ❌ | 전환 중 ❌ | ❌ | 100% | ✅ | Chrome/Edge/Safari 18+ (Firefox 뒤처짐) | 낮음 |
| **I. 배경 캔버스 오버레이** | HTML 뒤/앞에 별도 `<canvas>`를 깔고 이펙트만 그림 | ✅ | ✅ | **❌ HTML을 못 읽음** | — | ✅ | 전 브라우저 | 낮음 |
| **J. Satori / 서버 렌더** | 서버에서 HTML/CSS → SVG/PNG | ❌ | ❌ | ✅ | 중 (지원 CSS 부분집합) | ❌ | 무관 | 서버 비용 |

---

## 2. 각 방식의 실무 판단

### A. 네이티브 (기준선)
당연히 최선이지만 **오늘 프로덕션 불가**. 나머지 전부는 "A의 어떤 성질을 포기할 것인가"의 문제다.

### B. `three-html-render` 폴리필 — **가장 현실적인 "같은 코드, 전 브라우저" 해법**

`https://github.com/repalash/three-html-render` (MIT). three.js 공식 예제 `webgl_materials_texture_html.html`이 실제로 이걸 쓴다.

```js
if (!('requestPaint' in HTMLCanvasElement.prototype)) {
  const { installHtmlInCanvasPolyfill } = await import('three-html-render/polyfill');
  installHtmlInCanvasPolyfill();
}
```

- **API 표면을 그대로 제공**: `requestPaint()`, `captureElementImage()`, `drawElementImage()`, `texElementImage2D()`, `copyElementImageToTexture()`.
- 내부 동작: 캔버스 자식을 오프스크린 host div로 옮기고 → SVG `foreignObject`로 변환 → `<img>`로 2D 캔버스에 렌더 → 텍스처 업로드. DOM 오버레이를 CSS `matrix3d`로 3D 지오메트리에 정렬해 **포인터 이벤트는 진짜 DOM이 받는다.**
- Chrome Canary에서는 네이티브 `texElementImage2D` fast path를 자동으로 탄다.
- three.js 0.150.0+ 및 standalone WebGL/WebGPU 지원.

**알려진 한계 (README 명시):**
- `textarea` 내부 스크롤이 텍스처에 반영되지 않음
- `contenteditable`의 캐럿/선택 영역이 렌더되지 않음
- 동적 스타일시트는 수동 무효화 필요
- **`:visited`는 폴리필 불가** (브라우저 보안)
- 일부 CSS가 `foreignObject` 컨텍스트에서 다르게 렌더됨

**판정**: three.js 기반 3D UI라면 **이걸 쓰고, 네이티브가 켜지면 자동으로 빨라지는 구조**가 정답.

### C. SnapDOM — **정지 스냅샷이 필요할 때 최선**

`https://snapdom.dev` — DOM을 인라인 SVG `foreignObject`로 직렬화한 뒤 브라우저 네이티브 파이프라인으로 래스터화.
- "브라우저가 화면에 그릴 수 있는 것이면 대체로 캡처된다" — 그라디언트, 필터, 블렌드 모드, transform, 최신 CSS 포함.
- html2canvas 대비 **2~16배 빠름** (복잡한 요소 기준, 단순 요소는 1ms 미만).
- **한계**: 정지 이미지다. 매 프레임 재캡처하면 GC와 이미지 디코딩 비용이 프레임을 잡아먹는다.

**판정**: "클릭 순간에 카드가 산산조각 나며 사라진다" 같은 **일회성 트랜지션**에는 충분하다. 스냅샷을 한 번 뜬 뒤 원본 DOM을 숨기고, 스냅샷 텍스처만 셰이더로 애니메이션하면 A와 시각적으로 구분이 잘 안 된다. 지속적 실시간 왜곡에는 부적합.

### D. `foreignObject` 직접 구현
SnapDOM/html-to-image가 하는 일을 손으로 하는 것. 직접 만들 이유는 거의 없다. 다만 **왜 라이브러리들이 폰트·이미지를 base64로 인라인하는지**는 알아두는 게 좋다 — SVG `<img>`는 외부 리소스를 가져오지 못하고, 외부 리소스가 있으면 **canvas가 taint되어 `getImageData`/WebGL 업로드가 막힌다.**

주요 함정:
- 외부 이미지 → CORS 필요, 아니면 캔버스 taint
- 웹폰트 → `@font-face`의 `src`를 data URI로 인라인해야 함
- `<canvas>` 자식이 taint 상태면 전체 실패
- Safari는 `foreignObject` 안의 일부 CSS 처리가 Chromium과 다르다

### E. html2canvas — **더 이상 기본 선택지가 아니다**
브라우저 렌더링 엔진을 JS로 재구현하는 방식이라, 모든 CSS 속성·레이아웃 예외·텍스트 렌더링 엣지케이스를 손으로 재현해야 한다. 그래서 **"지원하지 않는 CSS 속성" 목록이 길다.** 외부 도메인 이미지는 CORS 헤더 없이는 못 읽는다. 느리다.
**판정**: 레거시 유지보수가 아니면 SnapDOM 또는 html-to-image로 갈아타라.

### F. `CSS3DRenderer` — **"3D 배치"만 필요하면 이게 정답**
three.js 애드온. 실제 DOM 요소에 `matrix3d`를 걸어 3D 공간에 배치한다.
- **장점**: 100% 진짜 DOM. 상호작용·접근성·폰트 렌더링 완벽. 전 브라우저. 가볍다.
- **결정적 한계**: 요소는 **DOM 합성 레이어**로 남으므로 **WebGL 씬과 진짜로 섞이지 않는다.** 깊이 테스트, 오클루전, 셰이더 왜곡, 조명, 그림자, 후처리 어느 것도 적용할 수 없다. 항상 WebGL 캔버스 위/아래 별도 레이어다.
- 즉 **평면을 3D로 눕히는 것까지는 되지만, 천이 구겨지거나 유리에 굴절되게는 못 한다.**

**판정**: "카드를 3D로 기울인다" → CSS3DRenderer. "카드를 물결처럼 왜곡한다" → 불가.

### G. `backdrop-filter` + SVG `feDisplacementMap` — **Liquid Glass 폴백의 정석**
`filter` 프로퍼티로 SVG 필터를 참조해 배경 레이어를 굴절시킨다. `feDisplacementMap`이 두 번째 이미지의 R/G 채널로 첫 이미지를 공간 변위시키므로, 높이장/법선/스넬 법칙 기반 변위 맵을 만들면 진짜 굴절처럼 보인다.

**브라우저 현실 (2026 기준):**
- **Chromium(Chrome/Edge/Brave/Arc)**: `backdrop-filter: url(#filter)` 동작 → 진짜 굴절
- **Safari / Firefox**: `backdrop-filter`에 SVG 필터 URL을 **지원하지 않는다.** GPU 가속 안정성 때문에 내장 CSS 필터 함수로 제한. → **자동으로 블러 글래스모피즘으로 degrade** (별도 코드 불필요)
- 상호운용 표준화 논의 진행 중: [w3c/svgwg#1142](https://github.com/w3c/svgwg/issues/1142) "define interoperable backdrop displacement/refraction for 'liquid glass' UI"

**판정**: 영상의 "Apple Liquid Glass" 인상만 필요하다면 **HTML-in-Canvas 없이 CSS+SVG로 상당 부분 재현 가능**하고, 브라우저 지원 범위도 오히려 넓다. 다만 "유리 아래 콘텐츠가 마우스를 따라 실시간으로 출렁이며 색수차까지" 수준은 셰이더가 필요하다.

### H. View Transitions API — **"상태 A → 상태 B" 전환 한정**
- `document.startViewTransition()` 이 전/후 스냅샷을 만들고 `::view-transition-*` 의사 요소를 CSS로 애니메이션한다.
- **Matt Rothenberg의 정확한 대비**: "View Transitions는 **두 개의 스냅샷**을 clip-path와 opacity로 애니메이션한다. (HTML-in-Canvas는) **두 개의 라이브 렌더**와 셰이더를 준다."
- 즉 불타는 다크모드 전환의 **타이밍과 구조**는 View Transitions로 잡되, **불꽃의 픽셀 단위 시뮬레이션**은 포기하고 CSS `mask-image`(노이즈 PNG/SVG) 애니메이션으로 근사하는 게 현실적인 폴백이다. `mask-image` + `mask-position` 애니메이션으로 "타들어가는 마스크"는 꽤 그럴듯하게 나온다.

### I. 배경 캔버스 오버레이 — **가장 흔한 오해**
"어차피 HTML 뒤에 canvas 깔면 되는 거 아냐?"에 대한 Matt Rothenberg의 답:

> **"배경 캔버스는 그릴 수는 있어도 읽을 수는 없다(A background canvas can draw. It can't read.)"**

이 방식으로 **불가능한 것 3가지:**
1. **픽셀 단위 왜곡** — CSS `transform`은 요소 박스 전체에 걸린다. div를 회전/스케일/스큐할 수는 있어도, 그 안에 렌더된 **텍스트를 배럴 왜곡**하거나 **input의 아래쪽 절반만 압축**할 수는 없다.
2. **두 HTML 상태의 커스텀 블렌딩** — 라이트/다크 테마를 동시에 텍스처로 올려 노이즈·불·스캔라인으로 픽셀 단위 합성.
3. **렌더된 콘텐츠에 반응** — 셰이더가 픽셀 휘도를 읽어 어두운 픽셀과 밝은 픽셀을 다르게 처리하거나, 렌더된 HTML의 엣지를 검출해 **바운딩 박스가 아니라 콘텐츠의 실제 모양을 따라가는** 효과.

**판정**: 글로우, 파티클, 커서 트레일, 배경 그라디언트 같은 **"HTML을 읽을 필요 없는" 효과라면 이 방식으로 충분하고 훨씬 싸다.** 실제로 많은 "화려한" 사이트가 이 정도로 만족한다.

### J. Satori 등 서버 렌더
OG 이미지 생성처럼 **결과가 이미지여도 되는** 경우. 지원 CSS 부분집합이 제한적이지만 서버에서 안정적으로 돈다. 브라우저 인터랙션 효과의 폴백으로는 부적합.

---

## 3. 목표별 권장 조합

| 만들고 싶은 것 | 네이티브 있을 때 | 폴백 |
|---|---|---|
| **3D 씬 안의 상호작용 UI** (영상의 천 위 포트폴리오, 3D 책) | `THREE.HTMLTexture` + `InteractionManager` | **`three-html-render` 폴리필** (동일 코드, 자동 전환). 3D 배치만 필요하면 `CSS3DRenderer` |
| **Liquid Glass / 굴절 오버레이** | WebGL 굴절 셰이더 + `drawElementImage` | **`backdrop-filter` + SVG `feDisplacementMap`** (Chromium 굴절, Safari/FF 블러 degrade) |
| **다크모드 불타는 전환** | 두 텍스처 + FBM 셰이더 | **View Transitions + CSS `mask-image` 노이즈 애니메이션** |
| **폼 포커스 글로우 / 배경 반응** | 셰이더 한 패스에서 글로우+콘텐츠 합성 | **배경 캔버스 오버레이** (I). 글로우가 폼 *뒤*, 도트 *앞*에 오는 레이어링만 포기 |
| **버튼 리플 / 클릭 시 산산조각** | `drawElementImage` + 픽셀 조작 | **SnapDOM으로 1회 스냅샷** → 요소 숨김 → 파티클 애니메이션 |
| **HTML → 이미지/영상 내보내기** | `drawElementImage` + `canvas.captureStream()` | **SnapDOM** (클라이언트) 또는 **Satori/Puppeteer** (서버) |
| **캔버스 앱의 리치 텍스트 UI** (Figma/Docs류) | `drawElementImage` | 현행 유지 (DOM 오버레이 또는 자체 텍스트 레이아웃) |

---

## 4. 실전 폴백 패턴 — CanvasUI 모델

`canvasui.dev`(David Haz)가 채택한 원칙이 가장 건전하다:

> "컴포넌트가 **런타임에 지원 여부를 감지하고 우아하게 degrade한다**. API가 없으면 콘텐츠는 그냥 일반 HTML로 렌더되고, 그래도 돌 수 있는 이펙트 부분은 계속 돈다."

구현 형태:

```js
const HAS_HIC = 'requestPaint' in HTMLCanvasElement.prototype;

if (HAS_HIC) {
  canvas.setAttribute('layoutsubtree', '');
  mountShaderEffect(canvas);          // 셰이더 왜곡 전체 경로
} else {
  canvas.replaceWith(...canvas.childNodes);   // HTML 을 문서로 승격
  mountCssOnlyEffect(container);      // backdrop-filter / transition / mask 로 근사
}
```

**핵심 설계 규칙 3가지:**
1. **HTML을 먼저 쓰고 캔버스를 나중에 씌운다.** 캔버스가 없어도 페이지가 완성되어 있어야 한다.
2. **캔버스는 장식이지 구조가 아니다.** 레이아웃·포커스 순서·읽기 순서는 전부 HTML이 결정한다.
3. **효과의 "의미"와 "구현"을 분리한다.** "제출 시 폼이 사라진다"는 의미는 셰이더 왜곡으로도, CSS `scale`+`opacity`로도 표현된다. 폴백은 열화판이지 부재가 아니어야 한다.

---

## 5. 비용·성능 메모

- **A(네이티브)**: 스냅샷은 브라우저 내부 디스플레이 리스트에서 나오므로 직렬화·디코딩이 없다. 다만 explainer가 경고하듯 **캔버스 안 콘텐츠는 컴포지터 스레드 스크롤/애니메이션 혜택을 잃고 JS에 묶인다.** 캔버스 안에 스크롤 영역을 넣기보다 캔버스 전체를 스크롤시켜라.
- **B/C/D(foreignObject)**: 매 캡처마다 DOM 직렬화 → SVG 파싱 → 이미지 디코드. 60fps 지속 갱신에는 부적합. **무효화 시점에만 재캡처**하도록 설계할 것.
- **E(html2canvas)**: 가장 느리다. 신규 채택 근거 없음.
- **F(CSS3DRenderer)**: DOM 합성 레이어라 저렴하지만, 요소가 많으면 레이어 폭발.
- **G(backdrop-filter)**: GPU 가속이지만 큰 영역에 걸면 비싸다. Safari/FF가 SVG 필터를 backdrop에 안 붙이는 이유가 정확히 이것(GPU 사용량·불안정성).
- **I(배경 캔버스)**: 가장 싸다. HTML을 읽을 필요가 없다면 이걸 먼저 검토하라.
