클립보드 액세스 차단 해제

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

기존에는 클립보드 상호작용을 위해 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와 마찬가지로 클립보드 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도 데모를 제공했습니다. 이 도움말의 일부를 검토해 주신 카리크님과 게리 카치마르치크님께 다시 한번 감사드립니다.

UnsplashMarkus Winkler님 제공 히어로 이미지