unno 프로젝트에서 사용자가 이미지를 업로드하면 해당 파일은 Supabase Storage에 저장된다. 이후 명함을 새로 만들 때도 기존에 업로드한 이미지들을 재사용할 수 있도록 DB에 저장된 이미지 URL을 기반으로 캔버스(Stage)에 추가하는 구조로 설계했다.
하지만, 캔버스에서 Stage.toBlob()을 통해 이미지를 저장하려고 할 때, 일부 이미지가 포함된 Stage에서 Blob 변환이 실패하는 문제가 발생했다.
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을 반환현상
toBlob() 결과가 null로 반환됨{
"previewUrl": "blob:http://localhost:3000/...",
"height": 98.54,
...
}
{
"previewUrl": "https://your-project.supabase.co/storage/v1/object/public/uploadimg/...",
"height": 84.23,
...
}
이 문제의 본질은 CORS (Cross-Origin Resource Sharing) 정책 때문이다.
브라우저는 다른 origin의 이미지를 <canvas>에 그릴 경우, 해당 canvas를 tainted 상태로 간주한다.
이 상태에서는 .toBlob(), .toDataURL() 등 출력 관련 API 사용이 제한된다.
특히 Konva.Image는 <img> 태그 기반이므로, 다음 조건이 필요하다:
- crossOrigin="anonymous" 설정
- 이미지 서버(Supabase)에서 CORS 헤더 허용
Supabase 프로젝트에서 다음과 같이 CORS 설정을 추가해야 한다
[
{
"allowed_origins": ["*"], // 또는 "https://uuno.vercel.app"
"allowed_methods": ["GET"],
"allowed_headers": ["*"],
"max_age": 86400
}
]
allowed_origins: 실제 배포 도메인을 등록
allowed_methods: 최소한 GET은 포함해야 함
useImage 훅에서 'anonymous' 옵션을 명시해야 한다.
const [image] = useImage(element.previewUrl, 'anonymous');
element-image-canvas.tsx, element-upload-canvas.tsx 등 모든 이미지 요소 컴포넌트에 이 설정을 추가해야 함
toBlob()이 안정적으로 Blob 객체를 반환 ✅
Supabase 이미지가 포함된 canvas도 문제 없이 저장 가능 ✅
업로드한 이미지 재활용 시에도 CORS 에러 없이 저장 가능 ✅
crossOrigin 설정이 누락되면 에러 없이 그냥 null을 반환하기 때문에 에러 처리를 반드시 해야 한다.