본문 바로가기
개발개발/LifeGraph_인생그래프

[인생 그래프] 아이폰 safari에서 이미지 복사가 안됨 - 해결 과정

by yelimu 2025. 2. 13.

 

카톡 인앱 브라우저에서 외부 브라우저로 강제 리다이렉트를 시켜서 이미지 저장 기능은 구현했다.

그런데 이미지 복사는 안되는 상황

 

Clipboard API 를 사용해 구현한 코드이다. 

  const handleImageAction = async () => {
    const image = imageRef.current;
    if (!image) return;

    try {
      const canvas = await html2canvas(image, { scale: 2 });
      canvas.toBlob(async (blob) => {
        if (!blob) {
          alert("이미지 파일이 생성되지 않았습니다.");
          return;
        }
          await navigator.clipboard.write([
            new ClipboardItem({
              "image/png": blob,
            }),
          ]);
          alert("이미지가 클립보드에 저장되었습니다.");
    } catch (err) {
      console.log(`에러 발생`, err);
    }
  };

안드로이드는 확인을 못해봤는데 아래 블로그를 참조해보니 아이폰 safari에서만 Clipboard API 가 동작하지 않는다고 한다. 

클립보드 작업이 사용자 작업에 의해 비동기가 아닌 directly하게 트리거 되어야 하며, 그렇지 않은 경우 사용자의 직접적인 요청으로 간주하지 않아 클립보드 작업에 대한 액세스가 차단된다.

 

https://velog.io/@inmyhead

 

클립보드 복사, 링크 공유하기 기능 만들기 | 모바일 사파리,크롬 애플기기 에러

#클립보드 #링크공유 #복붙 #GPTarot💫 OpenAI api와 DeepL api로 유저가 질문을 하면 타로 카드를 뽑을 수 있고 이를 GPT가 해석해주는 서비스이다.

velog.io

 

그러면 이미지 생성하는 부분이랑, 클립보드 동작을 구분해서 clipboard.write()에 바로 넘겨주도록 해볼까

  const imageRef = useRef<HTMLDivElement>(null);

  const getImage = async () => {
    const image = imageRef.current;
    if (!image) return;
    try {
      const canvas = await html2canvas(image, { scale: 2 });
      canvas.toBlob(async (blob) => {
        if (!blob) {
          alert("이미지 파일이 생성되지 않았습니다.");
          return;
        }
        return blob;
      });
    } catch (err) {
      console.log(`이미지 생성성 중 에러 발생`, err);
    }
  };

  const handleSaveImage = (blob : Blob) => {
    saveAs(blob, "life-graph.png");
  };

  const handleCopyImage = (blob: Blob) => {
    navigator.clipboard.write([
      new ClipboardItem({
        "image/png": blob,
      }),
    ]);
    alert("이미지가 클립보드에 저장되었습니다.");
  };
  
  ...
  return (
    <button
    type="button"
    id="copy-button"
    onClick={() => handleCopyImage(getImage())} // ⚠️ 파라미터 타입 불일치
    data-html2canvas-ignore
  >
    📋 복사
  </button>
  )

하나의 함수 안에서 이미지 blob 생성 + blob 넘겨서 복사/저장 하던 코드를 각각 분리해주었다. 

그런데 getImage() 함수는 비동기 작업 후 Promise를 반환하기 때문에 handleCopyImage 함수의 파라미터로 넘겨줄 수가 없다 ㅠ!

일단 위에서 작성한 getImage()함수의 반환값이 Blob | null 타입을 보장하도록 수정한 함수...

  const getImage = async (): Promise<Blob | null> => {
    const image = imageRef.current;
    if (!image) return null;
    try {
      const canvas = await html2canvas(image, { scale: 2 });
      return new Promise((resolve) => {
        canvas.toBlob((blob) => {
          if (!blob) {
            alert("이미지 파일이 생성되지 않았습니다.");
            resolve(null);
          } else {
            resolve(blob);
          }
        }, "image/png");
      });
    } catch (err) {
      console.log(`이미지 생성성 중 에러 발생`, err);
      return null;
    }
  };

 

 

Blob을 핸들러 파라미터로 전달하기 위해 결국은 이렇게 await 을 사용해야 하는데, 이건 내가 의도한 바가 아니다! 

(비동기 컨텍스트로 빠져나감 -> 보안 정책상 클립보드 접근 차단)

<button 
  type="button" 
  onClick={async () => {
    const blob = await getImage();
    if (blob) handleCopyImage(blob);
  }} 
>
  📋 복사
</button>

 

지피티가 .then 을 써보라고 해서 이렇게 작성하니까 alert 창은 뜨는데 이미지는 여전히 복사가 안된다 ☠️ ☠️ ☠️

<button
    type="button"
    onClick={() => {
      getImage().then((blob) => {
        if (blob) handleCopyImage(blob);
      });
    }}
    >
    📋 복사
</button>

 

왜 이런 차이가 발생할까? 

then 은 promise 는 비동기로 처리하고 = getImage()에서 이미지 캡쳐 진행 중

먼저 할수 있는 동기 작업을 먼저 처리한다. = alert() 띄우기 

 

반면에 await를 썼을때는 promise 처리될때까지 기다렸다가 alert를 띄우기때문에 

alert도 비동기 흐름으로 넘어가서 실행되지 않는것

더보기

async/await vs. then : 두 가지 모두 비동기 처리를 위해 사용되며, Promise를 다룬다. 

=> Promise가 fulfilled 상태가 된 이후에 콜백 함수가 실행된다. 

그러나 코드의 실행 흐름이 다르다고 한다. 

 

async/await : 비동기 작업을 동기적인 코드 흐름처럼 보이도록 한다. - 사실 이 말이 잘 이해가 안감

then : 콜백방식으로 체이닝 - 완전히 비동기 방식, 이벤트 루프에 의해 나중에 실행 

// ✅ await 사용
async function exampleAwait() {
  console.log("1. 시작");
  const result = await new Promise((resolve) => 
    setTimeout(() => resolve("2. 데이터 로드 완료"), 1000)
  );
  console.log(result);
  console.log("3. 끝");
}

exampleAwait();

// ✅ then 사용
function exampleThen() {
  console.log("1. 시작");
  new Promise((resolve) => 
    setTimeout(() => resolve("2. 데이터 로드 완료"), 1000)
  ).then((result) => {
    console.log(result);
  });
  console.log("3. 끝");
}

exampleThen();

await 방식 출력값 : 

1. 시작 (1초 후) 2. 데이터 로드 완료 3.

 => 비동기를 동기처럼 보이게 함 ! 

await은 promise의 콜백함수가 끝나는것까지 기다린다.

 

then 방식 출력값 :

1. 시작 3. 끝 (1초 후) 2. 데이터 로드 완료

비동기는 일단 두고 동기 작업먼저 처리한다.

 

아무튼 어쨌든 비동기라서 클립보드에 접근이 안된다 ㅠㅠ 


비동기와 이벤트 루프 참조 블로그

 

[JS] 비동기, 이벤트 루프, Promise, async-await 정리

자바스크립트를 처음 배울 때 가장 어렵다고 느낀 부분 중 하나는 '비동기'에 관한 것이었다. 비동기의 개념과 동작 원리, Promise, async-await 등에 관하여 나만의 언어로 정리해 본 글이다. 1. '비동

velog.io


 

https://velog.io/@haryan248/Safari-Clipboard-%EC%9D%B4%EC%8A%88

 

Safari - Clipboard 이슈

썸네일 IE가 없어지더니 Safari가 문제다 🤬🤬🤬🤬 비동기로직을 통해 클립보드에 복사할 상황이 생겨 개발하던 도중 겪은 이슈이다. 비동기 객체를 복사하기 단순히 텍스트를 복사할 때는 Clip

velog.io

이 블로그에서 소개해준 방법도 실패..


됐다..

하핫 그렇다면 클릭했을때 비동기작업 (이미지 캡쳐)가 일어나지 않고, 저장해둔 이미지를 바로 클립보드에 넘겨주면 되는거잖아? 싶어서 시도한 방법이 먹혔당

 

// 로컬 상태 추가
const [imageBlob, setImageBlob] = useState<Blob | null>(null);
const imageRef = useRef<HTMLDivElement>(null);
const image = imageRef.current;

// getImage함수 실행 : 그래프 이미지 캡쳐 - 컴포넌트 마운트시, imageRef 변경시, resultType 변경 시 
  useEffect(() => {
    if (image) getImage().then((blob) => setImageBlob(blob));
  }, [image, resultType]);

// 이미지 저장 버튼 핸들러 -> 상태로 관리하는 blob을 그대로 saveAs에 넘겨줌
  const handleSaveImage = async () => {
    if (imageBlob) saveAs(imageBlob, "life-graph.png");
  };

// 이미지 복사 버튼 핸들러 -> 상태로 관리하는 blob을new ClipboardItem 으로 전달
  const handleCopyToClipboard = () => {
    if (!imageBlob) return;

    try {
      navigator.clipboard.write([
        new ClipboardItem({
          "image/png": imageBlob,
        }),
      ]);
      alert("이미지가 클립보드에 저장되었습니다.");
    } catch (err) {
      console.error("클립보드 복사 실패:", err);
      alert("클립보드 복사에 실패했습니다.");
    }
  };

 

유저가 이미지 저장이나 복사를 요청하지 않아도 getImage 함수를 실행하는게 성능에 좋지않은 영향을 줄까 싶었지만

이미지 캡처는 한번만 발생하니까 크게 무리 없다고 판단했다. 

그리고 일단 기능이 되잖아~~!

모바일에서 복사 버튼 숨겨야하나 했는데 매우 뿌듯하게 포스팅 마무리할 수 있어 기분 좋다 : >

이미지를 클릭하면 사이트로 이동합니다.