웹 개발자는 웹에서 복잡하고 상호작용성이 높은 시각적 애플리케이션을 빌드할 때 몇 년 동안 어려운 아키텍처 선택을 해야 했습니다. 풍부한 시맨틱 기능을 위해 DOM을 사용해야 할까요, 아니면 하위 수준 그래픽 성능을 위해 <canvas> 요소에 직접 렌더링해야 할까요?
이제 오리진 트라이얼에서 사용할 수 있는 새로운 실험용 HTML-in-Canvas API를 사용하면 선택할 필요가 없습니다. 이 API를 사용하면 UI를 상호작용 가능하고 접근성 있으며 즐겨찾는 브라우저 기능에 연결된 상태로 유지하면서 DOM 콘텐츠를 2D 캔버스 또는 WebGL/WebGPU 텍스처에 직접 그릴 수 있습니다. HTML과 하위 수준 그래픽 처리를 결합하면 이전에는 불가능했던 환경을 만들 수 있습니다.
DOM과 캔버스 비교
이 새로운 API의 기능을 이해하려면 DOM과 캔버스의 상대적 강점을 살펴보는 것이 좋습니다.
DOM은 웹 UI의 기본 요소입니다. 시맨틱하게 이해된 콘텐츠를 사용하여 풍부한 인터페이스를 만드는 즉시 텍스트 레이아웃 솔루션을 제공합니다. 이를 통해 사용자는 웹페이지에서 일반적인 작업을 원활하게 실행할 수 있습니다. 예를 들어 텍스트를 강조표시하여 복사하거나 이미지를 마우스 오른쪽 버튼으로 클릭하여 저장하는 것과 같은 작업을 할 수 있습니다. DOM은 접근성 도구, 번역, 페이지 내 검색, 리더 모드, 확장 프로그램, 다크 모드, 브라우저 확대/축소, 자동 완성 등 필수 브라우저 기능과도 통합됩니다.
캔버스 (및 WebGL/WebGPU)를 사용하면 고도로 발전된 2D 및 3D 그래픽을 위해 픽셀 그리드를 구동하는 하위 수준 액세스가 가능합니다. 게임 및 복잡한 웹 앱 (예: Google Docs 또는 Figma)에는 이러한 성능이 우수한 하위 수준 액세스가 필요합니다. 캔버스는 기본적으로 픽셀 그리드이므로 반응형 텍스트와 같은 기능을 지원하려면 복잡한 맞춤 UI 로직이 필요하여 번들 크기가 크게 증가했습니다. 중요한 점은 UI가 정적 캔버스 픽셀 그리드 내에 갇히면 DOM에 통합된 모든 강력한 브라우저 기능이 완전히 중단된다는 것입니다.
캔버스에 DOM을 가져오는 이점
HTML-in-Canvas API는 두 가지 장점을 모두 제공하는 브리지입니다. HTML을 <canvas> 요소 내에 배치하고 변환을 동기화하면 콘텐츠가 완전히 상호작용 가능하고 모든 브라우저 통합이 자동으로 작동합니다.
다음은 <canvas> 요소 내에서 DOM이 UI를 처리하도록 허용하여 얻을 수 있는 이점입니다.
- 텍스트 레이아웃 및 형식 지정: CSS 스타일이 적용된 여러 줄 또는 양방향 텍스트를 포함하여 텍스트 레이아웃 및 형식 지정을 간소화합니다.
- 양식 컨트롤: 광범위한 맞춤설정 옵션이 있는 표현력이 풍부하고 사용하기 쉬운 양식 컨트롤입니다.
- 텍스트 선택, 복사/붙여넣기, 마우스 오른쪽 버튼 클릭: 사용자는 3D 장면 내에서 텍스트를 강조표시하거나 컨텍스트 메뉴를 기본적으로 마우스 오른쪽 버튼으로 클릭할 수 있습니다.
- 텍스트 선택, 복사/붙여넣기, 마우스 오른쪽 버튼 클릭: 사용자는 3D 장면 내에서 텍스트를 강조표시하거나 컨텍스트 메뉴를 기본적으로 마우스 오른쪽 버튼으로 클릭할 수 있습니다.
- 접근성: 캔버스 내에 렌더링된 콘텐츠가 접근성 트리에 노출됩니다. 접근성 시스템은 일반 HTML과 마찬가지로 UI를 파싱하고 스크린 리더와 같은 시스템에 노출할 수 있습니다.
- Find-in-page: 사용자는 페이지 내 검색 (Ctrl/Cmd+F)을 사용하여 텍스트를 검색할 수 있으며 브라우저에서 WebGL 텍스처 내에서 직접 강조표시합니다.
- Find-in-page: 사용자는 페이지 내 검색 (Ctrl/Cmd+F)을 사용하여 텍스트를 검색할 수 있으며 브라우저에서 WebGL 텍스처 내에서 직접 강조표시합니다.
- 색인 생성 가능 여부 및 AI 에이전트 인터페이스 가능: 웹 크롤러와 AI 에이전트는 2D 및 3D 장면으로 렌더링된 텍스트를 원활하게 색인 생성하고 읽을 수 있습니다.
- 확장 프로그램 통합: 브라우저 확장 프로그램이 기본적으로 작동합니다. 예를 들어 텍스트 대체 확장 프로그램은 3D 메시에서 렌더링된 텍스트를 자동으로 업데이트합니다.
- DevTools 통합: Chrome DevTools에서 직접 WebGL/WebGPU UI 요소를 포함한 캔버스 콘텐츠를 검사할 수 있습니다. 검사기에서 CSS 스타일을 조정하고 3D 텍스처에서 즉시 업데이트되는 것을 확인하세요.
대략적인 사용 사례
이 API는 여러 도메인에서 놀라운 잠재력을 발휘합니다.
- 대형 캔버스 기반 애플리케이션: Google Docs, Miro 또는 Figma와 같은 대규모 웹 앱은 이제 복잡한 애플리케이션 UI 구성요소를 캔버스 기반 작업공간에 기본적으로 렌더링하여 접근성을 개선하고 번들 무게를 줄일 수 있습니다.
- 3D 장면 및 게임: 마케팅 사이트, 몰입형 WebXR 환경, 웹 게임은 이제 실제 DOM 텍스트를 사용하는 3D 책 또는 복사 및 붙여넣기를 기본적으로 지원하는 게임 내 터미널과 같은 3D 장면에서 완전히 상호작용 가능한 웹 UI를 배치할 수 있습니다.
API 사용 방법
API 사용은 캔버스 설정, 캔버스에 렌더링, 브라우저가 화면에서 요소가 실제로 있는 위치를 알 수 있도록 CSS 변환 업데이트의 세 단계로 이루어집니다.
기본 요건
HTML-in-Canvas API는 Chrome 148~150에서 오리진 트라이얼로 제공됩니다. 사이트에서 테스트하려면 chrome://flags/#canvas-draw-element 플래그가 사용 설정된 Chrome Canary 149 이상을 사용하세요. 다른 사용자를 위해 API를 사용 설정하려면 오리진 트라이얼에 등록하세요.
1단계: 기본 캔버스 설정
먼저 layoutsubtree 속성을 <canvas> 태그에 추가합니다. 이렇게 하면 브라우저가 캔버스 내에 중첩된 콘텐츠를 인식하여 캔버스 내에 표시할 수 있도록 준비하고 접근성 트리에 노출합니다.
<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);
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 요소는 픽셀 단위로 크기가 조정됩니다 (예: 너비 200픽셀). 하지만 WebGL은 일반적으로 객체를 '단위 정사각형'(예: 0~1 범위)으로 취급합니다. 정규화하지 않으면 200픽셀 버튼이 200배 더 크게 표시됩니다.
- 캔버스 표시 영역에 매핑합니다. 이 단계는 "크기 조정" 단계입니다. 단위 공간 수학을 다시 늘려 화면의
<canvas>요소의 실제 픽셀 크기와 일치시킵니다. 또한 Y축을 뒤집습니다. WebGL에서는 위가 양수이지만 CSS에서는 아래가 양수이기 때문입니다. - 최종 변환을 계산합니다. 행렬을 순서대로 곱합니다.
Viewport * MVP * Normalization.이를 하나의 최종 변환으로 결합하면 브라우저에 3D 드로잉과 정렬하기 위해 HTML 요소 레이어가 정확히 어디에 있어야 하는지 알려주는 '지도'가 생성됩니다. - 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 모델을 기반으로 빛을 굴절하는 WebGPU 젤리 슬라이더로, 표준 HTML
<input type="range">단계 속성에도 계속 응답합니다. - 애니메이션 텍스처: 맞춤 애니메이션 루프가 필요 없이 DOM을 사용하여 WebGL 텍스처에 직접 애니메이션 SVG 연필을 렌더링하는 동적 3D 빌보드입니다.
- 굴절 오버레이: 움직이는 3D 커서로 왜곡되지만 페이지 내 검색을 사용하여 완전히 선택하고 검색할 수 있는 상호작용형 타이포그래피 레이어입니다.
커뮤니티에서 만든 데모 모음을 확인하세요. 이 모음에 HTML-in-Canvas 데모를 포함하려면 pull 요청을 만들어 추가하세요.
제한사항
강력한 API이지만 몇 가지 제한사항이 있습니다.
- 교차 출처 콘텐츠: 보안 및 개인 정보 보호를 위해 API는 교차 출처 iframe 콘텐츠와 작동하지 않습니다.
- 기본 스레드 스크롤: HTML-in-canvas는 JavaScript로 그려지므로 캔버스 외부에서와 같이 스크롤 및 애니메이션이 JavaScript와 독립적으로 업데이트될 수 없습니다. 개발자는 스크롤 콘텐츠를 캔버스 내에 배치하는 것과 전체 캔버스를 스크롤하는 것의 성능 특성을 신중하게 고려해야 합니다.
의견
HTML-in-Canvas API를 실험하고 있다면 의견을 들려주세요. 오리진 트라이얼에 가입하여 실험 단계에 있는 동안 사이트에서 이 기능을 사용 설정하여 API 설계를 개선하는 데 도움을 줄 수 있습니다. 문제를 제출하여 의견을 제공할 수도 있습니다.