ABOUT ME

-

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

    외부 이미지의 화면 표시와 CORS 승인 뒤 캔버스 PNG 내보내기를 나눈 흐름도
    사진 표시와 픽셀 읽기의 허용 조건을 나눈 자체 제작 개념도입니다.

    외부 사진을 넣은 카드가 화면에서는 잘 보이는데 저장 버튼에서 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
    반응형
Designed by Tistory.