Async Clipboard API의 정리되지 않은 HTML

토마스 슈타이너
토마스 슈타이너

Chrome 120부터 Async Clipboard API에서 새로운 unsanitized 옵션을 사용할 수 있습니다. 이 옵션은 클립보드의 콘텐츠를 복사했을 때와 동일하게 붙여넣어야 하는 HTML의 특수한 상황에서 유용할 수 있습니다. 즉, 브라우저에서 일반적으로 그리고 타당한 이유로 적용되는 중간 정리 단계가 없습니다. 이 가이드에서 사용 방법을 알아보세요.

대부분의 경우 Async Clipboard API로 작업할 때 개발자는 클립보드에 있는 콘텐츠의 무결성에 관해 걱정할 필요가 없으며 클립보드에서 클립보드에 쓰는 내용 (복사)이 클립보드에서 데이터를 읽을 때 (붙여넣기)할 때와 동일하다고 가정할 수 있습니다.

텍스트에서도 마찬가지입니다. DevTools 콘솔에 다음 코드를 붙여넣은 후 즉시 페이지의 포커스를 다시 이동해 보세요. Async Clipboard API의 요구사항인 페이지에 집중할 시간을 충분히 확보하려면 setTimeout()가 필요합니다. 보시다시피 입력은 출력과 정확히 동일합니다.

setTimeout(async () => {
  const input = 'Hello';
  await navigator.clipboard.writeText(input);
  const output = await navigator.clipboard.readText();
  console.log(input, output, input === output);
  // Logs "Hello Hello true".
}, 3000);

이미지의 경우에는 조금 다릅니다. 소위 압축 폭탄 공격을 방지하기 위해 브라우저는 PNG와 같은 이미지를 다시 인코딩하지만 입력 및 출력 이미지는 픽셀당 픽셀 수로 시각적으로 완전히 동일합니다.

setTimeout(async () => {
  const dataURL =
    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=';
  const input = await fetch(dataURL).then((response) => response.blob());
  await navigator.clipboard.write([
    new ClipboardItem({
      [input.type]: input,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read();
  const output = await clipboardItem.getType(input.type);
  console.log(input.size, output.size, input.type === output.type);
  // Logs "68 161 true".
}, 3000);

그렇다면 HTML 텍스트는 어떻게 될까요? 짐작할 수 있듯이 HTML의 경우 상황이 다릅니다. 여기서 브라우저는 HTML 코드를 정리하여 HTML 코드 (<meta>, <head>, <style> 등)에서 <script> 태그를 제거하고 CSS를 인라인 처리하여 잘못된 상황이 발생하지 않도록 합니다. 다음 예를 고려하고 DevTools 콘솔에서 시도해 보세요. 출력이 입력과 크게 다르다는 것을 알 수 있습니다.

setTimeout(async () => {
  const input = `<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
    <meta name="ProgId" content="Excel.Sheet" />  
    <meta name="Generator" content="Microsoft Excel 15" />  
    <style>  
      body {  
        font-family: HK Grotesk;  
        background-color: var(--color-bg);  
      }  
    </style>  
  </head>  
  <body>  
    <div>hello</div>  
  </body>  
</html>`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read();
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  console.log(input, output);
}, 3000);

일반적으로 HTML 제거는 좋은 일입니다. 대부분의 경우 정리되지 않은 HTML을 허용하여 보안 문제에 노출되지 않도록 할 수 있습니다. 하지만 개발자가 자신이 하는 일을 정확히 알고 있고 앱이 올바르게 작동하는 데 내부 및 출력 HTML의 무결성이 중요한 시나리오가 있습니다. 이러한 상황에서는 다음 두 가지 옵션 중에서 선택할 수 있습니다.

  1. 복사와 붙여넣기를 모두 제어하는 경우(예: 앱 내에서 복사하여 앱 내에 붙여넣는 경우) Async Clipboard API용 웹 맞춤 형식을 사용해야 합니다. 여기서 멈추고 링크된 도움말을 확인하세요.
  2. 앱에서 붙여넣기 끝부분만 제어하고 복사 끝은 제어하지 않는 경우, 복사 작업이 웹 맞춤 형식을 지원하지 않는 네이티브 앱에서 발생하는 것이 원인일 수 있습니다. 이 경우 unsanitized 옵션을 사용해야 합니다. 이 문서의 나머지 부분에 설명되어 있습니다.

정리에는 script 태그 삭제, 스타일 인라인 처리, HTML 형식 지정 확인 등이 포함됩니다. 이 목록은 포괄적이지 않으며 향후 더 많은 단계가 추가될 수 있습니다.

제거되지 않은 HTML 복사하여 붙여넣기

Async Clipboard API를 사용하여 클립보드에 HTML을 write() (복사)하면 브라우저에서는 DOM 파서를 통해 HTML 문자열을 실행하고 결과 HTML 문자열을 직렬화하여 올바르게 구성되었는지 확인하지만 이 단계에서는 정리가 이루어지지 않습니다. 해야 할 작업은 없습니다. 다른 애플리케이션이 클립보드에 배치한 HTML을 read()하고 웹 앱에서 충실도 높은 콘텐츠를 가져오기로 선택하고 자체 코드에서 정리를 실행해야 하는 경우 unsanitized 속성과 ['text/html'] 값을 사용하여 옵션 객체를 read() 메서드에 전달할 수 있습니다. 이 독립적인 버전은 다음과 같습니다. navigator.clipboard.read({ unsanitized: ['text/html'] }) 아래의 다음 코드 샘플은 이전에 표시된 코드와 거의 동일하지만 이번에는 unsanitized 옵션이 있습니다. DevTools 콘솔에서 사용해 보면 입력과 출력이 동일한 것을 확인할 수 있습니다.

setTimeout(async () => {
  const input = `<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
    <meta name="ProgId" content="Excel.Sheet" />  
    <meta name="Generator" content="Microsoft Excel 15" />  
    <style>  
      body {  
        font-family: HK Grotesk;  
        background-color: var(--color-bg);  
      }  
    </style>  
  </head>  
  <body>  
    <div>hello</div>  
  </body>  
</html>`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read({
    unsanitized: ['text/html'],
  });
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  console.log(input, output);
}, 3000);

브라우저 지원 및 기능 감지

기능이 지원되는지 확인할 수 있는 직접적인 방법은 없으므로 특성 감지는 동작 관찰을 기반으로 합니다. 따라서 다음 예는 <style> 태그가 유지(지원을 나타내는 경우)되는지 아니면 인라인 처리 중(지원되지 않음을 나타내는지)인지를 감지합니다. 이 작업이 가능하려면 페이지에 이미 클립보드 권한이 있어야 합니다.

const supportsUnsanitized = async () => {
  const input = `<style>p{color:red}</style><p>a`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read({
    unsanitized: ['text/html],
  });
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  return /<style>/.test(output);
};

데모

unsanitized 옵션이 작동하는 것을 보려면 Glitch 데모를 참고하고 소스 코드를 확인하세요.

결론

소개에서 설명한 대로 대부분의 개발자는 클립보드 정리에 대해 걱정할 필요가 없으며 브라우저에서 선택한 기본 정리 옵션만 사용할 수 있습니다. 드물지만 개발자가 주의해야 하는 경우를 위해 unsanitized 옵션이 있습니다.

감사의 말씀

이 문서는 아누팜 스니그다레이첼 앤드류가 검토했습니다. 이 API는 Microsoft Edge팀에서 지정하고 구현했습니다.