웹 개발을 하다 보면 외부 이미지 리소스를 처리할 때 종종 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
- Access-Control-Allow-Headers: 허용할 헤더를 지정하거나 모든 헤더를 허용하려면
*
- Access-Control-Allow-Methods: 허용할 HTTP 메서드를 지정합니다.
모든 메서드를 허용하려면 GET, POST, PUT, DELETE, OPTIONS 등을 지정
- Access-Control-Allow-Origin: 허용할 특정 도메인을 지정하거나 모든 도메인을 허용하려면
*
- Access-Control-Expose-Headers: 브라우저가 접근할 수 있게 하려는 헤더를 지정
모든 헤더를 노출 시키지 않으려면 이 필드를 비워두거나, 특정 헤더만 지정먼저 S3 버킷의 CORS 설정을 확인했다.
설정은 모든 출처에서의 요청을 허용하도록 되어 있어 문제가 없는 것으로 파악
캐시된 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에 타임스탬프를 추가하는 캐시 우회 방법이 필요할 수 있다.
개발 환경인 경우 일시적으로 프론트엔드 개발 서버의 프록시 설정을 사용하여 요청을 중계할 수 있다.
다만, 이 방법은 개발 환경일 경우만 가능 배포 환경에서는 사용할 수 없음
package.json
에서 아래와 같이 proxy 설정을 추가{
"proxy": "https://버킷이름.s3.ap-northeast-2.amazonaws.com"
}
/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}` />
// 데이터 URI의 경우 캐시 우회가 필요 없기 때문에 조건문으로 분기
// 만약 데이터 URI, 일반 URL 같이 코드를 사용한다 하면 아래와 같이 사용
const imageCachedUrl = (url) => {
const imgUrl = /^data:image/.test(url) ? url : url + '?' + new Date().getTime();
return imgUrl;
}
useRef
Hook을 사용const timestamp = useRef(new Date().getTime()).current;
const imageUrl = "https://example.com/image.jpg" + "?" + timestamp;
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;