본문 바로가기

Chrome

HTML-in-Canvas 업데이트: 더 나은 Web API를 향한 반복

원문: HTML-in-Canvas updates: Iterating toward a better Web API
— Chrome Developers 원문은 CC-BY-4.0
라이선스로 제공되며, 이 글은 한국어로 번역한 2차 저작물입니다.
Chrome 150 및 155에서 HTML-in-Canvas API에 몇 가지 변경 사항이 도입되었습니다. 이러한 변경은 커뮤니티 및 표준 피드백에 대한 응답으로 API의 사용 편의성을 개선하는 것을 목표로 했습니다. 이 블로그 게시물에서는 변경 사항과 구현을 최신 상태로 유지하는 방법을 설명합니다.
업데이트에는 속성 및 메서드 이름 변경, 명시적 텍스처 메모리 사전 할당 요구, DOM 동기화 모델 업데이트가 포함됩니다. 또한 HTML-in-Canvas Origin Trial
이 연장
되었습니다. 이어지는 내용은 변경 사항, 그 이유 및 코드베이스를 업데이트하는 방법에 대한 설명입니다.

Chrome 150의 변경 사항

Chrome 150에서는 WebGL 및 WebGPU 전반에 걸쳐 인자 시그니처를 변경하여 중복을 제거하고 최신 그래픽 규칙에 맞게 조정했습니다.

WebGL

texElementImage2D
에서 중복 인자(
level
,
srcFormat
,
destType
)를 제거했습니다(자세한 내용은 WebGL 토론
참조).
// 이전 구현 (Chrome < 150)
const level = 0;
const internalFormat = gl.RGBA;
const srcFormat = gl.RGBA;
const destType = gl.UNSIGNED_BYTE;

gl.texElementImage2D(
  gl.TEXTURE_2D,
  level,
  internalFormat,
  srcFormat,
  destType,
  element
);

// 새 구현 (Chrome 150부터)
const internalFormat = gl.RGBA8;
gl.texElementImage2D(gl.TEXTURE_2D, internalFormat, element);

WebGPU

copyElementImageToTexture
매개변수를
sourceDict
및
destDict
객체로 표준화했습니다(GPUWeb PR#6250
참조).
// 이전 구현 (Chrome < 150)
GPUQueue.copyElementImageToTexture(
  element,
  width,
  height,
  { texture: texture }
);

// 새 구현 (Chrome 150부터)
const sourceDict = { source: element };
const destDict = {
  destination: { texture: texture },
  width: width,
  height: height
};

GPUQueue.copyElementImageToTexture(sourceDict, destDict);

Chrome 155의 변경 사항

Chrome 155는 개발자 피드백을 기반으로 추가적인 개선 사항을 도입합니다. 메서드 이름을 업데이트하고, 명시적 텍스처 메모리 사전 할당을 요구하며, 복잡한 DOM 하위 트리를 지원하고, DOM 동기화를 분리합니다.

WebGPU

WebGPU 메서드 이름이
copyElementImageToTexture
에서
drawElementImageToTexture
로 변경되었습니다. 이름 변경과 함께 매개변수 좌표가 확장되고(
sourceX
,
sourceWidth
), 대상 경계가 표준
size
(
GPUExtent3D
) 객체로 그룹화됩니다.
// 이전 (Chrome 150)
const sourceDict = {
  source: myElement,
  sx: 0,
  sy: 0,
  swidth: 100,
  sheight: 100
};
const destDict = {
  destination: { texture: myTexture },
  width: 300,
  height: 200
};

queue.copyElementImageToTexture(sourceDict, destDict);

// 이후 (Chrome 155+)
const sourceDict = {
  source: myElement,
  sourceX: 0,
  sourceY: 0,
  sourceWidth: 100,
  sourceHeight: 100
};
const destDict = {
  texture: myTexture,
  size: { width: 300, height: 200 }
};

queue.drawElementImageToTexture(sourceDict, destDict);

WebGL

HTML-in-Canvas의 WebGL 변경 사항에는 세 가지 측면이 있습니다.
  • 메서드 이름이
    texElementImage2D
    에서
    texElementSubImage2D
    로 변경됩니다.
  • 메서드 시그니처가 변경되며, 가장 중요한 점은 요소가 그려질 대상 텍스처의 오프셋을 선택적으로 지정할 수 있도록
    xoffset
    및
    yoffset
    인자가 추가되었다는 것입니다.
  • 메서드는 더 이상 대상 텍스처에 대한 버퍼 스토리지를 자동으로 할당하지 않습니다. API를 사용하기 전에 스토리지을 명시적으로 할당해야 합니다. 이는
    texSubImage2D
    의 동작과 유사합니다.
// 1. 요소의 자연스러운 레이아웃 크기 캡처
const elementImage = canvas.captureElementImage(element);

// 2. 텍스처 백킹을 한 번 (또는 요소 크기 변경 시) 사전 할당
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA8,
  Math.ceil(elementImage.width),
  Math.ceil(elementImage.height),
  0,
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  null // null은 픽셀 업로드를 건너뛰고 VRAM을 예약합니다
);

// 3. 사전 할당된 VRAM에 DOM 업로드
gl.texElementSubImage2D(
  gl.TEXTURE_2D,
  0,                  // level
  0,                  // xoffset
  0,                  // yoffset
  element             // DOM 요소 전달
);

Canvas 속성 및 그릴 수 있는 자식 요소

HTML을 렌더링하기 위해 canvas를 준비하는 방법을 업데이트하고 있습니다.

layoutsubtree
을
content="drawable"
로 이름 변경

HTML 속성 이름 규칙에 맞추기 위해
<canvas>
요소의 부울
layoutsubtree
속성 이름이
content="drawable"
로 변경되었습니다(자세한 논의는 WICG Issue #169
참조).

조상 요소에 대한
drawable
속성

이전에는 canvas의 직계 자식만 렌더링되었습니다. 마크업을 평면화하지 않고 복잡한 DOM 계층 구조를 지원하려면 독립적으로 그리려는 모든 조상 요소에
drawable
속성을 명시적으로 추가할 수 있습니다.
<!-- 이전 -->
<canvas layoutsubtree>
  <div id="badge">
    <h2>Player Stats</h2>
    <button id="actionBtn">Equip</button>
  </div>
</canvas>

<!-- 이후 (Chrome 155+) -->
<canvas content="drawable">
  <!-- 하위 트리 A (본문 및 제목) -->
  <div id="badge" drawable>
    <h2>Player Stats</h2>
    <!-- 하위 트리 B (하위 트리 A에서 제외됨; 독립적으로 그려지고 애니메이션됨) -->
    <button id="actionBtn" drawable>Equip</button>
  </div>
</canvas>

drawable
속성이 있는 요소는
drawable
을 명시적으로 지정하는 중첩된 조상 요소를 제외한 전체 하위 트리를 캡처합니다. 이를 통해 부모 컨테이너와 버튼과 같은 자식 요소를 별도의 그리기 호출로 애니메이션하고 그릴 수 있습니다.

히트 테스트 동기화

이전에는 개발자가 그리기 메서드의 반환 값을 캡처하여
element.style.transform
으로 파이핑해야 했습니다.
drawElementImage()
메서드는 이제 void를 반환하며, 동기화는 컨텍스트에 따라 달라집니다.

2D 컨텍스트: 자동 동기화

2D canvas에서는 브라우저가 DOM 히트 테스트 경계와 화면 판독기 포커스 링을 그려진 좌표에 자동으로 동기화합니다. 모든 수동 변환 파이핑을 제거할 수 있습니다.
// 이전
const transform = ctx.drawElementImage(element, x, y);
element.style.transform = transform.toString();

// 이후 (Chrome 155+)
ctx.drawElementImage(element, x, y);
// 스타일 업데이트 불필요: 위치 및 히트 테스트 자동 동기화

2D에서 기하학적 모양을 수동으로 관리하려면
{ preserveElementGeometry: true }
를
drawElementImage()
에 전달하고
canvas.updateElementGeometry()
를 사용하여 변환을 적용합니다.

3D 컨텍스트: 명시적
updateElementGeometry()

WebGL 또는 WebGPU 컨텍스트에서 DOM을 동기화하려면
DOMMatrix
변환을 구성하고
canvas.updateElementGeometry()
에 전달합니다.
canvas.addEventListener('paint', () => {
  // 1. 요소 스냅샷 GPU 텍스처에 업로드
  device.queue.drawElementImageToTexture({ source: element }, { texture });

  // 2. 3D 씬 렌더링...
  drawScene();

  // 3. 필요한 대로 변환 행렬 구성
  const canvasTransform = new DOMMatrix().translate(x, y);

  // 4. DOM 히트 테스트 및 접근성 경계 상자 업데이트
  canvas.updateElementGeometry(element, { canvasTransform });
});

버전 간 호환성

전환 기간 동안 여러 Chrome 버전의 사용자를 지원해야 하는 애플리케이션의 경우, 프로토타입에서 업데이트된 메서드의 존재 여부를 확인하고 구현을 조정해야 합니다.
모든 브라우저 버전에서 올바른
<canvas>
요소 속성을 설정해야 합니다.
// ============================================================================
//  버전 간 캔버스 마크업
// ============================================================================
if (!canvas.hasAttribute('content') && !canvas.hasAttribute('layoutsubtree')) {
  if ('content' in HTMLCanvasElement.prototype) {
    // Chrome 155+ (새로운 표준화된 속성)
    canvas.setAttribute('content', 'drawable');
  } else {
    // Chrome < 155 (레거시 부울 속성)
    canvas.setAttribute('layoutsubtree', '');
  }
}

2D 컨텍스트의 경우,
drawElementImage()
의 반환 값을 검사하고 canvas 프로토타입 속성을 확인하여 두 버전을 모두 지원합니다.
// ============================================================================
// 버전 간 2D 그리기 및 동기화
// ============================================================================
function draw2DElement(ctx, element, x, y) {
  // 2D 그리기 호출 실행
  const result = ctx.drawElementImage(element, x, y);

  if (result !== undefined && typeof result?.toString === 'function') {
    // Chrome < 155: drawElementImage가 DOMMatrix를 반환했습니다.
    // DOM 히트 테스트 경계를 정렬하기 위해 수동 변환 파이핑이 필요합니다.
    element.style.transform = result.toString();
  } else {
    // Chrome 155+: drawElementImage가 void/undefined를 반환합니다.
    // 브라우저가 DOM 위치, 클릭 및 접근성 경계 상자를 자동으로 정렬합니다!
    // 이전에 여기에 element.style.transform 로직이 있었다면, 지우십시오.
    if (element.style.transform) {
      element.style.transform = "";
    }
  }
}

WebGL의 경우 호환되는 메서드 시그니처와 이름을 사용합니다.
// ============================================================================
// WebGPU 버전 간 지원
// ============================================================================
if ('drawElementImageToTexture' in GPUQueue.prototype) {
  // Chrome 155+ (최신 구현)
  queue.drawElementImageToTexture(
    { source: element, sourceX: 0, sourceY: 0, sourceWidth: w, sourceHeight: h },
    { texture: myTexture, size: { width: w, height: h } }
  );
} else if (typeof queue.copyElementImageToTexture === "function") {
  // Chrome 150 - 154: 먼저 Chrome 150+ 사전 객체 시그니처를 시도한 다음,
  // Chrome < 150 시그니처로 대체합니다.
  try {
    queue.copyElementImageToTexture(
      { source: element },
      { destination: { texture: myTexture }, width: w, height: h }
    );
  } catch (err) {
    // Chrome < 150
    queue.copyElementImageToTexture(element, { texture: myTexture });
  }
}

WebGL의 브라우저 호환성을 확인해야 합니다.
// ============================================================================
// WebGL 버전 간 지원
// ============================================================================
if ('texElementSubImage2D' in WebGL2RenderingContext.prototype) {
  // Chrome 155+ (명시적 사전 할당 + 부분 이미지)
  gl.texElementSubImage2D(gl.TEXTURE_2D, 0, 0, 0, element);
} else if (gl.texElementImage2D && gl.texElementImage2D.length === 3) {
  // Chrome 150-154 (3개 인수 시그니처)
  gl.texElementImage2D(gl.TEXTURE_2D, gl.RGBA8, element);
} else if (gl.texElementImage2D) {
  // Chrome < 150 (6개 인수 시그니처)
  gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, element);
}

Origin Trial 연장

HTML-in-Canvas Origin Trial
이 Chrome 160까지 연장되었습니다. HTML-in-Canvas 기능을 사용자에게 계속 제공하려면 origin trial 토큰을 갱신해야 합니다. 자세한 내용은 Origin Trial 기사
를 참조하세요.
WICG GitHub 저장소에 이슈를 제기
하여 애플리케이션에서 이러한 업데이트가 어떻게 작동하는지 알려주세요. 개발자 뉴스레터
에 가입하여 HTML-in-Canvas 관련 소식을 계속 받아보세요.