[TIL] HTML in Canvas - drawElement()로 캔버스 안에 DOM을 그린다

2026. 04. 30.Yeji Kim
Frontend

들어가며

에디터를 만들다보면 canvas vs DOM 사이에서 끝없이 줄타기를 하게 된다. 그림판처럼 자유롭게 그리고 싶으면 캔버스가 좋고, 텍스트를 다루거나 접근성·국제화를 챙기려면 DOM이 좋다. 그치만 차트 위에 풍부한 텍스트 라벨, 게임 위에 메뉴 HUD, 3D 씬 위에 폼 - 늘 두 세계를 어떻게 합쳐 붙일지 고민한다.

이런 관점에서 HTML-in-Canvas가 흥미로운데, WICG에서 논의 중이고, 이미 Chrome Canary 138.0.7175.0+에서 실험 플래그로 켤 수 있다고 한다.

오늘은 이 API가 뭐고, 왜 흥미로운지 알아보자~!


1. 지금까지 캔버스 안에 HTML을 어떻게 넣었나

  • html2canvas - DOM 트리를 읽어 캔버스에 다시 그림. 스타일 호환성 이슈 + 성능 부담
  • CSS 절대 위치 오버레이 - 캔버스 위에 <div>를 띄움. 단, WebGL 3D 씬과 깊이 맞추기가 거의 불가능
  • SVGForeignObject - SVG 안에 HTML을 넣을 수는 있지만, 캔버스 안엔 못 넣음
  • measureText + 직접 그리기 - 캔버스 안에서 텍스트를 셀로 그리는 방식.

요컨대 그동안의 해법은 바깥에 띄우거나, 픽셀로 직접 다시 그리거나 둘 중 하나였다.


2. 새로 들어오는 API - drawElement, texElement2D, ...

새로 생기는? API를 살펴보면 다음과 같다.

API역할
CanvasRenderingContext2D.drawElement()캔버스의 자식 HTML 요소를 캔버스에 렌더링 (위치/리사이즈 지정)
WebGLRenderingContext.texElement2D()HTML 요소를 WebGL 텍스처로 변환 → 3D 씬에 삽입
setHitTestRegions캔버스에 그려진 영역을 마우스/터치 이벤트와 연결
fireOnEveryPaintHTML 변화 자동 감지 → 캔버스 자동 재렌더링

실제 코드는 이런 식이다 (현재 Canary 138 기준).

js
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
 
// canvas 자식 HTML 요소를 가져와서 캔버스에 그린다
const legend = canvas.querySelector(".chart-legend");
ctx.drawElement(legend, 20, 20);   // (element, x, y) - 3개 인자
 
// 클릭/포인터 이벤트도 연결 (옵션 형태)
ctx.setHitTestRegions([
  { element: legend, x: 20, y: 20, width: 200, height: 80 },
]);
구현하면서 만난 가벼운 이슈들

Only immediate children of the <canvas> element can be passed to DrawElementImage 에러가 났다. 그릴 HTML은 반드시 <canvas>의 직속 자식 이어야 한다. 즉 <canvas><div class="card">...</div></canvas> 구조. 자식은 평소엔 fallback content라 화면엔 안 보이고, drawElement로 호출되는 순간 픽셀로 구워진다. 모델이 명확해서 오히려 좋은 제약 같다. (캔버스는 자기가 그릴 콘텐츠를 명시적으로 소유한다.)

<canvas> elements without layoutsubtree do not support DrawElementImage. 자식 HTML이 레이아웃되도록 캔버스에 layoutsubtree 속성을 명시적으로 붙여야 한다. opt-in이라 일반 캔버스(2D 컨텍스트만 쓰는 것)는 자식 레이아웃 비용을 안 내고, drawElement를 쓸 때만 비용이 발생한다.

html
<canvas layoutsubtree>
  <div class="card">...</div>
</canvas>

정리하면 미래 API의 미니멈 사용 패턴은 다음 4가지를 동시에 만족해야 한다.

  1. <canvas>layoutsubtree 속성
  2. 그릴 HTML은 캔버스의 직속 자식
  3. ctx.drawElement(element, x, y) - 위치 인자 3개
  4. Chrome Canary 138+, Experimental Web Platform features 플래그 ON

WebGL 쪽도 가볍게 살펴보자.

js
const gl = canvas.getContext("webgl2");
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
 
// HTML 요소를 그대로 텍스처로
gl.texElement2D(gl.TEXTURE_2D, 0, gl.RGBA, document.querySelector("#hud"));

지금까지 3D 씬에 UI를 얹으려면 텍스트를 픽셀로 베이크해두거나, DOM 오버레이를 띄우거나 했는데 - 이걸 그냥 HTML로 만들고 텍스처로 굽는 게 가능해진다.


3. DOM 에디터, 그리고 <canvas> 안의 DOM 에디터

간단한 테스트를 위해 같은 도형 에디터를 좌우로 두 개 두었다.

  • 왼쪽: DOM 에디터
  • 오른쪽: 같은 에디터인데, 사실은 <canvas layoutsubtree>의 immediate child
HTML (DOM)live · interactive
빈 캔버스에 클릭해서 도형을 추가해보세요클릭=선택 · 드래그=이동 · 우클릭=삭제 · 색깔 클릭=선택된 도형 색 변경
shapes: 0history: 0/0selected:
DOM 기반 (HTML + SVG)
HTML inside CANVASfallback: foreignObject
빈 캔버스에 클릭해서 도형을 추가해보세요클릭=선택 · 드래그=이동 · 우클릭=삭제 · 색깔 클릭=선택된 도형 색 변경
shapes: 0history: 0/0selected:
DOM 기반 (HTML + SVG)

↑ 양쪽 다 인터랙티브. 도형 추가/이동/삭제·undo·redo — 사용 경험은 동일. 오른쪽은 사실 <canvas>의 자식이고, 매 변화마다 ctx.drawElement()로 캔버스에 픽셀이 그려진다.

drawElement renders: 0last: 0.0msbrowser:
⚠ 네이티브 API 비활성 — SVG <foreignObject> 폴백 사용 중

양쪽 다 인터랙티브하고, 양쪽 다 자기 state를 가진다. 도형 추가, 드래그 이동, 우클릭 삭제, undo/redo 어느 쪽에서 해도 똑같이 작동한다.

내부 구조는 이렇다.

html
<!-- 왼쪽 -->
<div class="shape-editor">...</div>
 
<!-- 오른쪽 -->
<canvas layoutsubtree>
  <div class="shape-editor">...</div>  <!-- canvas의 immediate child -->
</canvas>

layoutsubtree 속성이 자식의 layout + hit testing 을 활성화한다. 그래서 자식 자체는 invisible(픽셀로 그려지기 전까진)이지만 사용자의 클릭/드래그는 자식 트리로 그대로 통과한다. 매 변화마다 ctx.drawElement(child, 0, 0)이 호출되어 자식이 캔버스에 픽셀화되고, 그 픽셀이 사용자에게 보인다.

환경 요구사항 - 데모가 진짜로 동작하려면

이 기능은 실험 플래그가 켜진 환경에서만 네이티브로 동작한다. html-in-canvas 문서 안내 기준으로 정리하면,

  • 브라우저: Chrome Canary 또는 Brave Stable (Chromium 147+)
  • 플래그: chrome://flags/#canvas-draw-element (Brave는 brave://flags/...)
    • "Enable the new drawElement API for Canvas" → Enabled
    • 주소창에 직접 붙여넣어야 함 (보안상 직링크 차단됨)
  • 재시작 필수 - Relaunch 누르고 페이지 새로고침
  • <canvas>layoutsubtree 속성 주입 - 자식 트리의 layout과 hit testing을 활성화

조건 중 하나라도 빠지면 위 데모는 SVG <foreignObject> fallback 경로로 동작한다 (모양은 비슷하지만 외부 CSS 무시·CORS 제약·이벤트 통과 불가 등 한계가 있음). 데모 하단 상태바에서 어떤 경로인지 확인할 수 있게 만들어두었다.

주의사항

WICG 단계의 기능이라서 그렇다. 명세는 진행 중이고 인자 시그니처도 자주 바뀐다 (이 글 쓰면서도 (el, {x,y})로 시도했다가 (el, x, y)로 발견했고, immediate child 제약과 layoutsubtree 옵트인까지 한 번씩 막혀봤다). 프로덕션에 쓰기엔 아직 이를듯...!

미래 API가 풀어줄 부분

drawElement 외에도 함께 제안된 API들이 같이 자리잡으면 활용 폭이 더 넓어진다.

  • fireOnEveryPaint - HTML이 바뀌면 자동 재렌더 (지금은 직접 트리거 필요)
  • setHitTestRegions - 영역과 element를 이벤트 매핑으로 묶기 (현재 layoutsubtree로 자식이 직접 hit testing 받음)
  • gl.texElement2D - HTML을 WebGL 텍스처로, 3D 표면에 입히기

4. 같은 도형을 DOM과 plain Canvas로 - 차이를 만져보자

같은 도형을 DOMCanvas로 동시에 그렸다. 두 박스 안의 라벨 텍스트(#a, #b, ...)를 마우스로 드래그해 선택해보자.

DOMselectable · accessible · stylable
CANVASpixels · no a11y · no select

↑ 두 박스 안의 라벨 텍스트(#a, #b, ...)를 마우스로 드래그해 선택해보세요. DOM 쪽만 선택됩니다.

왼쪽(DOM)은 텍스트 선택, 우클릭, 카피, 접근성 트리 - 다 된다. 오른쪽(Canvas)은 그냥 픽셀이라 그 무엇도 되지 않는다. 같은 모양이지만 사용자 입장에서 사용 가능한 능력이 완전히 다르다.

캔버스가 잃어버리는 것들

  • 텍스트 선택/복사 - 검색도 안 되고 스크린리더도 못 읽음
  • 국제화 (i18n) - 한국어 줄바꿈, 한자 보조 폰트 폴백, 양방향 텍스트, 이모지 합성... 직접 구현하려면 죽음의 길
  • 접근성 (a11y) - 캔버스는 비스듬한 그림. ARIA로 뭐라도 달려면 별도 DOM mirror가 필요
  • 이벤트 시스템 - 도형 단위 클릭/호버 → 직접 hit-testing 구현 (각 도형마다 좌표 검사)
  • CSS 시스템 전체 - flex, grid, 미디어 쿼리, dark mode, transition... 전부 손으로

이게 그동안 우리가 캔버스 안에서 텍스트를 다룰 때 매번 마주쳤던 벽이다.


5. HTML-in-Canvas가 해결할 수 있는 것 예시

5-1. dom & canvas 혼용 시 쌓임 맥락 문제

캔버스 위에 띄운 DOM 메뉴는 늘 z-index 마법과 좌표 동기화가 필요했다.

5-2. 에디터 도메인 관련

예시 시나리오

  • 메인 캔버스는 WebGL/Canvas2D 기반의 무한 좌표계
  • 그 위의 텍스트 블록, 스티커, 코멘트 등은 HTML 요소
  • 사용자가 줌 인하면 텍스트는 그대로 또렷하게 (벡터 + CSS 텍스트 렌더링)
  • 텍스트 선택, 드래그, 다국어 입력, 우클릭 컨텍스트 메뉴 - 전부 기존 DOM 시스템의 장점 그대로 누릴 수 있음

6. 주의해서 봐야 할 점들

보안 & 격리

캔버스 안에 그려진 HTML을 외부 origin이 픽셀로 읽을 수 있는가 가 핵심 이슈다. 일반 캔버스는 cross-origin 이미지를 그리면 tainted 캔버스가 되어 getImageData가 막힌다. HTML도 같은 정책으로 묶일 가능성이 크다. 즉 외부 iframe HTML을 그려서 픽셀 스크래핑 하는 시나리오는 막힐 수도 있겠다.

성능

fireOnEveryPaint는 매력적이지만, 자주 변하는 HTML을 매 프레임 굽는 건 비싸다. 결국 레이아웃과 페인트가 함께 일어나는 단위가 된다. 차트 라벨처럼 변동이 적은 곳에 어울리고, 활발한 애니메이션 텍스트는 여전히 캔버스 native 그리기가 유리할 수 있다.

정리

  • HTML-in-Canvas는 캔버스 안에서 HTML을 다루기 위한 WICG 표준 제안
  • 핵심 API는 drawElement, texElement2D, setHitTestRegions, fireOnEveryPaint 4종
  • 지금까지 캔버스가 잃었던 텍스트 선택 / i18n / a11y / CSS / 이벤트 시스템을 한 번에 되찾을 수 있는 길이 열림
  • 차트 라벨, 게임 HUD, 줌 가능한 에디터 같은 시나리오에서 큰 변화가 예상됨
  • 정착 전이라도, 데이터-렌더 분리로 미리 준비해두면 바로 갈아끼울 수 있음

레퍼런스