티스토리 뷰

반응형

Cloud CDN 연동 시 CORS 문제 해결: Fetch API와 이미지 태그 요청의 차이점

Cloud CDN을 통해 GCP 버킷에 저장된 파일에 접근할 때, <img> 태그의 src 속성으로 이미지를 불러오는 것과 JavaScript의 fetch API를 사용하는 경우 CORS(Cross-Origin Resource Sharing) 문제가 발생하는 경우가 있습니다. 본 포스팅에서는 이러한 상황의 원인을 분석하고, 정확하고 자세한 해결 방법을 제시하며, 관련 gcloud 명령어 예시와 더불어 preflight 요청의 동작 방식까지 함께 알아보겠습니다.


🚀 문제 상황: Cloud CDN과 함께하는 버킷 파일 접근, fetch만 CORS 에러?

Cloud CDN이 연결된 Google Cloud Platform (GCP) 버킷에 저장된 파일, 특히 이미지를 다운로드하는 시나리오를 가정해 봅시다.

  • <img> 태그를 통한 접근: HTML의 <img> 태그의 src 속성에 버킷 URL을 지정하여 이미지를 불러오는 것은 정상적으로 작동합니다.
  • JavaScript fetch API를 통한 접근: 동일한 버킷 URL을 사용하여 JavaScript의 fetch API로 파일을 다운로드하려고 하면 CORS 오류가 발생합니다.

이러한 상황은 종종 개발자들에게 혼란을 야기하며, 왜 한 방식은 되고 다른 방식은 안 되는지에 대한 의문을 품게 합니다.


💡 원인 분석: fetch는 왜 CORS 에러를 뿜는가?

결론부터 말하자면, 파일 서버 (여기서는 GCP 버킷)에서 요청하는 Origin (프론트엔드 애플리케이션의 출처)에 대한 명시적인 허용이 없기 때문에 fetch 요청 시 CORS 문제가 발생합니다.

그렇다면 왜 <img> 태그는 문제가 없는 것일까요? 이는 브라우저의 기본적인 동작 방식과 관련이 있습니다.

  • <img> 태그의 src 요청: <img> 태그의 src 속성을 통해 이미지를 로드하는 것은 브라우저가 "No-CORS" (혹은 "Same-Origin") 요청으로 간주합니다. 즉, 브라우저는 이 요청을 단순한 리소스 로딩으로 처리하며, SOP(Same-Origin Policy)의 엄격한 CORS 검증을 거치지 않습니다. 서버에서 Access-Control-Allow-Origin 헤더가 없더라도 브라우저는 파일을 가져와 표시하려고 시도합니다.
  • fetch API의 요청: 반면, JavaScript의 fetch API는 기본적으로 "CORS-aware" 요청입니다. fetch는 브라우저의 CORS 메커니즘을 따르며, 서버가 허용하지 않는 Origin으로부터의 요청에 대해서는 엄격하게 차단하고 CORS 오류를 발생시킵니다.

즉, fetch는 서버에게 "내가 이 Origin인데, 이 데이터를 요청해도 될까요?"라고 미리 묻는(Preflight) 과정을 거칠 수 있으며, 서버의 응답에 따라 접근 여부가 결정됩니다.


🛠️ 해결 방법: GCP 버킷 CORS 설정 업데이트 및 CDN 캐시 무효화

이 문제를 해결하기 위해서는 Cloud Storage 버킷 자체에 CORS 설정을 적용하여, fetch API가 접근하려는 Origin을 허용해 주어야 합니다.

1. CORS 설정용 JSON 파일 생성

먼저, 허용할 Origin 및 HTTP 메서드 등을 정의한 JSON 파일을 생성합니다. 이 파일은 gcloud 명령어를 통해 버킷에 적용됩니다.

  • cors-config.json (예시):**origin: 프론트엔드 애플리케이션이 실행되는 도메인 (포트 번호 포함)을 명시합니다. 여러 Origin을 허용해야 한다면 배열로 나열합니다. 보안을 위해 * (모든 Origin 허용) 보다는 특정 Origin을 명시하는 것이 좋습니다.
    **method
    : 허용할 HTTP 메서드를 지정합니다. 파일 다운로드의 경우 GETHEAD가 주로 사용됩니다.
    responseHeader: 클라이언트가 요청할 수 있는 응답 헤더를 지정합니다. CORS 관련 헤더인 Access-Control-Allow-Origin을 포함하는 것이 일반적입니다.
    maxAgeSeconds: 브라우저가 Preflight 요청 결과를 캐싱하는 시간을 설정합니다. 이 시간 동안에는 동일한 Origin, 메서드, 헤더 조합에 대해 Preflight 요청이 다시 발생하지 않습니다.
  • [ { "origin": [ "http://localhost:3000", // 로컬 개발 환경 (React, Vue 등) "https://your-frontend-domain.com" // 실제 배포된 프론트엔드 도메인 ], "method": ["GET", "HEAD"], // 허용할 HTTP 메서드 (다운로드 시 GET, HEAD) "responseHeader": [ "Content-Type", "Access-Control-Allow-Origin" // CORS 응답 헤더 포함 ], "maxAgeSeconds": 3600 // Preflight 요청 결과를 캐싱할 시간 (초) } ]

2. 버킷에 CORS 설정 업데이트 (gcloud 명령어)

생성한 cors-config.json 파일을 사용하여 버킷에 CORS 설정을 적용합니다.

# 현재 활성화된 프로젝트 확인 (필요시)
gcloud config configurations list
# gcloud config configurations activate <your-configuration-name>

# CORS 설정을 적용할 Cloud Storage 버킷 이름
YOUR_BUCKET_NAME="your-gcs-bucket-name"

# CORS 설정을 적용할 JSON 파일 경로
CORS_FILE="cors-config.json"

# 버킷에 CORS 설정 적용
gcloud storage buckets update gs://${YOUR_BUCKET_NAME} \
    --cors-file=${CORS_FILE}

명령어 설명:

  • gcloud storage buckets update gs://${YOUR_BUCKET_NAME}: 지정된 버킷의 설정을 업데이트하는 명령어입니다.
  • --cors-file=${CORS_FILE}: 앞에서 생성한 JSON 파일의 경로를 지정하여 CORS 정책을 설정합니다.

3. CORS 설정 확인

CORS 설정이 올바르게 적용되었는지 확인합니다.

gcloud storage buckets describe gs://${YOUR_BUCKET_NAME} --format="json(cors)"

이 명령어를 실행하면 해당 버킷에 설정된 CORS 관련 정보가 JSON 형식으로 출력됩니다. 앞서 cors-config.json에 입력한 내용과 일치하는지 확인합니다.

4. CDN 캐시 무효화

버킷의 CORS 설정을 변경했다면, Cloud CDN에 캐시된 이전 버전의 콘텐츠(CORS 헤더가 적용되지 않은)가 여전히 제공될 수 있습니다. 따라서 CDN 캐시를 무효화하여 최신 CORS 설정이 반영된 콘텐츠를 가져오도록 해야 합니다.

gcloud 명령어를 사용하여 특정 URL 맵의 캐시를 무효화할 수 있습니다.

# 캐시를 무효화할 URL 맵의 이름 (Cloud CDN 설정에서 확인 필요)
YOUR_URL_MAP_NAME="your-cdn-url-map-name"

# 모든 경로의 캐시 무효화 (가장 일반적)
gcloud compute url-maps invalidate-cache ${YOUR_URL_MAP_NAME} --path="/" --async

# 특정 경로의 캐시 무효화 (예: /images/some-image.jpg)
# gcloud compute url-maps invalidate-cache ${YOUR_URL_MAP_NAME} --path="/images/some-image.jpg" --async

명령어 설명:

  • gcloud compute url-maps invalidate-cache ${YOUR_URL_MAP_NAME}: 지정된 URL 맵에 연결된 CDN 캐시를 무효화합니다.
  • --path="/": 모든 경로의 캐시를 무효화하도록 지정합니다. 특정 파일이나 디렉터리만 무효화하려면 해당 경로를 명시합니다.
  • --async: 작업이 백그라운드에서 비동기적으로 실행되도록 합니다.

이렇게 버킷의 CORS 설정을 업데이트하고 CDN 캐시를 무효화한 후, 다시 fetch API를 사용하여 파일을 다운로드해보면 CORS 문제가 해결된 것을 확인할 수 있습니다.


🧠 알게 된 것: 이미지 src vs. fetch 요청의 차이

이번 경험을 통해 우리는 브라우저가 리소스를 요청하는 방식에 따라 CORS 정책 적용 여부가 달라진다는 것을 알게 되었습니다.

  • <img> 태그: 브라우저가 "No-CORS" 모드로 리소스를 로드하여, 서버의 Access-Control-Allow-Origin 헤더가 없어도 파일을 가져와 렌더링하려고 시도합니다.
  • fetch API: 브라우저의 CORS 메커니즘을 따르며, 서버의 Access-Control-Allow-Origin 헤더가 요청 Origin과 일치해야만 응답을 받고 처리합니다.

따라서 JavaScript를 통해 서버 리소스에 동적으로 접근하거나 조작해야 할 때는 fetch API가 CORS 정책을 엄격하게 준수하기 때문에, 서버 측에서 명확한 CORS 설정을 해주는 것이 필수적입니다.


🧐 심화 학습: Preflight 요청이란 무엇이며, 브라우저는 어떻게 처리할까?

fetch API를 통해 데이터를 주고받을 때 CORS 에러를 마주치면 종종 OPTIONS 메서드로 시작하는 요청을 볼 수 있습니다. 이것이 바로 Preflight 요청입니다. Preflight 요청은 웹 보안을 위한 중요한 메커니즘이며, 브라우저가 어떻게 동작하는지 이해하는 데 필수적입니다.

❓ Preflight 요청은 언제 필요한가요?

모든 CORS 요청이 Preflight를 거치는 것은 아닙니다. 브라우저는 서버에게 "이 요청, 해도 안전한가?" 라고 미리 확인하는 Preflight 과정을 특정 조건에서만 수행합니다.

Preflight 요청이 필요한 경우는 다음과 같습니다.

  1. 안전하지 않은 HTTP 메서드 사용:

    • GET, HEAD, POST (특정 조건)를 제외한 다른 HTTP 메서드를 사용할 때. 예를 들어, PUT, DELETE, OPTIONS, PATCH 등은 서버의 상태를 변경할 가능성이 있어 "안전하지 않은" 것으로 간주됩니다.
    • POST 요청의 경우: Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain 이 세 가지 중 하나일 때는 Preflight가 발생하지 않습니다. 하지만 Content-Typeapplication/json 이거나 사용자 정의 헤더를 사용하는 경우, 이는 "안전하지 않은" 것으로 간주되어 Preflight가 트리거됩니다.
  2. "안전하지 않은" 사용자 정의 헤더 사용:

    • Access-Control-Request-Headers에 명시되지 않은 사용자 정의 헤더를 실제 요청에 포함시킬 때. 예를 들어 Authorization 헤더나 X-Custom-Header 와 같은 헤더는 Preflight를 필요로 합니다.

간단히 말해, 단순한 정보 조회(GET)를 넘어 서버의 상태를 변경하거나, 예측 불가능한 사용자 정의 헤더를 사용하는 "복잡하거나 민감한" 요청일 때 브라우저는 Preflight를 수행하여 안전을 확인합니다.

📊 Preflight 요청 발생 여부 요약 (Chrome 기준)

요청 메서드 Content-Type 헤더 Preflight 발생 여부 비고
GET 어떤 Content-Type 이든 (일반적으로 Content-Type 헤더가 없거나 text/plain 등) 발생 안 함 (항상 안전) 데이터를 조회하는 용도로, 서버 상태를 변경하지 않기 때문입니다.
POST application/x-www-form-urlencoded
multipart/form-data
text/plain
발생 안 함 (안전한 POST) 데이터를 제출하지만, 흔히 사용되는 형식이며 서버 상태 변경이 예측 가능하므로 안전한 POST로 간주됩니다.
POST application/json
text/xml
사용자 정의 Content-Type
발생 함 (안전하지 않은 POST) application/json과 같이 복잡하거나 사용자 정의된 형식은 서버 상태 변경을 야기할 수 있으므로 Preflight가 필요합니다.
PUT application/json
text/xml
사용자 정의 Content-Type (일반적)
발생 함 (안전하지 않은 PUT) PUT은 기본적으로 데이터를 수정하거나 생성하므로 서버 상태를 변경하는 것으로 간주되어 Preflight가 항상 필요합니다. Content-Type과 무관합니다.

💬 Preflight 요청의 과정 및 브라우저의 응답 처리

Preflight 요청은 다음과 같은 흐름으로 진행되며, 브라우저는 서버의 응답을 기반으로 실제 요청 진행 여부를 결정합니다.

  1. 브라우저 → 서버 (Preflight 요청):

    • fetch API 또는 XMLHttpRequest 객체로 요청을 보낼 때, 실제 요청을 보내기 전에 브라우저는 OPTIONS 메서드를 사용하여 Preflight 요청을 보냅니다.
    • 이 Preflight 요청에는 실제 요청에 사용될 정보가 담긴 특별한 헤더들이 포함됩니다.
      • Origin: 요청을 보내는 출처 (예: http://localhost:3000)
      • Access-Control-Request-Method: 실제 요청에서 사용될 HTTP 메서드 (예: POST, PUT)
      • Access-Control-Request-Headers: 실제 요청에 포함될 헤더 목록 (예: Content-Type, Authorization)
  2. 서버 (GCP 버킷)의 응답:

    • GCP 버킷은 Preflight 요청을 받으면, 설정된 CORS 정책을 기반으로 응답합니다.
    • 허용하는 경우 (유효한 응답):
      • Access-Control-Allow-Origin: 요청 Origin과 일치하는 값 (또는 *)
      • Access-Control-Allow-Methods: 허용된 HTTP 메서드 목록 (예: GET, HEAD, POST, PUT)
      • Access-Control-Allow-Headers: 허용된 헤더 목록 (예: Content-Type, Authorization)
      • Access-Control-Max-Age: Preflight 응답 캐싱 시간 (초)
      • 이러한 헤더들이 올바르게 응답되면, 브라우저는 해당 Origin, 메서드, 헤더 조합을 허용된 것으로 간주합니다.
    • 거부하는 경우 (무효한 응답):
      • 요청한 Origin, 메서드, 헤더와 일치하는 CORS 관련 응답 헤더가 없거나, 명시적으로 거부하는 헤더가 포함되면 브라우저는 해당 요청을 차단합니다.
  3. 브라우저의 판단 및 실제 요청:

    • 서버로부터 유효하고 일치하는 Preflight 응답을 받으면, 브라우저는 이제 실제 요청 (예: POST, PUT)을 서버로 보냅니다.
    • Preflight 응답이 없거나 거부 응답을 받은 경우, 브라우저는 CORS 에러를 발생시키고 실제 요청을 보내지 않습니다.

결론적으로, fetch API로 application/json과 같은 안전하지 않은 Content-Type과 함께 POST나 PUT 요청을 보낼 때는 Preflight 요청이 발생하며, 이에 대한 서버 측의 올바른 CORS 응답이 있어야만 실제 요청이 성공적으로 이루어집니다.


#CloudCDN #GCP #CORS #FetchAPI #CloudStorage #PreflightRequest


이 포스팅이 Cloud CDN과 GCP 버킷 연동 시 CORS 문제를 이해하고 해결하는 데 도움이 되셨기를 바랍니다. 웹 개발에서 CORS는 자주 마주치는 중요한 개념이므로, 서버와 클라이언트 양쪽의 설정을 정확하게 이해하고 적용하는 것이 중요합니다.

반응형