본문 바로가기
FrontEnd/HTML5 & CSS & JavaScript

[JavaScript] iOS에서 클립보드 복사(navigator.clipboard.writeText) 동작 안하는 이슈

by 푸고배 2025. 7. 20.

navigator.clipboard.writeText?

설명

writeText()는 자바스크립트로 ‘텍스트 복사(control+c)’ 기능을 지원하는 메서드로, 매개변수로 텍스트를 넘기면 브라우저가 해당 텍스트를 사용자의 클립보드에 저장한다.

복사에 성공하면 Promise가 완료(resolved)되고, 실패하면 NotAllowedError DOMException오류가 발생한다.

예시코드:

navigator.clipboard.writeText("텍스트 복사하기!")
  .then(() => {
    alert("복사 성공 😀");
  })
  .catch(() => {
    alert("복사 실패 😨");
  });

 

25.07.18 기준 브라우저 호환성

주의할 점

  • 대부분의 브라우저에서 https 환경에서만 동작한다.
  • 명확한 사용자의 액션(클릭이나 키보드 액션 등)이 있어야 복사가 가능하다.

아래와 같이 클릭 → 서버 요청 → 응답 → 복사와 같은 동작하는 코드를 작성했다. 그리고 이 코드는 iOS 환경의 safari 브라우저에서 오류가 발생했다.

import React from 'react';

const ClipboardCopy: React.FC = () => {
  const handleCopy = async () => {
    try {
      const response = await fetch('<https://api.example.com/data>');
      const data = await response.text(); // 예: JSON이라면 .json() 사용
      await navigator.clipboard.writeText(data);
      alert('클립보드에 복사되었습니다!');
    } catch (err) {
      console.error('복사 실패:', err);
      alert('클립보드 복사에 실패했습니다.');
    }
  };

  return (
    
      응답 받아 클립보드에 복사하기
    
  );
};

export default ClipboardCopy;

❌ iOS 환경에서 동작하지 않는 이유

iOS Safari(뿐만 아니라 일부 iOS 브라우저)에서는 다음과 같은 제약이 있다.

  1. 지원 범위 제한: 일부 iOS 버전이나 브라우저는 여전히 navigator.clipboard API 전체 또는 일부를 지원하지 않음.
  2. HTTPS 필수: iOS Safari에서는 클립보드 접근을 위해 HTTPS 환경이어야 하며, 로컬 환경이나 HTTP에선 작동하지 않습니다.
  3. 보안 정책: navigator.clipboard.writeText()는 반드시 사용자의 직접적인 동작(onClick 등 이벤트 핸들러) 안에서 실행되어야 하고, 이 동작과 완전히 동기적으로 연결되어야 한다.
  • await fetch(...)와 같은 비동기 작업 이후에 실행되면 iOS에서는 직접적인 사용자 동작으로 간주되지 않아 차단될 수 있다.

이 케이스는 3번의 이유로 오류가 발생하는 것이었다.

💡 해결 방법

1. 클립보드 복사를 fetch 이전에 수행하거나, 사용자 인터랙션 직후에 가능한 동기적으로 실행하기

✅ 페이지 마운트 시점에 데이터 가져오기

import { useEffect, useState } from 'react';

function ClipboardCopyButton() {
  const [textToCopy, setTextToCopy] = useState('');

  // 컴포넌트 마운트 시 fetch로 미리 데이터 받아오기
  useEffect(() => {
    fetch('/api/data')
      .then((res) => res.text()) // 또는 .json(), .blob() 등 응답 타입에 따라 변경
      .then((data) => {
        setTextToCopy(data);
      })
      .catch((err) => {
        console.error('데이터 불러오기 실패:', err);
      });
  }, []);

  const handleCopy = async () => {
    if (!textToCopy) {
      alert('복사할 데이터가 없습니다.');
      return;
    }

    try {
      await navigator.clipboard.writeText(textToCopy);
      alert('클립보드에 복사되었습니다!');
    } catch (err) {
      alert('복사에 실패했습니다. iOS에서는 사파리에서만 동작할 수 있어요.');
      console.error(err);
    }
  };

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

export default ClipboardCopyButton;

하지만 버튼을 클릭 할 때마다 새로운 값을 받아오는 초기 의도와는 동작이 달라질 수 있다. (페이지 마운트 시 1회 fetch)

다른 방법으로는 버튼 클릭 시마다 API를 호출하고, API 응답 값을 클립보드에 복사하고 싶다면 다음과 같은 코드로 수동 복사를 유도 할 수 있다.

 

✅ 요구사항

  • 버튼 클릭 시마다 fetch 수행
  • 응답을 클립보드에 복사
  • 클립보드 API가 실패할 경우 prompt()를 통해 수동 복사 유도
import { useCallback } from 'react';

export function useClipboardWithFetch() {
  const copyFromApi = useCallback(async (url: string) => {
    try {
      const response = await fetch(url);
      const text = await response.text();

      // 1. 클립보드 복사 시도
      try {
        await navigator.clipboard.writeText(text);
        alert('클립보드에 복사되었습니다.');
      } catch {
        // 2. 실패 시 fallback으로 prompt
        const userConfirmed = window.prompt('복사할 수 없어 수동으로 복사해주세요:', text);
        if (userConfirmed === null) {
          alert('복사가 취소되었습니다.');
        }
      }
    } catch (err) {
      alert('API 요청 실패: ' + String(err));
    }
  }, []);

  return copyFromApi;
}

 

✏️ 사용 예시

import { useClipboardWithFetch } from './useClipboardWithFetch';

export default function CopyButton() {
  const copyFromApi = useClipboardWithFetch();

  const handleClick = () => {
    copyFromApi('/api/data'); // 원하는 API 경로로 변경
  };

  return (
    <button onClick={handleClick}>
      복사하기
    </button>
  );
}

다만 이 경우는 실패 시 fallback으로 prompt로 수동 복사를 안내하는 것이라, 문제를 해결하는 방법이라고 보기는 어렵다.

2. execCommand('copy') 방식(구식 방법)을 fallback으로 시도하기

✅ execCommand fallback을 포함하는 useClipboardWithFetch

import { useCallback } from 'react';

export function useClipboardWithFetch() {
  const copyFromApi = useCallback(async (url: string) => {
    try {
      const response = await fetch(url);
      const text = await response.text();

      // 1차 시도: 최신 clipboard API
      try {
        await navigator.clipboard.writeText(text);
        alert('클립보드에 복사되었습니다.');
        return;
      } catch (err) {
        console.warn('navigator.clipboard.writeText 실패, fallback 시도:', err);
      }

      // 2차 fallback: execCommand('copy')
      try {
        const textarea = document.createElement('textarea');
        textarea.value = text;
        textarea.style.position = 'fixed'; // iOS에서도 스크롤 영향 안 받도록
        textarea.style.opacity = '0';
        document.body.appendChild(textarea);
        textarea.focus();
        textarea.select();

        const successful = document.execCommand('copy');
        document.body.removeChild(textarea);

        if (successful) {
          alert('클립보드에 복사되었습니다. (fallback)');
        } else {
          throw new Error('execCommand 복사 실패');
        }
      } catch (fallbackErr) {
        alert('복사 실패: ' + String(fallbackErr));
      }
    } catch (apiErr) {
      alert('API 요청 실패: ' + String(apiErr));
    }
  }, []);

  return copyFromApi;
}

 

✏️ 사용 예시

import { useClipboardWithFetch } from './useClipboardWithFetch';

export default function CopyButton() {
  const copyFromApi = useClipboardWithFetch();

  return (
    <button onClick={() => copyFromApi('/api/data')}>
      복사하기
    </button>
  );
}

3. iOS에서도 잘 작동하는 외부 라이브러리 사용 고려

✅ React + clipboard.js 예제

yarn add clipboard 
# npm install clipboard
import { useEffect, useRef } from 'react';
import ClipboardJS from 'clipboard';

export default function ClipboardButton() {
  const buttonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (!buttonRef.current) return;

    const clipboard = new ClipboardJS(buttonRef.current, {
      text: async () => {
        const response = await fetch('/api/data');
        if (!response.ok) throw new Error('API 요청 실패');
        const text = await response.text();
        return text;
      },
    });

    clipboard.on('success', () => {
      alert('복사되었습니다!');
    });

    clipboard.on('error', (e) => {
      alert('복사에 실패했습니다: ' + e.action);
    });

    return () => {
      clipboard.destroy();
    };
  }, []);

  return (
    <button ref={buttonRef}>
      복사하기
    </button>
  );
}
**핵심 포인트**

 

📄 항목 설명

ref 복사 버튼 DOM에 직접 연결 필요
text() 함수 버튼 클릭 시 실행되어 복사할 내용을 동적으로 반환
이벤트 리스너 success, error 핸들링 가능
자동 선택 및 복사 내부적으로 execCommand('copy')를 사용하여 호환성 확보

 

참고

 

Clipboard: writeText() method - Web APIs | MDN

The writeText() method of the Clipboard interface writes the specified text to the system clipboard, returning a Promise that is resolved once the system clipboard has been updated.

developer.mozilla.org

 

Clipboard API and events

Abstract This document describes APIs for accessing data on the system clipboard. It provides operations for overriding the default clipboard actions (cut, copy and paste), and for directly accessing the clipboard contents. Status of this document Table of

w3c.github.io

 

댓글