-
이미지는 보이는데 캔버스 PNG 저장은 왜 실패할까Programming 2026. 10. 2. 07:50728x90반응형

사진 표시와 픽셀 읽기의 허용 조건을 나눈 자체 제작 개념도입니다. 외부 사진을 넣은 카드가 화면에서는 잘 보이는데 저장 버튼에서
SecurityError가 난다면, 사진의 CORS 설정부터 확인하세요.<img>에 표시할 수 있다는 사실만으로 JavaScript가 그 픽셀을 읽을 권한까지 얻지는 않습니다.캔버스에 외부 사진을 그린 뒤
toBlob()이나toDataURL()을 호출하는 흐름에서 이 차이가 드러납니다. 글자나 배경색을 바꾸기 전에, 어떤 주소에서 읽은 이미지가 캔버스에 들어갔는지 찾는 편이 빠릅니다.저장할 때 뒤늦게 막히는 이유
브라우저는 캔버스의 비트맵에
origin-clean상태를 관리합니다. CORS 승인을 받지 않은 다른 출처의 이미지가 들어오면 그 상태가 깨집니다. 흔히 캔버스가 오염됐다고 표현하는 상황입니다. 이미지 자체를 못 그리는 것이 아니라, 그린 결과를 스크립트로 읽어 내보내는 단계가 막힙니다. HTML 표준의 캔버스 보안 규칙여기서 출처는 프로토콜·호스트·포트의 조합입니다.
https://app.example의 페이지와https://images.example의 사진은 서로 다른 출처입니다. 같은 회사가 운영한다거나 도메인의 끝부분이 같다는 것으로 합쳐지지 않습니다. 동일 출처의 정의사진 한 장이 원인이어도 전체 캔버스 내보내기가 영향을 받습니다. 그 위에 색을 덮거나 다른 깨끗한 그림을 추가한다고 해당 상태가 풀리는 것은 아닙니다. 깨끗한 새 캔버스를 만들고 허용된 자료로 다시 그려야 합니다. 캔버스 크기 재설정은 비트맵도 초기화하므로 이미 그린 내용을 살려 주는 복구 방법으로 생각하면 안 됩니다.
요청을 보내는 쪽과 사진을 주는 쪽을 함께 바꿉니다
앱은 이미지를 CORS 방식으로 요청하고, 이미지 서버는 그 요청의 출처를 허용해야 합니다. 공개 사진을 인증 없이 가져오는 경우에는 다음과 같은 코드로 요청할 수 있습니다. 주소는 설명용이며 실제로 동작하는 이미지 URL로 바꿔야 합니다.
async function makePng(imageUrl) { const photo = new Image(); photo.crossOrigin = "anonymous"; photo.src = imageUrl; await photo.decode(); const canvas = document.createElement( "canvas" ); canvas.width = photo.naturalWidth; canvas.height = photo.naturalHeight; const context = canvas.getContext("2d"); if (!context) { throw new Error("No context"); } context.drawImage(photo, 0, 0); return new Promise((ok, no) => { canvas.toBlob((blob) => { if (blob) ok(blob); else { no(new Error("No PNG")); } }, "image/png"); }); }crossOrigin은src보다 먼저 지정합니다. 사진을 이미 받은 뒤 속성만 바꾸는 흐름과 섞지 않기 위해서입니다.decode()를 기다린 다음 원본 픽셀 크기로 캔버스를 만들고 그립니다. 이 함수는 PNG 데이터를 담은 Blob을 반환할 뿐이며, 다운로드 버튼과 파일명 처리는 별도로 붙입니다. 이미지 로딩과 CORS 예제이미지 서버의 응답에는 앱의 출처를 허용하는 헤더가 필요합니다. 위 예의 앱이라면 다음처럼 정할 수 있습니다.
헤더 이름은
Access-Control-Allow-Origin, 값은https://app.example입니다.여러 출처가 요청하는 공개 이미지라면 서버 정책에 따라
*를 사용할 수도 있습니다. 쿠키 등 자격 증명을 포함하는 CORS 요청에는 다른 조건이 적용되므로, 인증이 필요한 사진에 이 설정을 그대로 복사해서는 안 됩니다. 허용 출처를 요청마다 바꿔 응답한다면 캐시도 출처별 응답을 구분하도록Vary: Origin을 검토합니다. CORS 응답 헤더와 자격 증명사진 한 장으로 먼저 범위를 좁힙니다
가상 카드 편집기에 배경 사진, 작은 로고, 사용자 문구가 있다고 해 보겠습니다. 배경 사진의 서버 헤더를 고쳤는데도 저장이 실패한다면 로고도 같은 조건을 만족하는지 봐야 합니다. 완성된 카드만 반복해서 저장하면 어느 입력이 원인인지 알기 어렵습니다.
빈 캔버스에 글자만 넣어 저장한 뒤, 배경 사진 한 장을 추가하고, 마지막에 로고를 추가하는 순서로 나눠 확인하세요. 새 캔버스를 매번 사용하는 이유는 이전 단계에서 깨진 상태가 다음 검사에 남는 일을 피하기 위해서입니다. 특정 이미지를 추가하는 순간 실패한다면 그 이미지 요청을 개발자 도구의 Network에서 확인합니다.
이때 살필 것은 파일명보다 실제 최종 응답입니다. 처음 주소가 다른 주소로 리디렉션되거나 CDN에서 응답하면, 앱이 기대한 서버 설정과 실제 이미지 응답이 다를 수 있습니다. 요청의
Origin, 응답의Access-Control-Allow-Origin, 실패한 요청 상태를 함께 봅니다. URL을 브라우저 새 탭에서 열어 보는 검사만으로는 앱 출처에서의 CORS 승인을 확인할 수 없습니다.crossOrigin을 추가한 뒤 아예 사진 로딩이 실패할 수도 있습니다. 이전에는 표시만 가능한 방식으로 받았던 사진을 이제 CORS 요청으로 받으면서 서버의 허용 여부가 드러난 것입니다. 속성을 다시 지워 화면만 복구하면 내보내기 실패도 돌아옵니다. 이미지 서버를 수정할 수 있는지부터 판단해야 합니다.외부 서버를 바꿀 수 없다면
사용 권한이 있는 이미지를 앱과 같은 출처에서 제공하거나, 적절한 CORS 헤더를 주는 저장소로 옮길 수 있습니다. 백엔드에서 이미지를 받아 전달하는 방식도 설계할 수 있지만, 임의 URL을 서버가 대신 읽도록 열어 두면 별도의 접근 제한이 필요합니다. 이미지 제공 방식까지 운영할 수 없다면 그 자료를 사용하는 카드의 저장 기능을 제한하는 판단도 필요합니다.
fetch(..., {mode: "no-cors"})는 응답을 자유롭게 읽도록 허용하는 옵션이 아닙니다. 다른 출처의 응답은 불투명한 응답이 되어 JavaScript가 그 내용을 읽는 데 제약이 생깁니다. 외부 사진을 PNG로 내보내려는 문제의 해결책으로 붙이지 마세요. Fetch의 요청 모드먼저 사진 한 장으로 요청과 응답을 확인하고, 그 사진만 그린 새 캔버스에서 Blob을 얻는지 검사하세요. 그다음 편집기의 나머지 레이어를 붙이면 이미지 접근 문제와 합성 코드 문제를 분리해 수정할 수 있습니다.
2026년 9월 28일 HTML 표준과 MDN 문서를 확인했습니다. 코드는 공개 이미지의 익명 CORS 흐름을 설명하며, 특정 브라우저·CDN에서 실행한 결과를 제시한 것은 아닙니다.
728x90반응형'Programming' 카테고리의 다른 글
자동재생을 켰는데 비디오가 멈춰 있는 이유 (0) 2026.10.04 같은 SVG인데 img로 넣을 때와 직접 열 때가 다른 이유 (0) 2026.10.02 Docker Compose는 켜졌는데 앱이 DB에 연결하지 못하는 이유 (0) 2026.10.02 컨테이너에 있던 파일이 볼륨을 연결하자 사라진 이유 (0) 2026.10.02 .gitignore에 넣었는데 왜 변경 파일에 계속 나올까 (0) 2026.10.02