수년 동안 웹 개발자는 웹에서 복잡하고 상호작용이 많은 시각적 애플리케이션을 빌드할 때 어려운 아키텍처 선택을 해야 했습니다. 풍부한 시맨틱 기능을 위해 DOM을 사용해야 할까요, 아니면 저수준 그래픽 성능을 위해 <canvas> 요소에 직접 렌더링해야 할까요?
이제 오리진 트라이얼을 통해 제공되는 새로운 실험용 HTML-in-Canvas API를 사용하면 선택할 필요가 없습니다. 이 API를 사용하면 UI를 상호작용 가능하고 액세스 가능하며 즐겨 사용하는 브라우저 기능에 연결한 상태로 DOM 콘텐츠를 2D 캔버스 또는 WebGL/WebGPU 텍스처에 직접 그릴 수 있습니다. HTML을 저수준 그래픽 처리와 결합하면 이전에는 불가능했던 환경을 만들 수 있습니다.
DOM과 Canvas
이 새로운 API의 강력한 기능을 이해하려면 DOM과 Canvas의 상대적인 강점을 살펴보는 것이 좋습니다.
DOM은 웹 UI의 핵심입니다. 의미론적으로 이해된 콘텐츠를 사용하여 풍부한 인터페이스를 만드는 즉시 사용 가능한 텍스트 레이아웃 솔루션을 제공합니다. 이를 통해 사용자는 웹페이지에서 복사할 텍스트를 강조 표시하거나 이미지를 마우스 오른쪽 버튼으로 클릭하여 저장하는 등 일반적으로 당연하게 여기는 작업을 원활하게 수행할 수 있습니다. DOM은 접근성 도구, 번역, 페이지 내 검색, 읽기 모드, 확장 프로그램, 어두운 모드, 브라우저 확대/축소, 자동 완성 등 필수 브라우저 기능과도 통합됩니다.
반면 Canvas (및 WebGL/WebGPU)는 고급 2D 및 3D 그래픽을 위해 픽셀 그리드를 구동하는 하위 수준 액세스를 허용합니다. 게임과 복잡한 웹 앱 (예: Google Docs, Figma)에는 이러한 고성능의 하위 수준 액세스가 필요합니다. 캔버스는 기본적으로 픽셀 그리드이므로 반응형 텍스트와 같은 기능을 지원하려면 복잡한 맞춤 UI 로직이 필요했고, 이로 인해 번들 크기가 크게 증가했습니다. 중요한 점은 UI가 정적 캔버스 픽셀 그리드 내에 갇혀 있으면 DOM에 통합된 모든 강력한 브라우저 기능이 완전히 중단된다는 것입니다.
DOM을 Canvas로 가져올 때의 이점
HTML-in-Canvas API는 두 가지 장점을 모두 누릴 수 있는 가교 역할을 합니다. <canvas> 요소 내에 HTML을 배치하고 변환을 동기화하면 콘텐츠가 완전히 상호작용 가능한 상태로 유지되고 모든 브라우저 통합이 자동으로 작동합니다.
DOM이 <canvas> 요소 내에서 UI를 처리하도록 하면 다음과 같은 이점이 있습니다.
- 텍스트 레이아웃 및 서식: CSS 스타일이 적용된 여러 줄 또는 양방향 텍스트를 포함한 간소화된 텍스트 레이아웃 및 서식입니다.
- 양식 컨트롤: 광범위한 맞춤설정 옵션을 통해 표현력이 뛰어나고 사용하기 쉬운 양식 컨트롤입니다.
- 텍스트 선택, 복사/붙여넣기, 마우스 오른쪽 버튼 클릭: 사용자는 3D 장면 내에서 텍스트를 강조 표시하거나 기본적으로 컨텍스트 메뉴를 마우스 오른쪽 버튼으로 클릭할 수 있습니다.
- 접근성: 캔버스 내에서 렌더링된 콘텐츠는 접근성 트리에 노출됩니다. 접근성 시스템은 일반 HTML을 파싱하는 것처럼 UI를 파싱하고 스크린 리더와 같은 시스템에 노출할 수 있습니다.
- 페이지 내 찾기: 사용자는 페이지 내 찾기 (Ctrl/Cmd+F)를 사용하여 텍스트를 검색할 수 있으며 브라우저는 WebGL 텍스처 내에서 직접 텍스트를 강조 표시합니다.
- 색인 생성 가능성 및 AI 에이전트 인터페이스 가능성: 웹 크롤러와 AI 에이전트는 2D 및 3D 장면에 렌더링된 텍스트의 색인을 원활하게 생성하고 읽을 수 있습니다.
- 확장 프로그램 통합: 브라우저 확장 프로그램이 기본적으로 작동합니다. 예를 들어 텍스트 대체 확장 프로그램은 3D 메시에 렌더링된 텍스트를 자동으로 업데이트합니다.
- DevTools 통합: Chrome DevTools에서 직접 WebGL/WebGPU UI 요소를 포함한 캔버스 콘텐츠를 검사할 수 있습니다. 검사기에서 CSS 스타일을 조정하고 3D 텍스처에서 즉시 업데이트되는 모습을 확인하세요.
대략적인 사용 사례
이 API는 여러 분야에서 놀라운 잠재력을 발휘합니다.
- 대규모 캔버스 기반 애플리케이션: 이제 대규모 웹 앱에서 복잡한 애플리케이션 UI 구성요소를 캔버스 기반 작업공간에 기본적으로 렌더링하여 접근성을 개선하고 번들 무게를 줄일 수 있습니다.
- 3D 장면 및 게임: 마케팅 사이트, 몰입형 WebXR 환경, 웹 게임에서 이제 실제 DOM 텍스트를 사용하는 3D 책이나 복사 및 붙여넣기를 기본적으로 지원하는 게임 내 터미널과 같이 완전히 상호작용 가능한 웹 UI를 3D 장면에 배치할 수 있습니다.
API 사용 방법
API를 사용하는 과정은 캔버스 설정, 캔버스에 렌더링, CSS 변환 업데이트의 세 단계로 이루어지며, 이를 통해 브라우저는 요소가 화면에 물리적으로 배치되는 위치를 알 수 있습니다.
기본 요건
HTML-in-Canvas API는 Chrome 148~150에서 오리진 트라이얼 중입니다. 사이트에서 테스트하려면 Chrome Canary 149 이상을 사용하고 chrome://flags/#canvas-draw-element 플래그를 사용 설정하세요. 다른 사용자가 API를 사용 설정하려면 오리진 트라이얼에 등록하세요.
1단계: 기본 캔버스 설정
먼저 <canvas> 태그에 layoutsubtree 속성을 추가합니다. 이렇게 하면 브라우저가 캔버스 내에 중첩된 콘텐츠를 인식하고 캔버스 내에 표시되도록 준비하며 접근성 트리에 노출합니다.
<canvas id="canvas" style="width: 200px; height: 200px;" layoutsubtree>
<div id="form_element">
<label for="name">Name:</label> <input id="name" type="text">
</div>
</canvas>
캔버스 그리드 크기 조정
렌더링된 콘텐츠가 흐리게 표시되지 않도록 하려면 기기 배율과 일치하도록 캔버스 그리드의 크기를 조정해야 합니다.
const observer = new ResizeObserver(([entry]) => {
const dpc = entry.devicePixelContentBoxSize;
canvas.width = dpc ? dpc[0].inlineSize : Math.round(entry.contentRect.width * window.devicePixelRatio);
canvas.height = dpc ? dpc[0].blockSize : Math.round(entry.contentRect.height * window.devicePixelRatio);
});
const supportsDevicePixelContentBox =
typeof ResizeObserverEntry !== 'undefined' &&
'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);
2thought 2단계: 렌더링
2D 컨텍스트의 경우 drawElementImage 메서드를 사용합니다. 이 작업은 텍스트 강조 표시 또는 사용자 입력 중과 같이 요소가 다시 그려질 때마다 트리거되는 paint 이벤트 내에서 실행합니다. 상호작용이 계속 작동하도록 반환 값으로 요소의 CSS 변환을 업데이트하는 것이 중요합니다.
const ctx = document.getElementById('canvas').getContext('2d');
const form_element = document.getElementById('form_element');
const canvas = document.getElementById('canvas');
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
let transform = ctx.drawElementImage(form_element, 0, 0);
// Use the transform returned later on...
};
WebGL로 렌더링
WebGL의 경우 texElementImage2D을 사용합니다. texImage2D와 유사하게 작동하지만 DOM 요소를 소스로 사용합니다.
canvas.onpaint = () => {
if (gl.texElementImage2D) {
gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, form_element);
}
};
WebGPU로 렌더링
WebGPU는 copyExternalImageToTexture와 유사하게 기기 큐에서 copyElementImageToTexture 메서드를 사용합니다.
canvas.onpaint = () => {
root.device.queue.copyElementImageToTexture(
valueElement,
{ texture: targetTexture }
);
};
3단계: CSS 변환 업데이트
이제 요소를 캔버스에 렌더링했으므로 브라우저에 요소의 위치를 업데이트해야 합니다. 이렇게 하면 캔버스와 DOM 레이아웃 간의 공간 동기화가 보장됩니다. 이는 브라우저가 이벤트 영역(예: 사용자가 정확히 클릭하거나 마우스를 가져가는 위치)을 요소가 렌더링되는 위치와 올바르게 매핑할 수 있도록 하는 데 중요합니다.
2D 컨텍스트의 경우 렌더링 호출에서 반환된 변환을 .style.transform property에 적용합니다.
const ctx = document.getElementById('canvas').getContext('2d');
const form_element = document.getElementById('form_element');
const canvas = document.getElementById('canvas');
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
let transform = ctx.drawElementImage(form_element, 0, 0);
// Sync the DOM location with the drawn location
form_element.style.transform = transform.toString();
};
WebGL 또는 WebGPU를 사용하면 요소의 화면 위치는 셰이더 코드에서 출력 텍스처를 사용하는 방식에 따라 달라지며 캔버스 렌더링 컨텍스트에서 추론할 수 없습니다. 하지만 셰이더 프로그램이 일반적인 모델 뷰 프로젝션을 사용하여 텍스처를 그리는 경우 새로운 편의 함수 element.getElementTransform()를 사용하여 drawElementImage()의 반환 값과 동일한 방식으로 사용할 수 있는 변환을 계산할 수 있습니다. 이를 위해서는 다음을 수행해야 합니다.
- WebGL MVP 행렬을 DOM 행렬로 변환합니다.
- HTML 요소를 정규화합니다. HTML 요소의 크기는 픽셀 단위로 지정됩니다 (예: 너비 200px). 하지만 WebGL은 일반적으로 객체를 '단위 정사각형'(예: 0~1)으로 취급합니다. 정규화하지 않으면 200px 버튼이 200배 더 크게 표시됩니다.
- 캔버스 표시 영역에 매핑합니다. 이 단계는 '재조정' 단계입니다. 이 함수는 단위 공간 수학을 다시 확장하여 화면에 있는
<canvas>요소의 실제 픽셀 크기와 일치시킵니다. 또한 WebGL에서는 위쪽이 양수이지만 CSS에서는 아래쪽이 양수이므로 Y축을 뒤집습니다. - 최종 변환을 계산합니다. 행렬을 순서대로 곱합니다.
Viewport * MVP * Normalization.행렬을 하나의 최종 변환으로 결합하면 브라우저에 HTML 요소 레이어가 3D 드로잉과 정렬되기 위해 정확히 어디에 위치해야 하는지 알려주는 '지도'가 생성됩니다. - HTML 요소에 변환을 적용합니다. 이렇게 하면 HTML 요소 레이어가 렌더링된 픽셀 바로 위에 배치됩니다. 이렇게 하면 사용자가 버튼을 클릭하거나 텍스트를 선택할 때 실제 HTML 요소를 누르게 됩니다.
if (canvas.getElementTransform) {
// 1. Convert WebGL MVP Matrix to DOM Matrix
const mvpDOM = new DOMMatrix(Array.from(htmlElementMVP));
// 2. Normalize the HTML element (pixels -> 1x1 unit square)
const width = targetHTMLElement.offsetWidth;
const height = targetHTMLElement.offsetHeight;
const cssToUnitSpace = new DOMMatrix()
.scale(1 / width, -1 / height, 1) // Shrink to unit size and flip Y
.translate(-width / 2, -height / 2); // Center the element
// 3. Map to the canvas viewport
const clipToCanvasViewport = new DOMMatrix()
.translate(canvas.width / 2, canvas.height / 2) // Move origin to center
.scale(canvas.width / 2, -canvas.height / 2, 1); // Stretch to canvas dimensions
// 4. Multiply: (Clip -> Pixels) * (MVP) * (pixels -> unit square)
const screenSpaceTransform = clipToCanvasViewport
.multiply(mvpDOM)
.multiply(cssToUnitSpace);
// 5. Apply to the transform
const computedTransform = canvas.getElementTransform(targetHTMLElement, screenSpaceTransform);
if (computedTransform) {
targetHTMLElement.style.transform = computedTransform.toString();
}
}
라이브러리 및 프레임워크 지원
일부 인기 라이브러리에서는 이미 HTML-in-Canvas 기능을 지원하고 있습니다.
Three.js
행렬을 수동으로 업데이트하는 것은 지루할 수 있으므로 프레임워크가 이미 도입되고 있습니다. Three.js는 새로운 THREE.HTMLTexture를 사용하여 실험적으로 지원합니다.
const material = new THREE.MeshBasicMaterial();
material.map = new THREE.HTMLTexture(uiElement); // Pass the DOM element
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
PlayCanvas
PlayCanvas는 텍스처 API를 사용하여 HTML-in-Canvas도 지원합니다.
// Wait for the 'paint' event to set the source
canvas.addEventListener('paint', () => {
htmlTexture.setSource(htmlElement);
}, { once: true });
canvas.requestPaint();
// Keep up to date
canvas.addEventListener('paint', onPaintUpload);
const material = new pc.StandardMaterial();
material.diffuseMap = htmlTexture;
material.update();
데모
데모를 사용해 보기 전에 환경이 올바르게 구성되었는지 확인하세요.
API 사용을 위한 참조 역할을 하는 여러 데모가 있습니다. 번역 가능한 3D 책부터 유리 셰이더를 통해 굴절되는 UI 요소에 이르기까지 커뮤니티에서 이미 창의적인 솔루션을 선보이고 있습니다.
- 3D 책: 페이지에 HTML 레이아웃을 사용하는 WebGL 렌더링 3D 책입니다. 사용자는 CSS를 사용하여 글꼴을 바꿀 수 있습니다. DOM 기반이므로 기본 제공 번역이 즉시 작동하며 AI 에이전트가 더 간단하게 텍스트를 추출할 수 있습니다.
- 대화형 3D UI: 기본 3D 모델을 기반으로 빛을 굴절시키면서도 표준 HTML
<input type="range">단계 속성에 응답하는 WebGPU 젤리 슬라이더입니다. - 애니메이션 텍스처: 맞춤 애니메이션 루프 없이 DOM을 사용하여 애니메이션 SVG 연필을 WebGL 텍스처로 직접 렌더링하는 동적 3D 빌보드입니다.
- 굴절 오버레이: 움직이는 3D 커서에 의해 왜곡되지만 페이지 내 찾기를 사용하여 완전히 선택하고 검색할 수 있는 대화형 서체 레이어입니다.
커뮤니티에서 만든 데모 모음을 확인하세요. HTML-in-Canvas 데모를 이 컬렉션에 표시하려면 pull 요청을 생성하여 추가하세요.
제한사항
이 API는 강력하지만 다음과 같은 몇 가지 제한사항이 있습니다.
- 교차 출처 콘텐츠: 보안 및 개인 정보 보호를 위해 API는 교차 출처 iframe 콘텐츠에서 작동하지 않습니다.
- 기본 스레드 스크롤: HTML-in-canvas는 JavaScript로 그려집니다. 즉, 캔버스 외부에서와 같이 JavaScript와 독립적으로 스크롤 및 애니메이션을 업데이트할 수 없습니다. 개발자는 캔버스 내에 스크롤 콘텐츠를 배치하는 것과 전체 캔버스를 스크롤하는 것의 성능 특성을 신중하게 고려해야 합니다.
의견
HTML-in-Canvas API를 실험 중이라면 의견을 알려주세요. 오리진 트라이얼에 가입하여 실험 단계에서 사이트에 이 기능을 사용 설정하면 API 설계를 개선하는 데 도움이 됩니다. 문제를 신고하여 의견을 제공할 수도 있습니다.