Konva toBlob() 이슈와 Supabase 이미지 CORS

최종욱·2025년 4월 22일

Uuno

목록 보기
2/7
post-thumbnail

문제 상황

unno 프로젝트에서 사용자가 이미지를 업로드하면 해당 파일은 Supabase Storage에 저장된다. 이후 명함을 새로 만들 때도 기존에 업로드한 이미지들을 재사용할 수 있도록 DB에 저장된 이미지 URL을 기반으로 캔버스(Stage)에 추가하는 구조로 설계했다.

하지만, 캔버스에서 Stage.toBlob()을 통해 이미지를 저장하려고 할 때, 일부 이미지가 포함된 Stage에서 Blob 변환이 실패하는 문제가 발생했다.


문제 발생: toBlob이 null을 반환함

export const uploadStageImage = async (
  stage: Konva.Stage,
  userId: string,
  lastSlug: string,
  label: 'front' | 'back'
): Promise<string> => {
  const blob: Blob = await new Promise<Blob>((resolve, reject) => {
    stage.toBlob({
      mimeType: 'image/png',
      pixelRatio: 2,
      callback: (b: Blob | null) => {
        if (b) resolve(b);
        else reject(new Error('Konva.toBlob 반환값이 null입니다.'));
      },
    });
  });

  const fileName = `${label}_${lastSlug}_img.png`;
  const filePath = `${userId}/${lastSlug}/${fileName}`;

  return uploadToSupabaseStorage('cards', filePath, blob);
};

의도

  • Konva.Stage를 이미지로 변환하여 Supabase에 업로드하고, 해당 public URL을 반환

현상

  • 특정 이미지가 포함된 Stage에서는 toBlob() 결과가 null로 반환됨

상황 비교

업로드 직후 캔버스에 추가된 이미지

{
  "previewUrl": "blob:http://localhost:3000/...",
  "height": 98.54,
  ...
}
  • 로컬 Blob URL이므로 보안 제약 없이 사용 가능

DB에서 가져온 Supabase 이미지 URL

{
  "previewUrl": "https://your-project.supabase.co/storage/v1/object/public/uploadimg/...",
  "height": 84.23,
  ...
}
  • Supabase의 도메인이 다른 origin이기 때문에 브라우저 보안 정책에 의해 문제가 발생

원인 분석

이 문제의 본질은 CORS (Cross-Origin Resource Sharing) 정책 때문이다.

  • 브라우저는 다른 origin의 이미지를 <canvas>에 그릴 경우, 해당 canvas를 tainted 상태로 간주한다.

  • 이 상태에서는 .toBlob(), .toDataURL() 등 출력 관련 API 사용이 제한된다.

  • 특히 Konva.Image<img> 태그 기반이므로, 다음 조건이 필요하다:
    - crossOrigin="anonymous" 설정
    - 이미지 서버(Supabase)에서 CORS 헤더 허용


해결 방법

1. Supabase Storage의 CORS 정책 설정

Supabase 프로젝트에서 다음과 같이 CORS 설정을 추가해야 한다

[
  {
    "allowed_origins": ["*"], // 또는 "https://uuno.vercel.app"
    "allowed_methods": ["GET"],
    "allowed_headers": ["*"],
    "max_age": 86400
  }
]
  • allowed_origins: 실제 배포 도메인을 등록

  • allowed_methods: 최소한 GET은 포함해야 함

2. Konva 이미지 로딩 시 crossOrigin 속성 지정

useImage 훅에서 'anonymous' 옵션을 명시해야 한다.

const [image] = useImage(element.previewUrl, 'anonymous');

element-image-canvas.tsx, element-upload-canvas.tsx 등 모든 이미지 요소 컴포넌트에 이 설정을 추가해야 함


적용 결과

  • toBlob()이 안정적으로 Blob 객체를 반환 ✅

  • Supabase 이미지가 포함된 canvas도 문제 없이 저장 가능 ✅

  • 업로드한 이미지 재활용 시에도 CORS 에러 없이 저장 가능 ✅


마무리 팁

  • Supabase Storage에 이미지를 업로드할 때는 반드시 public 폴더에 업로드하고,
  • crossOrigin 설정이 누락되면 에러 없이 그냥 null을 반환하기 때문에 에러 처리를 반드시 해야 한다.
profile
항상 “Why?”로 시작하는 프론트엔드 개발자

0개의 댓글