복사 컴포넌트 제작 트러블슈팅 (clipboard.writeText, execCommand)

·2025년 3월 15일
post-thumbnail

개요

Next.js 환경에서 클립보드 복사 기능을 구현하는 과정에서 여러 가지 문제를 겪었습니다.
처음에는 navigator.clipboard.writeText를 사용했지만 배포 후 동작하지 않았고, 그 후 execCommand를 활용하려 했으나 예상과 다르게 동작하지 않는 문제가 발생했습니다.
이를 해결하는 과정과 함께 복사 기능을 구현하는 방법을 알아보도록 하겠습니다.

처음에는 navigator.clipboard.writeText()를 사용하여 복사 기능을 구현했습니다.
navigator.clipboard는 브라우저에서 제공하는 클립보드 API로, 사용자가 브라우저를 통해 데이터를 복사하거나 붙여넣을 수 있도록 지원하는 기능입니다.
클립보드 API에서 지원하는 함수는 아래와 같으며 문자열의 복사 기능 구현에는 writeText 함수가 사용됩니다.

  • writeText(): 문자열을 클립보드에 복사하는 비동기 함수입니다.
  • readText(): 클립보드에 저장된 텍스트를 읽는 함수이며 클립보드를 읽기위해 권한을 요구합니다.
  • write(): 문자열뿐만 아닌 이미지, 파일 등의 다양한 데이터를 클립보드에 저장하는 비동기 함수입니다.
  • read(): 문자열뿐만 아닌 이미지, 파일 등의 클립보드의 다양한 데이터를 읽어오는 비동기 함수이며, 권한이 필요합니다.
const handleCopy = async () => {
  try {
    await navigator.clipboard.writeText("복사할 텍스트")
    alert("복사되었습니다.")
  } catch (error) {
    console.error("복사에 실패했습니다.", error.message)
  }
}

return <button onClick={handleCopy}>복사하기</button>

위의 예시 코드와 같이 navigator.clipboard.writeText() 만으로 간단하게 복사 기능을 구현할 수 있습니다.

에러 발생

로컬 환경에서는 정상적으로 작동 했으나 문제는 배포 후에 발생했습니다.
클립보드 API는 보안상의 이유로 https 환경에서만 동작하기 때문에 에러가 발생했으며, 사내에서 테스트용으로 사용하고 있는 배포 환경은 http 프로토콜을 사용하여 에러가 발생했습니다.

  • 개발 환경(localhost)에서는 정상 작동했으나, 배포 환경에서 복사가 되지 않았다.
  • 배포 환경이 http였기 때문, 클립보드 API는 보안상의 이유로 https 환경에서만 동작한다.

해결 방법

결국 배포 환경을 https로 변경하거나, 다른 복사 방법을 사용해야만 했습니다.
배포 환경을 바꿀 수는 없었으므로 다른 방법으로 execCommand('copy')를 사용했습니다.

execCommand('copy')

document.execCommand는 문서에서 특정 편집 명령을 실행하는 메서드입니다. 이를 통해 텍스트를 굵게 만들거나, 복사 및 붙여넣기와 같은 동작을 수행할 수 있습니다.
execCommand는 복사 기능 외에도 다양한 문서 편집 명령을 실행할 수 있으며 자세한 사항은 여기에서 확인 가능합니다.
이 글에서는 몇 가지만 소개해보겠습니다.

  • document.execCommand("copy"): 선택한 텍스트를 복사
  • document.execCommand("cut"): 선택한 텍스트를 잘라내기
  • document.execCommand("paste"): 클립보드의 내용을 붙여넣기
  • document.execCommand("bold"): 선택한 텍스트를 굵게 만듦
  • document.execCommand("italic"): 선택한 텍스트를 기울임
  • document.execCommand("underline"): 선택한 텍스트에 밑줄 추가
const handleCopy = () => {
  const textArea = document.createElement("textarea") // 임의의 textarea 추가
  textArea.value = "복사할 텍스트" // teaxtarea에 내용 추가
  document.body.appendChild(textArea) // body에 textarea 추가
  textArea.select() // textarea 선택
  document.execCommand("copy") // 선택힌 내용 복사
  document.body.removeChild(textArea) // textarea 제거
  alert("복사되었습니다.")
};

return <button onClick={handleCopy}>복사하기</button>

위 코드 예시와 같이 이 방법은 input 또는 textarea 요소를 선택한 후 실행하는 방식으로 동작합니다.
주석으로 작성한 것와 같이 아래와 같은 로직으로 복사가 수행됩니다.

  1. 임의의 textarea 추가
  2. textarea 내용 추가
  3. bodytextarea 추가
  4. textarea 선택
  5. 선택한 내용 복사
  6. textarea 제거

하지만 execCommand현재 지원이 중단(deprecated) 되어 더 이상 최신 브라우저에서 권장되지 않으며, 일부 환경에서는 동작하지 않을 수 있습니다.
다만, 위에서 소개드린 navigator.clipboard.writeText와는 다르게 http에서도 사용 가능합니다.

에러 발생

이상하게도 저는 위의 방법으로 복사가 되지 않았습니다... document.execCommand("copy")를 콘솔에 찍어봤을 때 true를 반환했고, 분명 사용 가능한 환경인데 기능이 동작하지 않아서 아래와 같은 테스트들을 진행했습니다.

  1. Next.js 환경이 아닌 HTML 환경에서 작동이 되는지 확인 → 정상작동
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button id="copy">버튼</button>

    <script>
      document.getElementById("copy").addEventListener("click", function () {
        const copyText = document.createElement("textarea");
        copyText.value = "복사할 텍스트";
        document.body.appendChild(copyText);
        copyText.select();
        document.execCommand("copy");
        document.body.removeChild(copyText);
  		alert("복사되었습니다.")
      });
    </script>
  </body>
</html>
  1. 함수 안에서 textarea를 생성하는 것이 아닌, 컴포넌트 안에 textarea를 만들고 ref로 연결하여 복사 확인 → 정상작동
const textAreaRef = useRef<HTMLTextAreaElement|null>(null)
const handleCopy = () => {
  textAreaRef.current?.select()
  document.execCommand('copy')
  alert("복사되었습니다.")
};

return (
  <>
    <textarea ref={textAreaRef} defaultValue="복사할 텍스트" />
    <button onClick={handleCopy}>복사하기</button>
  </>
)

다른분의 PC에 확인을 하는 등... 이런저런 테스트를 하다가...
제가 복사 컴포넌트를 사용하고 있는 곳이 dialog 였다는 것을 깨달았습니다.

해결 방법

const [isDialog, setIsDialog] = useState(false)

// 컴포넌트가 다이얼로그 안에 있는지 확인
useEffect(() => {
  const dialog = document.querySelector('[role="dialog"]')
  setIsDialog(dialog !== null)
}, [])

const handleCopy = () => {
  const textArea = document.createElement('textarea')
  const documentTarget = isDialog ? '[role="dialog"]' : 'body' // dialog 여부에 따라 append할 element 선택
  textArea.value = "복사할 텍스트"
  document.querySelector(documentTarget)?.appendChild(textArea)
  textArea.select()
  const success = document.execCommand('copy')
  document.querySelector(documentTarget)?.removeChild(textArea)
  alert("복사되었습니다.")
}

return <button onClick={handleCopy}>복사하기</button>

포커스의 문제 때문에 textareabody가 아닌 dialog 내부에 append를 하니 복사가 정상적으로 작동했습니다 😅
현재 복사 컴포넌트가 사용되고 있는 곳이 dialog인지 감지 하기 위해 useEffect를 사용해 state에 담아주었으며, dialog 인지 여부에 따라 append할 element 선택하여 appendChild를 실행했습니다.

최종코드

const [isDialog, setIsDialog] = useState(false)

// 컴포넌트가 다이얼로그 안에 있는지 확인
useEffect(() => {
  const dialog = document.querySelector('[role="dialog"]')
  setIsDialog(dialog !== null)
}, [])

// copy
const handleCopy = async () => {
  try {
    if (navigator.clipboard && window.isSecureContext) { // navigator.clipboard가 사용 가능한지, https 여부 확인
      await navigator.clipboard.writeText("복사할 텍스트")
      alert("복사되었습니다.")
    } else {
      const textArea = document.createElement('textarea')
      const documentTarget = isDialog ? '[role="dialog"]' : 'body'
      textArea.value = "복사할 텍스트"
      document.querySelector(documentTarget)?.appendChild(textArea)
      textArea.select()
      const success = document.execCommand('copy')
      document.querySelector(documentTarget)?.removeChild(textArea)

      if (success) {
        alert("복사되었습니다.")
      } else {
        console.error("복사에 실패했습니다.", error.message)
      }
    }
  } catch (error) {
	console.error("복사에 실패했습니다.", error.message)
  }
}

return <button onClick={handleCopy}>복사하기</button>

최종적으로 코드를 정리하면 위와 같습니다.
navigator.clipboard를 사용 가능한 환경인지 확인 후에 사용이 불가능하다면 execCommand를 사용하고 성공, 실패에 대한 후속처리를 제공합니다.

마무리

추가적으로 clipboard.jsreact-copy-to-clipboard 와 같은 클립보드 라이브러리는 어떻게 동작하는지 확인해봤습니다.
라이브러리에서도 본문에서 설명한 execCommand를 사용하고 있어, 필요하다면 직접 구현하지 않고 라이브러리를 사용하는 방법도 있을 것 같습니다.
Next.js에서 복사 기능을 구현하면서 여러 가지 문제를 겪었습니다. 특히 보안 정책, dialog 포커스 등 다양한 원인을 파악하며 최종적으로 복사 기능을 구현할 수 있었습니다!

  1. navigator.clipboard.writeTexthttps 환경에서만 동작한다.
  2. execCommand('copy')는 지원이 중단되었지만, http 환경에서도 동작한다.
  3. dialog 내부에서 execCommand('copy')를 사용할 때는 포커스에 주의하자 😅
  4. 필요하다면 라이브러리를 활용하는 것도 좋은 선택이다.
profile
FE ✨

0개의 댓글