카톡 인앱 브라우저에서 외부 브라우저로 강제 리다이렉트를 시켜서 이미지 저장 기능은 구현했다.
그런데 이미지 복사는 안되는 상황
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하게 트리거 되어야 하며, 그렇지 않은 경우 사용자의 직접적인 요청으로 간주하지 않아 클립보드 작업에 대한 액세스가 차단된다.
클립보드 복사, 링크 공유하기 기능 만들기 | 모바일 사파리,크롬 애플기기 에러
#클립보드 #링크공유 #복붙 #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 함수를 실행하는게 성능에 좋지않은 영향을 줄까 싶었지만
이미지 캡처는 한번만 발생하니까 크게 무리 없다고 판단했다.
그리고 일단 기능이 되잖아~~!
모바일에서 복사 버튼 숨겨야하나 했는데 매우 뿌듯하게 포스팅 마무리할 수 있어 기분 좋다 : >
'개발개발 > LifeGraph_인생그래프' 카테고리의 다른 글
[인생 그래프] 반응형 버그 해결 (0) | 2025.02.20 |
---|---|
[인생 그래프] 텍스트가 그래프 밖으로 튀어나온다 (0) | 2025.02.14 |
[인생 그래프] 카카오톡 인앱 브라우저 탈출하기 (0) | 2025.02.13 |
[인생 그래프] SCSS 반응형 디자인 구현하기 (0) | 2025.02.12 |
[인생 그래프] 이모지 피커를 추가하자 (0) | 2025.02.11 |