클립보드 액세스 차단 해제

텍스트 및 이미지에 대한 더 안전하고 차단되지 않은 클립보드 액세스

기존에는 클립보드 상호작용을 위해 document.execCommand()를 통해 시스템 클립보드에 액세스했습니다. 이 자르기 및 붙여넣기 방법은 널리 지원되지만 비용이 들었습니다. 클립보드 액세스는 동기식이며 DOM을 읽고 쓸 수만 있었습니다.

텍스트가 적은 경우에는 괜찮지만 클립보드 전송을 위해 페이지를 차단하는 것이 좋지 않은 환경인 경우가 많습니다. 콘텐츠를 안전하게 붙여넣기 전에 시간이 많이 걸리는 정리 또는 이미지 디코딩이 필요할 수 있습니다. 브라우저가 붙여넣은 문서에서 연결된 리소스를 로드하거나 인라인해야 할 수 있습니다. 그러면 디스크나 네트워크를 기다리는 동안 페이지가 차단됩니다. 클립보드 액세스를 요청하는 동안 브라우저가 페이지를 차단해야 하는 권한을 추가한다고 가정해 보겠습니다. 동시에 클립보드 상호작용을 위해 document.execCommand() 주위에 설정된 권한은 느슨하게 정의되어 있으며 브라우저마다 다릅니다.

Async Clipboard API는 이러한 문제를 해결하여 페이지를 차단하지 않는 잘 정의된 권한 모델을 제공합니다. Async Clipboard API는 대부분의 브라우저에서 텍스트와 이미지를 처리하는 것으로 제한되지만 지원은 다릅니다. 다음 섹션별로 브라우저 호환성 개요를 주의 깊게 검토하세요.

복사: 클립보드에 데이터 쓰기

writeText()

텍스트를 클립보드에 복사하려면 writeText()를 호출합니다. 이 API는 비동기식이므로 writeText() 함수는 전달된 텍스트가 성공적으로 복사되었는지에 따라 확인되거나 거부되는 프로미스를 반환합니다.

async function copyPageUrl() {
  try {
    await navigator.clipboard.writeText(location.href);
    console.log('Page URL copied to clipboard');
  } catch (err) {
    console.error('Failed to copy: ', err);
  }
}

브라우저 지원

  • Chrome: 66
  • Edge: 79
  • Firefox: 63.
  • Safari: 13.1.

소스

write()

실제로 writeText()는 일반 write() 메서드의 편의 메서드일 뿐이며, 이를 통해 이미지를 클립보드에 복사할 수도 있습니다. writeText()와 마찬가지로 비동기식이며 Promise를 반환합니다.

클립보드에 이미지를 쓰려면 이미지가 blob여야 합니다. 이를 수행하는 한 가지 방법은 fetch()를 사용하여 서버에서 이미지를 요청한 다음 응답에서 blob()를 호출하는 것입니다.

서버에서 이미지를 요청하는 것은 여러 가지 이유로 바람직하지 않거나 불가능할 수 있습니다. 다행히 이미지를 캔버스에 그리고 캔버스의 toBlob() 메서드를 호출할 수도 있습니다.

다음으로, ClipboardItem 객체의 배열을 write() 메서드에 매개변수로 전달합니다. 현재는 한 번에 하나의 이미지만 전달할 수 있지만 향후 여러 이미지 지원이 추가될 수 있기를 바랍니다. ClipboardItem는 이미지의 MIME 유형을 키로, blob을 값으로 사용하는 객체를 사용합니다. fetch() 또는 canvas.toBlob()에서 가져온 blob 객체의 경우 blob.type 속성에 이미지의 올바른 MIME 유형이 자동으로 포함됩니다.

try {
  const imgURL = '/images/generic/file.png';
  const data = await fetch(imgURL);
  const blob = await data.blob();
  await navigator.clipboard.write([
    new ClipboardItem({
      // The key is determined dynamically based on the blob's type.
      [blob.type]: blob
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

또는 ClipboardItem 객체에 약속을 작성할 수 있습니다. 이 패턴의 경우 데이터의 MIME 유형을 미리 알아야 합니다.

try {
  const imgURL = '/images/generic/file.png';
  await navigator.clipboard.write([
    new ClipboardItem({
      // Set the key beforehand and write a promise as the value.
      'image/png': fetch(imgURL).then(response => response.blob()),
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

브라우저 지원

  • Chrome: 76
  • Edge: 79
  • Firefox: 127
  • Safari: 13.1.

소스

사본 이벤트

사용자가 클립보드 복사를 시작하고 preventDefault()를 호출하지 않는 경우 copy 이벤트에는 이미 올바른 형식의 항목이 포함된 clipboardData 속성이 포함됩니다. 자체 로직을 구현하려면 preventDefault()를 호출하여 기본 동작이 실행되지 않도록 하고 자체 구현을 사용해야 합니다. 이 경우 clipboardData는 비어 있습니다. 텍스트와 이미지가 포함된 페이지를 생각해 보세요. 사용자가 모두 선택하고 클립보드 복사를 시작하면 맞춤 솔루션은 텍스트를 삭제하고 이미지만 복사해야 합니다. 아래 코드 샘플과 같이 이를 실행할 수 있습니다. 이 예에서는 클립보드 API가 지원되지 않을 때 이전 API로 대체하는 방법을 다루지 않습니다.

<!-- The image we want on the clipboard. -->
<img src="kitten.webp" alt="Cute kitten.">
<!-- Some text we're not interested in. -->
<p>Lorem ipsum</p>
document.addEventListener("copy", async (e) => {
  // Prevent the default behavior.
  e.preventDefault();
  try {
    // Prepare an array for the clipboard items.
    let clipboardItems = [];
    // Assume `blob` is the blob representation of `kitten.webp`.
    clipboardItems.push(
      new ClipboardItem({
        [blob.type]: blob,
      })
    );
    await navigator.clipboard.write(clipboardItems);
    console.log("Image copied, text ignored.");
  } catch (err) {
    console.error(err.name, err.message);
  }
});

copy 이벤트의 경우:

브라우저 지원

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

소스

ClipboardItem:

브라우저 지원

  • Chrome: 76
  • Edge: 79
  • Firefox: 127.
  • Safari: 13.1.

소스

붙여넣기: 클립보드에서 데이터 읽기

readText()

클립보드에서 텍스트를 읽으려면 navigator.clipboard.readText()를 호출하고 반환된 프로미스가 해결될 때까지 기다립니다.

async function getClipboardContents() {
  try {
    const text = await navigator.clipboard.readText();
    console.log('Pasted content: ', text);
  } catch (err) {
    console.error('Failed to read clipboard contents: ', err);
  }
}

브라우저 지원

  • Chrome: 66
  • Edge: 79
  • Firefox: 125.
  • Safari 13.1.

소스

read()

navigator.clipboard.read() 메서드도 비동기식이며 프로미스를 반환합니다. 클립보드에서 이미지를 읽으려면 ClipboardItem 객체 목록을 가져온 다음 이를 반복합니다.

ClipboardItem는 다양한 유형의 콘텐츠를 보유할 수 있으므로 for...of 루프를 다시 사용하여 유형 목록을 반복해야 합니다. 각 유형의 경우 현재 유형을 인수로 사용하여 getType() 메서드를 호출하여 상응하는 blob을 가져옵니다. 이전과 마찬가지로 이 코드는 이미지에 연결되지 않으며 향후 다른 파일 형식과도 호환됩니다.

async function getClipboardContents() {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      for (const type of clipboardItem.types) {
        const blob = await clipboardItem.getType(type);
        console.log(URL.createObjectURL(blob));
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
}

브라우저 지원

  • Chrome: 76
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

소스

붙여넣은 파일 작업

사용자가 ctrl+cctrl+v와 같은 클립보드 단축키를 사용할 수 있으면 유용합니다. Chromium은 아래에 설명된 대로 클립보드에 읽기 전용 파일을 노출합니다. 이 메서드는 사용자가 운영체제의 기본 붙여넣기 단축키를 누르거나 브라우저의 메뉴 바에서 수정을 클릭한 다음 붙여넣기를 클릭하면 트리거됩니다. 추가 배관 코드가 필요하지 않습니다.

document.addEventListener("paste", async e => {
  e.preventDefault();
  if (!e.clipboardData.files.length) {
    return;
  }
  const file = e.clipboardData.files[0];
  // Read the file's contents, assuming it's a text file.
  // There is no way to write back to it.
  console.log(await file.text());
});

브라우저 지원

  • Chrome: 3.
  • Edge: 12.
  • Firefox: 3.6.
  • Safari: 4.

소스

붙여넣기 이벤트

앞서 언급했듯이 Clipboard API와 함께 작동하는 이벤트를 도입할 계획이지만 지금은 기존 paste 이벤트를 사용할 수 있습니다. 클립보드 텍스트를 읽는 새로운 비동기 메서드와 잘 작동합니다. copy 이벤트와 마찬가지로 preventDefault()를 호출하는 것을 잊지 마세요.

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  const text = await navigator.clipboard.readText();
  console.log('Pasted text: ', text);
});

브라우저 지원

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

소스

여러 MIME 유형 처리

대부분의 구현에서는 단일 잘라내기 또는 복사 작업을 위해 클립보드에 여러 데이터 형식을 배치합니다. 그 이유는 두 가지입니다. 앱 개발자는 사용자가 텍스트나 이미지를 복사하려는 앱의 기능을 알 수 없으며, 많은 애플리케이션이 구조화된 데이터를 일반 텍스트로 붙여넣기를 지원하기 때문입니다. 일반적으로 사용자에게 붙여넣기 및 스타일 일치 또는 서식 없음 붙여넣기와 같은 이름의 수정 메뉴 항목으로 표시됩니다.

다음 예는 이 작업을 실행하는 방법을 보여줍니다. 이 예에서는 fetch()를 사용하여 이미지 데이터를 가져오지만 <canvas> 또는 파일 시스템 액세스 API에서 가져올 수도 있습니다.

async function copy() {
  const image = await fetch('kitten.png').then(response => response.blob());
  const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
  const item = new ClipboardItem({
    'text/plain': text,
    'image/png': image
  });
  await navigator.clipboard.write([item]);
}

보안 및 권한

클립보드 액세스는 항상 브라우저의 보안 문제를 야기했습니다. 적절한 권한이 없으면 페이지에서 모든 종류의 악성 콘텐츠를 사용자의 클립보드에 자동으로 복사하여 붙여넣을 때 심각한 결과를 초래할 수 있습니다. rm -rf / 또는 압축 해제 폭탄 이미지를 클립보드에 자동으로 복사하는 웹페이지를 생각해 보세요.

사용자에게 클립보드 권한을 요청하는 브라우저 메시지
Clipboard API의 권한 메시지입니다.

웹페이지에 클립보드에 대한 제한 없는 읽기 액세스 권한을 부여하는 것은 더 큰 문제입니다. 사용자는 비밀번호 및 개인 정보와 같은 민감한 정보를 주기적으로 클립보드에 복사하며, 이러한 정보는 사용자 모르게 어느 페이지에서나 읽을 수 있습니다.

여러 새 API와 마찬가지로 Clipboard API는 HTTPS를 통해 제공되는 페이지에서만 지원됩니다. 악용을 방지하기 위해 페이지가 활성 탭인 경우에만 클립보드 액세스가 허용됩니다. 활성 탭의 페이지는 권한을 요청하지 않고 클립보드에 쓸 수 있지만 클립보드에서 읽으려면 항상 권한이 필요합니다.

복사 및 붙여넣기 권한이 Permissions API에 추가되었습니다. 페이지가 활성 탭이면 clipboard-write 권한이 페이지에 자동으로 부여됩니다. clipboard-read 권한을 요청해야 합니다. 클립보드에서 데이터 읽기를 시도하여 실행할 수 있습니다. 아래 코드는 후자를 보여줍니다.

const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);

// Listen for changes to the permission state
permissionStatus.onchange = () => {
  console.log(permissionStatus.state);
};

allowWithoutGesture 옵션을 사용하여 자르기 또는 붙여넣기를 호출하는 데 사용자 동작이 필요한지 여부도 제어할 수 있습니다. 이 값의 기본값은 브라우저마다 다르므로 항상 포함해야 합니다.

여기서 Clipboard API의 비동기 특성이 실제로 유용합니다. 클립보드 데이터를 읽거나 쓰려고 하면 사용자에게 권한이 아직 부여되지 않은 경우 자동으로 권한을 요청하는 메시지가 표시됩니다. API는 프로미스 기반이므로 완전히 투명하며 사용자가 클립보드 권한을 거부하면 프라미스가 거부되므로 페이지가 적절하게 응답할 수 있습니다.

브라우저는 페이지가 활성 탭일 때만 클립보드 액세스를 허용하므로 개발자 도구 자체가 활성 탭이므로 브라우저 콘솔에 직접 붙여넣으면 일부 예시가 실행되지 않습니다. 한 가지 방법이 있습니다. setTimeout()를 사용하여 클립보드 액세스를 지연한 다음 함수가 호출되기 전에 페이지 내부를 빠르게 클릭하여 포커스를 맞춥니다.

setTimeout(async () => {
  const text = await navigator.clipboard.readText();
  console.log(text);
}, 2000);

권한 정책 통합

iframe에서 API를 사용하려면 다양한 브라우저 기능과 API를 선택적으로 사용 설정 및 사용 중지할 수 있는 메커니즘을 정의하는 권한 정책을 사용하여 API를 사용 설정해야 합니다. 구체적으로는 앱의 요구사항에 따라 clipboard-read 또는 clipboard-write 중 하나 또는 둘 다를 전달해야 합니다.

<iframe
    src="index.html"
    allow="clipboard-read; clipboard-write"
>
</iframe>

기능 감지

모든 브라우저를 지원하면서 Async Clipboard API를 사용하려면 navigator.clipboard를 테스트하고 이전 메서드로 대체합니다. 예를 들어 다른 브라우저를 포함하도록 붙여넣기를 구현하는 방법은 다음과 같습니다.

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  let text;
  if (navigator.clipboard) {
    text = await navigator.clipboard.readText();
  }
  else {
    text = e.clipboardData.getData('text/plain');
  }
  console.log('Got pasted text: ', text);
});

하지만 그게 다가 아닙니다. Async Clipboard API 이전에는 웹브라우저마다 다양한 복사 및 붙여넣기 구현이 혼합되어 있었습니다. 대부분의 브라우저에서는 document.execCommand('copy')document.execCommand('paste')를 사용하여 브라우저 자체의 복사 및 붙여넣기를 트리거할 수 있습니다. 복사할 텍스트가 DOM에 없는 문자열인 경우 DOM에 삽입하고 선택해야 합니다.

button.addEventListener('click', (e) => {
  const input = document.createElement('input');
  input.style.display = 'none';
  document.body.appendChild(input);
  input.value = text;
  input.focus();
  input.select();
  const result = document.execCommand('copy');
  if (result === 'unsuccessful') {
    console.error('Failed to copy text.');
  }
  input.remove();
});

데모

아래 데모에서 Async Clipboard API를 사용해 볼 수 있습니다. Glitch에서 텍스트 데모 또는 이미지 데모를 리믹스하여 실험해 볼 수 있습니다.

첫 번째 예에서는 텍스트를 클립보드로 이동하고 클립보드에서 텍스트를 이동하는 방법을 보여줍니다.

이미지로 API를 사용해 보려면 이 데모를 사용하세요. PNG만 지원되며 일부 브라우저에서만 지원됩니다.

감사의 말씀

Async Clipboard API는 다윈 황게리 카치마르치크가 구현했습니다. Darwin도 데모를 제공했습니다. 이 자료를 검토해 주신 Kyarik님과 다시 한번 Gary Kačmarčík님께 감사드립니다.

UnsplashMarkus Winkler님 제공 히어로 이미지