본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 05. 26. 11:32

Chrome 확장 기능에서 Google 스프레드시트로의 직접 fetch 시 발생한 문제: GAS 프록시로 CORS 장벽을 회피한 이야기

요약

Chrome 확장 기능에서 Google 스프레드시트 데이터를 직접 fetch할 때 발생하는 CORS 및 인증 리다이렉트 문제를 해결하는 과정을 다룹니다. Content Script와 Background Service Worker를 활용한 시도와 최종적으로 Google Apps Script(GAS)를 프록시로 사용하여 문제를 해결한 기술적 경험을 공유합니다.

핵심 포인트

  • MV3 Content Script의 fetch는 host_permissions가 있어도 CORS 정책의 영향을 받음
  • Background Service Worker로 fetch를 위임해도 인증 리다이렉트 시 CORS 문제가 발생할 수 있음
  • Google Apps Script(GAS)를 프록시로 활용하여 CORS 장벽을 우회하는 해결책 제시
  • AI와의 디버깅을 통해 단순 CORS를 넘어 인증 리다이렉트 문제를 식별함

업무 효율화를 위해, Jira의 타임라인 뷰(Timeline View)에 근태 데이터를 시각화하는 Chrome 확장 기능(Chrome Extension)을 만들고 있었습니다.

구체적으로는, 사내 Google 스프레드시트(Google Sheets)에서 관리하고 있는 근태표를 참조하여, 각 멤버의 「전휴(全休)」일을 Jira의 타임라인 상에서 주황색으로 하이라이트하는 기능입니다.

이 기사에서는 최종적으로 Google Apps Script (GAS)를 프록시(Proxy)로 세움으로써 해결하기까지의 실패 과정도 포함하여 공유합니다.

  • Jira의 타임라인 뷰에 상주하는 Chrome 확장 기능 (Manifest V3)
  • URL의 assignee 파라미터로부터 누구의 타임라인인지 판정
  • Google 스프레드시트에서 해당 사용자의 근태 데이터를 취득
  • 「전휴」인 날을 주황색으로 착색

구현은 AI와의 대화 형식으로 진행하며, 에러 메시지를 붙여넣으면서 원인을 좁혀 나갔습니다. 그 과정에서 "이것은 CORS 문제뿐만 아니라, 인증 리다이렉트(Authentication Redirect)가 얽혀 있다"라는 근본 원인을 깨달을 수 있었습니다.

최초의 아키텍처(Architecture)는 심플했습니다.

Jira 타임라인 페이지
↓ content script가 동작
fetch("https://docs.google.com/spreadsheets/d/SHEET_ID/export?format=csv&gid=GID")
...

manifest.json에는 이렇게 작성했습니다.

{
"host_permissions": [
"https://*.atlassian.net/*",
...

"host_permissions에 적혀 있으니까 fetch 할 수 있을 것이다"라는 인식이었습니다.

확장 기능을 불러와서 Jira를 열자마자, 곧바로 콘솔에 에러가 발생했습니다.

TypeError: Failed to fetch

조사해 보니, MV3의 content script로부터의 크로스 오리진(Cross-Origin) fetch는, host_permissions에 선언되어 있더라도 CORS 정책의 대상이 된다는 것을 알게 되었습니다. content script는 어디까지나 웹 페이지의 컨텍스트(Context)에서 동작하기 때문에,

docs.google.com이 적절한 CORS 헤더를 반환하지 않는 한 차단됩니다. 대책으로서, background service worker에 fetch를 위임하는 구성으로 변경했습니다.

// content.js (수정 후): fetch를 background SW에 의뢰
const result = await new Promise((resolve) => {
chrome.runtime.sendMessage({ type: 'FETCH_CSV', url: csvUrl }, (res) => {
...
// background.js (신규 추가): background SW에서 fetch를 실행
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'FETCH_CSV') {
...

background service worker라면 호스트 권한 범위 내에서 자유롭게 fetch 할 수 있을 것이다——그렇게 생각했습니다.

background service worker로 이행해도, 동일한 에러가 계속해서 발생했습니다.

[HolidayExt] 스프레드시트 취득 에러: TypeError: Failed to fetch

AI와의 디버깅 세션에서 상세 내용을 파고들자, 2단계의 문제가 보였습니다.

최초의 background SW 구현에서는 credentials: 'include'를 붙이고 있었습니다. 이로 인해 CORS의 프리플라이트 요청(Preflight Request, OPTIONS 메서드)이 발생했고, Google 서버가 Access-Control-Allow-Credentials: true를 반환하지 않기 때문에 차단되고 있었습니다.

Access to fetch at 'https://docs.google.com/...'
from origin 'chrome-extension://...' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
...

credentials: 'include'

를 제거했습니다. 하지만——

credentials

를 제거해도 상황은 변하지 않았습니다. 진짜 원인은 바로 이곳입니다.

프라이빗(Private) 스프레드시트에 접근할 때, Google은 accounts.google.com으로 302 리다이렉트(Redirect)를 수행하여 인증을 요구합니다.

fetch("https://docs.google.com/spreadsheets/...")
→ 302 Redirect → https://accounts.google.com/signin/...

accounts.google.comhost_permissions에 포함되어 있지 않습니다. Chrome은 이 리다이렉트 대상에 대한 접근을 차단합니다.

background.js에서 redirect: 'manual'을 지정하여 리다이렉트 탐지를 시도해 본 결과, status: 0이 반환되어 302임을 확인할 수는 있었지만, 스프레드시트 데이터는 가져올 수 없었습니다.

host_permissionshttps://accounts.google.com/*를 추가하는 방법도 있지만, 애초에 Chrome 확장 기능에서 Google의 인증 플로우(Authentication Flow) 전체를 통과하는 fetch를 수행하는 것은 설계상 무리가 있다고 판단했습니다.

최종적으로 채택한 해결책은 Google Apps Script (GAS)를 웹 앱(Web App)으로 공개하여 프록시(Proxy)로 이용하는 것입니다.

GAS는 Google 서버 위에서 Google 인증이 완료된 상태로 동작하기 때문에, 스프레드시트 접근 시 인증 리다이렉트가 발생하지 않습니다. 웹 앱으로 공개하면 Chrome 확장 기능에서 일반적인 HTTP 요청으로 호출할 수 있습니다.

Chrome 확장 기능 (background SW)
↓ fetch (인증 불필요)
GAS 웹 앱 (Google 서버 상에서 실행)
...
const SPREADSHEET_ID = 'your-spreadsheet-id';
const SHEET_GID = 1234567890; // 숫자 형태의 시트 ID
function doGet(e) {
...

GAS 에디터의 「배포(Deploy)」 → 「새 배포(New deployment)」에서 다음과 같이 설정합니다.

유형: 웹 앱 (Web App) -
실행자: 나 (스프레드시트 접근 권한을 가진 Google 계정) -
액세스 권한이 있는 사용자: 조직 내 모든 사용자 (또는 나만)

보안 주의: 「모든 사용자(익명 포함)」로 설정하면 URL을 아는 누구나 접근할 수 있습니다. 사내 전용 도구라면 「조직 내 모든 사용자」로 충분합니다. 배포 URL이 외부에 유출되지 않도록 관리하십시오.

background.js를 GAS URL로 fetch 하도록 변경합니다.

// background.js (최종판)
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'FETCH_CSV') {
...

manifest.jsonhost_permissionsdocs.google.com에서 script.google.com으로 변경합니다.

"host_permissions": [
"https://*.atlassian.net/*",
"https://script.google.com/*"
...

GAS URL을 팝업 UI에서 설정하고 chrome.storage.local에 저장하는 방식으로 만들면 팀 단위로 공유하기 쉬워집니다 (각자가 자신의 GAS를 배포하고 URL을 입력하는 운영 방식).

GAS 프록시를 경유함으로써 프라이빗 스프레드시트 데이터를 인증 문제 없이 가져올 수 있게 되었고, 모든 공휴일을 주황색으로 칠하는 기능이 정상적으로 동작하게 되었습니다.

Chrome 확장 기능에서 Google 스프레드시트에 직접 접근하려 했을 때의 실패와 해결책을 되돌아봅니다.

【실패 ① content script에서 직접 fetch】
content script는 웹 페이지 컨텍스트(Context)
→ host_permissions가 있어도 CORS 장벽에 의해 차단
...

GAS 프록시 (GAS Proxy) 패턴은 이번 스프레드시트 사례 외에도 Google Drive, Gmail, Calendar 등 Google Workspace 서비스로의 액세스가 필요한 Chrome 확장 기능 전반에 응용할 수 있습니다.

"host_permissions에 작성했는데도 fetch가 통과되지 않는다"는 문제로 막혔을 때는, 해당 도메인이 인증 리다이렉트 (Authentication Redirect)를 포함하고 있는지 확인해 보시기 바랍니다. 리다이렉트 대상이 host_permissions 범위를 벗어나는 순간, Chrome은 예외 없이 차단합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0