📌 작성자 개발 환경
OS: Mac OS
Tool: Visual Studio Code

3 분 소요

til


커스텀 페이지에서 제작한 그림판 시안을 서버로 전송할때 생긴 CORS에러 해결

발생한 에러

til

에러 해석


서버로 이미지를 전송하는 과정에서 CORS에러 발생
GET 요청이 보내진 주소: https://mainproject022.s3.ap-northeast-2.amazonaws.com/ingredient/topping_strawberry side.png 요청을 보낸 도메인: http://localhost:5173

요청을 보낸 도메인인 http://localhost:5173과 이미지를 호스팅하는 도메인인https://mainproject022.s3.ap-northeast-2.amazonaws.com간에 CORS 헤더가 설정되지 않아서 발생한 것

CORS에 대해서

CORS는 웹 페이지에서 다른 도메인의 리소스에 접근할 때 브라우저가 적용하는 보안 정책이다. 기본적으로 동일 출처 정책(Same-Origin Policy)에 의해 다른 도메인의 리소스에 접근하려는 시도는 차단된다. 이 때, CORS 정책을 적용하여 동일 출처 정책을 느슨하게 해서, 웹 페이지에서 특정 조건하에서 다른 도메인의 리소스에 접근할 수 있게 해준다.

해결방식


이미지를 호스팅하는 서버에서 CORS 헤더를 설정하여 특정 도메인에서의 요청을 허용하도록 구성 서버 측에서 Access-Control-Allow-Origin 헤더를 설정하여 허용할 도메인을 지정

처음에는 서버 측에서 헤더에 CORS 설정을 넣어서 해결하는 간단한 문제일줄 알았지만 백엔드 코드를 수정한 뒤에도 문제가 지속됐다.

문제 해결


전체 코드

import { RefObject } from 'react';
import { addCustom } from '../../../api/customApis';
import { LocalStorage } from '../../../utils/browserStorage';
import { LOCAL_STORAGE_KEY_LIST } from '../../../assets/constantValue/constantValue';

// 이미지 데이터 형식 정의
type ImageData = {
  imageUrl: string;
  x: number;
  y: number;
  width: number;
  height: number;
};

// 이미지를 저장하는 함수
const saveAsImage = async (
  images: ImageData[],
  canvasRef: RefObject<HTMLCanvasElement>,
  storeId: number,
  productId: number,
  navigate: (path: string) => void
) => {
  // 캔버스와 컨텍스트 확인
  const canvas = canvasRef.current;
  if (!canvas) {
    return;
  }
  const ctx = canvas.getContext('2d');
  if (!ctx) {
    console.error('Context 2D is not available');
    return;
  }

  // 임시 캔버스 생성 및 이미지 그리기
  const tempCanvas = document.createElement('canvas');
  const tempCtx = tempCanvas.getContext('2d');
  if (!tempCtx) {
    console.error('Failed to get 2D context for temporary canvas');
    return;
  }
  tempCanvas.width = canvas.width;
  tempCanvas.height = canvas.height;
  tempCtx.drawImage(canvas, 0, 0);

  // Data URL 검사 함수 정의
  const isDataURL = (s: string) => {
    return !!s.match(/^data:image\/([a-zA-Z]*);base64,([^"]*)/);
  };

  // 이미지 로드 및 그리기 함수 정의
  const loadAndDrawImage = async (imageData: ImageData) => {
    return new Promise<void>((resolve, reject) => {
      const img = new Image();
      img.crossOrigin = 'anonymous';

      if (isDataURL(imageData.imageUrl)) {
        img.src = imageData.imageUrl;
      } else {
        img.src = `${imageData.imageUrl}?timestamp=${new Date().getTime()}`;
      }

      img.onload = () => {
        tempCtx.drawImage(img, imageData.x, imageData.y, imageData.width, imageData.height);
        resolve();
      };
      img.onerror = () => {
        reject(new Error(`Failed to load image at ${imageData.imageUrl}`));
      };
    });
  };

  // 이미지 로드 및 그리기 함수들을 Promise.all로 실행
  await Promise.all(images.map(loadAndDrawImage));

  // 이미지 데이터의 불투명한 부분 처리
  const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
  for (let i = 0; i < imageData.data.length; i += 4) {
    if (imageData.data[i + 3] === 0) {
      imageData.data[i] = 255;
      imageData.data[i + 1] = 255;
      imageData.data[i + 2] = 255;
      imageData.data[i + 3] = 255;
    }
  }
  tempCtx.putImageData(imageData, 0, 0);

  // 이미지를 Blob 형식으로 변환
  const dataUrl = tempCanvas.toDataURL('image/png');
  const byteString = atob(dataUrl.split(',')[1]);
  const arrayBuffer = new ArrayBuffer(byteString.length);
  const int8Array = new Uint8Array(arrayBuffer);
  for (let i = 0; i < byteString.length; i++) {
    int8Array[i] = byteString.charCodeAt(i);
  }
  const blob = new Blob([int8Array], { type: 'image/png' });
  const file = new File([blob], 'canvas.png', { type: 'image/png' });

  // 로그인 상태 확인 후 이미지 추가
  const accessToken = LocalStorage.get(LOCAL_STORAGE_KEY_LIST.AccessToken);
  if (accessToken) {
    try {
      await addCustom(storeId, productId, file);
      return true;
    } catch (error) {
      alert('이미지 저장에 실패했습니다.');
      return false;
    }
  } else {
    alert('로그인 먼저 해주세요!');
    navigate('/auth');
    return false;
  }
};

export default saveAsImage;

문제가 됐던 부분


  // 이미지 로드 및 그리기 함수 정의
  const loadAndDrawImage = async (imageData: ImageData) => {
    return new Promise<void>((resolve, reject) => {
      const img = new Image();
      img.crossOrigin = 'anonymous';

      if (isDataURL(imageData.imageUrl)) {
        img.src = imageData.imageUrl;
      } else {
        img.src = `${imageData.imageUrl}?timestamp=${new Date().getTime()}`;
      }

      img.onload = () => {
        tempCtx.drawImage(img, imageData.x, imageData.y, imageData.width, imageData.height);
        resolve();
      };
      img.onerror = () => {
        reject(new Error(`Failed to load image at ${imageData.imageUrl}`));
      };
    });
  };
img.crossOrigin = "anonymous";

img.crossOrigin = 'anonymous';는 이미지 요청 시 CORS 정책을 적용하기 위한 설정, 해당 이미지의 원 서버에서 CORS 헤더를 올바르게 설정하고 응답할 경우, 브라우저는 이미지에 대해 CORS를 적용하고 사용자의 권한을 확인하게 된다.

이 설정을 사용하면 이미지를 다운로드할 때 원 서버로부터 허용된 경우에만 이미지를 다운로드 받을 수 있고, 다른 경우에는 차단된다.

댓글남기기