클립보드 액세스 차단 해제

텍스트와 이미지를 위한 더욱 안전하고 차단되지 않은 클립보드 액세스

시스템 클립보드에 액세스하는 기존 방법은 클립보드 상호작용에 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);
  }
}

브라우저 지원

  • 66
  • 79
  • 63
  • 13.1

소스

write()

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

클립보드에 이미지를 쓰려면 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);
}

브라우저 지원

  • 66
  • 79
  • 13.1

소스

복사 이벤트

사용자가 클립보드 복사를 시작하고 preventDefault()를 호출하지 않는 경우 copy 이벤트에는 항목이 이미 올바른 형식으로 되어 있는 clipboardData 속성이 포함됩니다. 자체 로직을 구현하려면 자체 구현을 위해 preventDefault()를 호출하여 기본 동작을 방지해야 합니다. 이 경우에는 clipboardData가 비어 있습니다. 텍스트와 이미지가 포함된 페이지를 생각해 보세요. 사용자가 모든 항목을 선택하고 클립보드 복사를 시작하면 커스텀 솔루션은 텍스트를 삭제하고 이미지만 복사해야 합니다. 아래 코드 샘플과 같이 이 작업을 수행할 수 있습니다. 이 예에서는 Clipboard 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 이벤트의 경우:

브라우저 지원

  • 1
  • 12
  • 22
  • 3

소스

ClipboardItem:

브라우저 지원

  • 76
  • 79
  • 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);
  }
}

브라우저 지원

  • 66
  • 79
  • 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);
  }
}

브라우저 지원

  • 66
  • 79
  • 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());
});

브라우저 지원

  • 3
  • 12
  • 3.6
  • 4

소스

붙여넣기 이벤트

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

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

브라우저 지원

  • 1
  • 12
  • 22
  • 3

소스

여러 MIME 유형 처리

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

다음 예에서는 그 방법을 보여줍니다. 이 예에서는 fetch()를 사용하여 이미지 데이터를 가져오지만 <canvas> 또는 File System Access 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만 지원되며 일부 브라우저에서만 지원됩니다.

감사의 말

비동기 Clipboard API는 다윈 후앙Gary Kačmarčík에 의해 구현되었습니다. 또한 다윈은 데모를 제공했습니다. 이 도움말의 일부를 검토해 주신 Kyarik님과 Gary Kačmarčík에게 다시 한번 감사드립니다.

Unsplash에 있는 마커스 윙클러의 히어로 이미지