S3 이미지 CORS 오류 해결

윤태현·2024년 1월 27일
0

REACT

목록 보기
19/19
post-thumbnail

🐛 문제 발생

이미지 처리 시 CORS 오류

웹 개발을 하다 보면 외부 이미지 리소스를 처리할 때 종종 CORS(Cross-Origin-Resource Sharing) 관련 오류에 직면한다.
특히 AWS S3와 같은 클라우드 스토리지에서 이미지를 가져와 처리해야 할 경우 이 문제는 더욱 자주 발생하는데
예를 들어, S3에서 가져온 이미지를 다시 파일로 변환하거나 Canvas에 새로 그리는 작업 또는 html2canvas와 같은 라이브러리를 사용하는 등 이미지에 접근할 때 주로 발생한다.

프로젝트를 진행하던 중 S3에 저장된 이미지의 색상을 추출하기 위해 color-thief-react 라이브러리를 사용했다. 이때, CORS 정책에 의해 리소스 접근이 제한이 되어 위의 사진과 같이 오류가 발생했고 이를 해결한 과정을 정리하려고 한다.

언어 : TypeScript (v5.2.2)
라이브러리 : React (v18.2.0)
빌드 도구: Vite (v4.4.5)
컴파일러 : SWC


🤔 원인 파악

1. S3 버킷 CORS 권한

  • Access-Control-Allow-Headers: 허용할 헤더를 지정하거나 모든 헤더를 허용하려면 *
  • Access-Control-Allow-Methods: 허용할 HTTP 메서드를 지정합니다.
    모든 메서드를 허용하려면 GET, POST, PUT, DELETE, OPTIONS 등을 지정
  • Access-Control-Allow-Origin: 허용할 특정 도메인을 지정하거나 모든 도메인을 허용하려면 *
  • Access-Control-Expose-Headers: 브라우저가 접근할 수 있게 하려는 헤더를 지정
    모든 헤더를 노출 시키지 않으려면 이 필드를 비워두거나, 특정 헤더만 지정

먼저 S3 버킷의 CORS 설정을 확인했다.
설정은 모든 출처에서의 요청을 허용하도록 되어 있어 문제가 없는 것으로 파악


2. 캐시 문제

  • 캐시된 S3 이미지 응답에 오래된 CORS 설정이 포함되어 있어, 새로운 설정이 반영되지 않는 것이다.

  • 이미지를 가져와 캐시 우회를 하면 해결될 것이라 생각

  • 그렇다면 어떤 부분에서 캐시 우회를 해야 되는지 먼저 데이터 URI와 일반 URL의 차이를 알아야 한다.

데이터 URI

  • 예시: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...
  • data:[<mediatype>][;base64],<data>
  • 데이터 URI는 이미지나 파일 데이터를 직접 URL에 포함시키는 방식
  • 데이터 URI는 외부 리소스를 요청하지 않으므로, CORS 정책의 영향을 받지 않는다.
  • 리소스가 이미 인코딩된 문자열로 제공되므로, 캐시 우회가 필요 없다.

일반 URL

  • 예시: https://example.com/image.jpg
  • 일반적은 웹 URL
  • 외부 서버에서 이미지나 파일을 불러오는 경우에 사용
  • 네트워크 요청을 통해 리소스를 로드하므로, CORS 정책에 의해 제한될 수 있다.
  • 캐시된 데이터가 아닌 최신 데이터를 받기 위해 URL에 타임스탬프를 추가하는 캐시 우회 방법이 필요할 수 있다.

📝 문제 해결 과정

1. Proxy 사용

개발 환경인 경우 일시적으로 프론트엔드 개발 서버의 프록시 설정을 사용하여 요청을 중계할 수 있다.
다만, 이 방법은 개발 환경일 경우만 가능 배포 환경에서는 사용할 수 없음

1-1. create-react-app을 사용하는 경우

  • package.json에서 아래와 같이 proxy 설정을 추가
{
  "proxy": "https://버킷이름.s3.ap-northeast-2.amazonaws.com"
}

1-2. Vite를 사용하는 경우

  • vite.config.ts 파일을 아래와 같이 수정
  • /s3-bucket으로 src를 설정하면 이미지 요청이 로컬 서버를 통해 프록시되고 CORS문제가 발생하지 않음
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    proxy: {
      // S3 버킷에 대한 프록시 설정 추가
      "/s3-bucket": {
        target: "https://버킷이름.s3.ap-northeast-2.amazonaws.com",
        changeOrigin: true,
        secure: false,
        rewrite: (path) => path.replace(/^\/s3-bucket/, '')
      },
    },
  }
});
<img src=`/s3-bucket/${imageName}` />

2. 캐시 우회 방법

  • 일반 URL의 경우 이미지 URL에 타임스탬프를 추가하여 각 요청이 고유하게 처리되도록 함으로써 브라우저 캐시를 우회할 수 있다.
// 데이터 URI의 경우 캐시 우회가 필요 없기 때문에 조건문으로 분기 
// 만약 데이터 URI, 일반 URL 같이 코드를 사용한다 하면 아래와 같이 사용
const imageCachedUrl = (url) => {
  const imgUrl = /^data:image/.test(url) ? url : url + '?' + new Date().getTime();
  return imgUrl;
}
  • 이 방법은 브라우저가 URL을 새로운 요청으로 인식하게 하여 캐시된 버전을 사용하지 않고 새로운 요청을 서버에 보내게 된다.

2-1. React에서의 무한 리렌더링

  • 캐시 우회 방법을 사용하면 URL이 지속적으로 변화하기 때문에, React 컴포넌트에서는 이를 새로운 Prop으로 간주하여 무한 리렌더링 문제가 발생할 수 있다.
  • 이를 방지하기 위해 useRef Hook을 사용
const timestamp = useRef(new Date().getTime()).current;
const imageUrl = "https://example.com/image.jpg" + "?" + timestamp;
  • 컴포넌트가 처음 렌더링될 때, 타임스탬프를 생성하고 이후 재렌더링에서는 동일한 타임스탬프 값을 유지하여 불필요한 리렌더링을 막을 수 있다.

2-2. 코드 적용

color-thief-react 라이브러리 사용

import { useColor } from "color-thief-react";
import { useRef } from "react";

const usePickMainImageColor = (imageUrl) => {
  const timestamp = useRef(new Date().getTime()).current;
 
  const { data: main_image_color } = useColor(
    `https://s3-bucket/path/your/image.jpg?${timestamp}`,
    "hex",
    {
      crossOrigin: "anonymous",
    },
  );
  return main_image_color;
};

export default usePickMainImageColor;

S3에서 가져온 이미지를 다시 파일로 변환

const convertUrlToFile = async (url) => {
  const response = await fetch(url + '?' + new Date().getTime());
  const data = await response.blob();
  const filename = url.split("/show/")[1];
  const extension = url.split(".").pop();
  const metadata = { type: `image/${extension}` };
  return new File([data], filename, metadata);
};

(async () => {
  try {
    const mainImageFile = await convertUrlToFile('https://s3-bucket/path/your/image.jpg');
    console.log(mainImageFile);
  } catch (error) {
    console.error('Error:', error);
  }
})();

S3에서 가져온 이미지를 canvas에 다시 그릴 경우

function loadImageToCanvas(url, callback) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  const img = new Image();
  img.crossOrigin = 'Anonymous'; // CORS 정책 준수
  img.onload = function() {
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);

    // 콜백 함수로 캔버스 처리
    callback(canvas);
  };
  img.onerror = function() {
    console.error('이미지 로딩 실패');
  };
  img.src = url + '?' + new Date().getTime();
}

// 사용 예시
loadImageToCanvas('https://s3-bucket/path/your/image.jpg', (canvas) => {
  // 여기에서 canvas를 사용하여 필요한 작업 수행
  // 예: 캔버스의 내용을 이미지 또는 또 다른 데이터 형태로 변환
});

S3에서 가져온 이미지를 html2canvas 라이브러리를 사용해서 적용하는 경우

import React, { useRef, useEffect } from 'react';
import html2canvas from 'html2canvas';

const Html2CanvasWithS3Image = () => {
  const imageRef = useRef(null);
  const canvasRef = useRef(null);

  // S3 이미지 URL
  const s3ImageUrl = "https://s3-bucket/path/your/image.jpg";
  // 타임스탬프를 추가하여 캐시 우회
  const cachedImageUrl = s3ImageUrl + '?' + new Date().getTime();

  useEffect(() => {
    if (imageRef.current) {
      // html2canvas로 이미지를 포함하는 요소를 캔버스로 변환
      html2canvas(imageRef.current)
        .then(canvas => {
          canvasRef.current.appendChild(canvas);
        })
        .catch(error => console.error("html2canvas error:", error));
    }
  }, []);

  return (
    <div>
      <div ref={imageRef}>
        <img src={cachedImageUrl} alt="S3 Image" />
      </div>
      <div ref={canvasRef}></div>
    </div>
  );
};

export default Html2CanvasWithS3Image;

0개의 댓글