> ## Documentation Index
> Fetch the complete documentation index at: https://help.airbridge.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 구글 앱스 스크립트로 쉐어링크 데이터 가져오기

구글 앱스 스크립트(Google Apps Script)를 사용하면 에어브릿지 쉐어링크 데이터를 구글 스프레드시트의 원하는 시트와 셀에 가져올 수 있습니다. 예시 스크립트를 붙여넣고 쉐어링크 URL만 입력하면 매일 지정한 시간대에 자동으로 실행할 수 있습니다. 이 가이드에서는 예시 스크립트를 추가하고 처음 실행하는 방법, 자동 갱신을 설정하는 방법, 설정을 변경하는 방법을 안내합니다.

## 시작하기 전에

구글 앱스 스크립트로 쉐어링크 데이터를 가져오려면 아래 2가지가 필요합니다.

* 에어브릿지 쉐어링크가 있어야 합니다. 쉐어링크를 만드는 방법은 [가이드](https://help.airbridge.io/ko/guides/share-airbridge-reports.md)를 참고하세요.
* 데이터를 가져올 구글 스프레드시트의 편집 권한이 있어야 합니다.

## 예시 스크립트 추가하기

구글 스프레드시트의 \[확장 프로그램]>\[Apps Script]에서 예시 스크립트를 붙여넣고 쉐어링크를 입력하세요.

1. 구글 스프레드시트 상단의 \[확장 프로그램]>\[Apps Script]를 선택하세요. 새 탭에 Apps Script 편집기가 열립니다. \[Apps Script] 아래에 표시되는 부가기능 목록은 계정마다 다릅니다.

![google-apps-script-sharelink-01-menu-extensions-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-01-menu-extensions-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=0529c2330449d465359e6fc2b65c7814)

2. Apps Script 편집기에 있는 `function myFunction()` 코드를 모두 지우고, [예시 스크립트](#예시-스크립트)의 코드를 복사해 붙여넣으세요.

![google-apps-script-sharelink-03-editor-pasted-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-03-editor-pasted-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=156d9b36ba425978626c64c02acb99c1)

3. `TARGETS`의 `url` 따옴표 안에 쉐어링크를 붙여넣고, `sheet`에 데이터를 넣을 시트 이름을 입력하세요. 작은따옴표(`' '`)와 줄 끝의 쉼표(`,`)는 지우지 않아야 합니다.

![google-apps-script-sharelink-04-targets-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-04-targets-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=29c319c3f1a65ddeb7053d2511464160)

4. 저장 아이콘을 클릭하거나 맥에서는 Cmd + S, 윈도우에서는 Ctrl + S를 눌러 저장하세요. 편집기 상단 문구가 \[저장되지 않은 변경사항]에서 \[Drive에 저장됨]으로 바뀌면 저장된 것입니다.

![google-apps-script-sharelink-05-save-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-05-save-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=a8baedc8d138a3d36f9ecaed73f687d5)

5. 구글 스프레드시트 탭으로 돌아가 브라우저를 새로고침하세요. 새로고침하고 몇 초 뒤 \[도움말] 오른쪽에 \[쉐어링크]가 나타납니다. Apps Script 편집기 탭은 닫아도 됩니다.

![google-apps-script-sharelink-06-sheet-menu-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-06-sheet-menu-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=1cb645e606fde25602b287c56f08e001)

## 권한 승인하고 처음 실행하기

구글 앱스 스크립트를 처음 실행하면 구글 계정 권한을 승인해야 합니다. 권한 승인은 처음 1번만 하면 됩니다.

> [!NOTE]
>
> **알립니다**
>
> 개인 구글 계정으로 구글 앱스 스크립트 권한을 승인하면 구글 권한 화면보다 먼저 \[Google에서 확인하지 않은 앱] 경고가 표시될 수 있습니다. 이 경고는 직접 붙여넣은 스크립트가 구글의 검증 절차를 거치지 않았다는 뜻입니다. '고급'을 클릭한 다음 아래에 표시되는 이동 링크를 클릭하면 구글 권한 화면으로 넘어갑니다.

1. 구글 스프레드시트에서 \[쉐어링크]>\[지금 바로 최신화]를 선택하세요.

![google-apps-script-sharelink-07-menu-open-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-07-menu-open-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=4ad04265289a2606f77c936b3a9fd0b7)

2. \[승인 필요]에서 '확인'을 클릭하세요.

![google-apps-script-sharelink-08a-auth-required-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-08a-auth-required-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=d80d1b6d3304472e0979aac32e1cc9b4)

3. 새 창으로 열린 구글 권한 화면에서 '모두 선택'을 체크한 다음 '계속'을 클릭하세요.

![google-apps-script-sharelink-08e-allow-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-08e-allow-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=5f80efaa30c3e0f8523da7597a27cf4f)

구글 권한 화면에 표시되는 권한 3개와 각 권한이 필요한 이유는 아래와 같습니다.

| 화면에 나오는 권한 | 필요한 이유 |
| - | - |
| 모든 Google Sheets 스프레드시트 확인, 수정, 생성, 삭제. | 가져온 데이터를 구글 스프레드시트에 쓰는 데 필요합니다. |
| 외부 서비스에 연결. | 쉐어링크에서 데이터를 받아오는 데 필요합니다. |
| 부재중에 이 애플리케이션을 실행하도록 허용. | 매일 지정한 시간대에 자동으로 실행하는 데 필요합니다. |

4. 구글 권한 화면이 닫힐 때까지 기다리세요. 권한 화면이 닫히면 데이터 갱신이 이어서 시작되므로 \[지금 바로 최신화]를 다시 선택하지 않아도 됩니다.

![google-apps-script-sharelink-08f-after-auth-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-08f-after-auth-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=dd07525de0241257230a5d33a9da3ae4)

5. 화면 오른쪽 하단에 '1개 중 1개 완료' 알림이 표시되면 지정한 시트에 데이터가 들어왔는지 확인하세요.

![google-apps-script-sharelink-09-result-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-09-result-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=aea88da1f974a10ab3b2bd25c6edfb0c)

## 매일 자동 갱신 설정하기

구글 앱스 스크립트로 매일 지정한 시간대에 쉐어링크 데이터를 자동으로 갱신하려면 \[쉐어링크]에서 자동 갱신을 켜세요. 예시 스크립트의 기본 시각은 오전 9시이며, 시각을 바꾸는 방법은 [예시 스크립트 설정 변경하기](#예시-스크립트-설정-변경하기)를 참고하세요.

1. 구글 스프레드시트에서 \[쉐어링크]>\[매일 9시 자동 최신화 켜기]를 선택하세요. 화면 오른쪽 하단에 '매일 9시경 자동으로 최신화합니다.' 알림이 표시되면 자동 갱신이 켜진 것입니다.

![google-apps-script-sharelink-10-menu-daily-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-10-menu-daily-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=d4f3429ee09bc1f0ad93d2138da007be)

2. 자동 갱신이 등록됐는지 확인하려면 Apps Script 편집기 왼쪽의 \[트리거]를 선택하세요. 목록에 `refreshNow` 1줄이 표시되면 등록된 것입니다.

![google-apps-script-sharelink-11-triggers-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-11-triggers-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=61be978170c4022e06ad11cbeeaccf26)

3. 데이터가 시작되는 셀에 마우스를 대고 '마지막으로 가져온 시각' 메모를 확인하세요. 자동 갱신이 실행될 때마다 메모의 시각이 바뀝니다.

![google-apps-script-sharelink-12-note-ko](https://mintcdn.com/airbridge-help-center/RIEdrreiY4VRiKUc/asset/image/google-apps-script-sharelink-12-note-ko.png?fit=max\&auto=format\&n=RIEdrreiY4VRiKUc\&q=85\&s=9d363ab2f4b5d9350ad6b9acc750a811)

자동 갱신은 오전 9시 정각이 아니라 오전 9시부터 오전 10시 사이의 한 시점에 실행되며, 매일 같은 시점에 실행됩니다. 스프레드시트를 닫아 두거나 컴퓨터를 꺼도 구글 서버에서 실행됩니다.

자동 갱신을 끄려면 \[쉐어링크]>\[자동 최신화 끄기]를 선택하세요. 화면 오른쪽 하단에 '자동 최신화를 껐습니다.' 알림이 표시됩니다.

## 예시 스크립트 설정 변경하기

구글 앱스 스크립트의 설정은 예시 스크립트 맨 위 `[설정]` 부분에서 바꿉니다. 값을 수정한 다음 스크립트를 저장하고 구글 스프레드시트를 새로고침하세요.

바꾸려는 설정별로 수정할 곳은 아래와 같습니다.

| 하고 싶은 것 | 수정할 곳 |
| - | - |
| 쉐어링크 추가 | `TARGETS`에 1줄을 추가하세요. |
| 데이터를 넣을 시트와 시작 셀 변경 | 해당 줄의 `sheet`와 `cell`을 수정하세요. |
| 자동 갱신 시각 변경 | `DAILY_HOUR`를 0부터 23 사이 숫자(24시간제)로 수정한 다음 \[쉐어링크]>\[매일 N시 자동 최신화 켜기]를 다시 선택하세요. N은 `DAILY_HOUR` 값입니다. |
| 실행할 때마다 새로 조회한 데이터 받기 | 해당 줄의 `url` 끝에 `useCache=false`를 추가하세요. `/v3/` 쉐어링크에서만 작동합니다. |
| 메뉴 이름과 이모지 변경 | `MENU`를 수정하세요. |

자동 갱신 시각은 Apps Script 편집기 왼쪽 \[프로젝트 설정]의 \[시간대]를 기준으로 합니다.

### 쉐어링크 여러 개 가져오기

구글 앱스 스크립트로 쉐어링크 여러 개를 가져오려면 `TARGETS`에 쉐어링크를 1줄에 1개씩 입력하세요. 아래는 쉐어링크 3개를 가져오는 예시입니다.

```javascript theme={"theme":{"light":"github-dark-dimmed","dark":"github-dark-dimmed"}}
const TARGETS = [
  { sheet: '원본_액츄얼스', url: 'https://report.airbridge.io/v3/토큰.csv?language=ko' },
  { sheet: '원본_리텐션', url: 'https://report.airbridge.io/v3/토큰.csv?language=ko' },
  { sheet: '대시보드', cell: 'H3', url: 'https://report.airbridge.io/v3/토큰.csv?language=ko' },
];
```

* `sheet`에 이미 있는 시트 이름을 입력하면 그 시트에 데이터를 넣고, 없는 이름을 입력하면 새 시트를 만듭니다.
* `cell`은 데이터가 시작되는 셀입니다. `cell`을 생략하면 A1 셀부터 데이터를 넣습니다.
* `importdata` 함수를 쓰던 시트에서는 함수를 먼저 지운 다음, 함수가 있던 셀을 `cell`에 입력하세요.
* 한 시트에 쉐어링크 2개를 넣을 때는 두 번째 쉐어링크의 `cell`을 첫 번째 쉐어링크의 데이터가 끝나는 행이나 열보다 뒤로 지정하세요.

### 실행할 때마다 새로 조회한 데이터 받기

3시간 이내에 에어브릿지 대시보드나 쉐어링크로 같은 리포트를 조회한 적이 있으면, 쉐어링크는 리포트를 새로 조회하지 않고 이미 조회한 리포트 데이터를 제공합니다. 조회한 적이 없으면 쉐어링크가 리포트를 새로 조회하고, 예시 스크립트는 조회가 끝날 때까지 기다렸다가 새로 조회한 리포트 데이터를 가져옵니다.

구글 앱스 스크립트를 실행할 때마다 새로 조회한 데이터를 받으려면 `TARGETS`의 `url`에 입력한 쉐어링크 URL 끝에 `useCache=false`를 추가하세요. URL에 물음표(`?`)가 있으면 `&useCache=false`, 없으면 `?useCache=false`를 붙이세요. `useCache=false`는 `/v3/` 쉐어링크에서만 작동합니다.

```javascript theme={"theme":{"light":"github-dark-dimmed","dark":"github-dark-dimmed"}}
{ sheet: '원본_새조회', url: 'https://report.airbridge.io/v3/토큰.csv?language=ko&useCache=false' },
```

`useCache=false`를 추가하면 실행 시간이 길어질 수 있습니다. 조회가 약 1분 안에 끝나지 않으면 예시 스크립트는 `useCache=false`를 뺀 URL로 다시 요청합니다. 그사이 조회가 끝났으면 새로 조회한 리포트 데이터를, 끝나지 않았으면 3시간 이내에 조회한 리포트 데이터를 가져옵니다. 자세한 내용은 [주의사항](#주의사항)을 참고하세요.

### 기존 스크립트의 URL을 `/v3/`로 바꾸기

기존에 사용하던 구글 앱스 스크립트에서 URL의 `/v2/`, `/funnel/`, `/retention/`, `/revenue/`를 `/v3/`로 바꿀 때는 URL 끝에 반드시 `useCache=false`를 추가하세요. `/v3/` URL은 3시간 이내에 조회한 리포트 데이터가 없으면 리포트 조회를 시작하고 데이터 대신 안내 문구를 바로 제공합니다. `useCache=false`를 추가하면 쉐어링크가 조회가 끝날 때까지 기다렸다가 리포트 데이터를 제공합니다. 에어브릿지가 제공하는 예시 스크립트는 `useCache=false`가 없는 `/v3/` 쉐어링크도 처리하므로 그대로 사용할 수 있습니다.

## 예시 스크립트

아래는 Apps Script 편집기에 붙여넣을 예시 스크립트 전문입니다. `[설정]` 부분만 수정하면 되며, 그 아래 코드는 수정하지 않아도 작동합니다.

> [accordion] 예시 스크립트
>
> ```javascript theme={"theme":{"light":"github-dark-dimmed","dark":"github-dark-dimmed"}}
> /* ==========================================================================
>  * 에어브릿지 쉐어링크 → 구글 시트 가져오기
>  *
>  * [설정] 부분만 고치면 됩니다. 그 아래는 고치지 않아도 동작합니다.
>  * 고친 뒤에는 저장(Cmd+S 또는 Ctrl+S)하고 시트를 새로고침하세요.
>  *
>  * 자주 하는 수정
>  *   링크를 추가하고 싶다            → TARGETS 에 한 줄 추가
>  *   데이터가 들어갈 시트·위치 변경   → 그 줄의 sheet, cell 수정
>  *   자동 최신화 시각 변경            → DAILY_HOUR 수정 후 메뉴에서 자동 최신화를 다시 켜기
>  *   메뉴 이름·이모지 변경            → MENU 수정
>  *   "Server is still calculating" 실패가 잦다 → WARMUP_WAIT_MS 를 늘리기
>  *   실행할 때마다 새로 조회하고 싶다   → /v3/ 주소에 useCache=false 추가 (아래 url 설명 참고)
>  * ========================================================================== */
>
> // ------------------------------------------------------------------ [설정]
>
> // 가져올 링크 목록. 링크 하나가 한 줄이다.
> //
> //   sheet : 데이터를 넣을 시트(탭) 이름. 이미 있는 시트 이름을 적으면 그 시트에 넣고,
> //           없는 이름을 적으면 새 시트를 만든다.
> //   cell  : 데이터가 시작될 셀. 이 셀부터 오른쪽·아래로 채워진다. 생략하면 A1.
> //           IMPORTDATA 수식을 쓰던 시트라면, 그 수식이 있던 셀을 적으면 된다.
> //           (수식은 먼저 지워야 한다. 셀에 내용이 있으면 덮어쓰지 않고 실패로 알려준다.)
> //   url   : 에어브릿지에서 복사한 쉐어링크 주소. /v2/ 든 /v3/ 든 그대로 붙여넣는다.
> //           useCache=false를 붙이면 실행할 때마다 새로 조회한 데이터를 받는다(/v3/ 주소만).
> //           주소에 ?가 있으면 &useCache=false, 없으면 ?useCache=false를 끝에 붙인다.
> //           새 조회가 끝날 때까지 기다리므로 실행이 그만큼 오래 걸리고,
> //           이런 링크가 여러 개면 하나씩 차례로 기다린다.
> //
> // 예시
> //   { sheet: '원본_액츄얼스', url: 'https://report.airbridge.io/v3/토큰.csv?language=ko' },
> //   { sheet: '원본_리텐션',   url: 'https://report.airbridge.io/v3/토큰.csv?language=ko' },
> //   { sheet: '대시보드', cell: 'H3', url: 'https://report.airbridge.io/v3/토큰.csv?language=ko' },
> //   { sheet: '원본_새조회',   url: 'https://report.airbridge.io/v3/토큰.csv?language=ko&useCache=false' },
> //
> // 줄 끝의 쉼표(,)와 따옴표(')를 빠뜨리면 전체가 동작하지 않으니 예시 모양을 그대로 따른다.
> // 한 시트에 링크 두 개를 넣을 때는 서로 겹치지 않게 cell 을 충분히 띄운다.
> const TARGETS = [
>   { sheet: '액츄얼스', cell: 'A1', url: '여기에_쉐어링크_주소를_붙여넣으세요' },
> ];
>
> // 매일 자동 최신화가 도는 시각. 0~23 사이 숫자(24시간제)로 적는다. 9 = 오전 9시, 15 = 오후 3시.
> // 구글이 그 시각부터 1시간 안의 한 시점을 골라 매일 같은 때에 실행한다. 분 단위 지정은 안 된다.
> // 기준 시간대는 Apps Script 편집기 왼쪽 톱니바퀴(프로젝트 설정)의 '시간대'를 따른다.
> const DAILY_HOUR = 9;
>
> // 시트 상단에 보이는 메뉴 이름. 따옴표 안의 글자와 이모지는 자유롭게 바꿔도 된다.
> const MENU = {
>   title: '📊 쉐어링크',
>   refresh: '🔄 지금 바로 최신화',
>   dailyOn: '⏰ 매일 ' + DAILY_HOUR + '시 자동 최신화 켜기',
>   dailyOff: '⏹️ 자동 최신화 끄기',
> };
>
> // 서버에 계산을 시작시킨 뒤 결과를 받으러 가기까지 기다리는 시간(1000 = 1초).
> // 리포트가 무거워 "Server is still calculating" 실패가 잦으면 40000 정도로 늘린다.
> const WARMUP_WAIT_MS = 25000;
>
> // 그래도 준비가 안 됐을 때 다시 물어보는 간격과 횟수. 보통은 고칠 일이 없다.
> const RETRY_WAIT_MS = 15000;
> const MAX_ROUNDS = 4;
>
> // ------------------------------------------------- 여기부터는 고치지 않아도 된다
>
> const STATE_HEADER = 'x-airbridge-sharelink-state';
> const SIZE_KEY_PREFIX = 'written-size:';
>
> /** 시트를 열 때 상단에 메뉴를 만든다. */
> function onOpen() {
>   SpreadsheetApp.getUi()
>     .createMenu(MENU.title)
>     .addItem(MENU.refresh, 'refreshNow')
>     .addSeparator()
>     .addItem(MENU.dailyOn, 'autoDaily')
>     .addItem(MENU.dailyOff, 'autoOff')
>     .addToUi();
> }
>
> /** 메뉴와 매일 트리거가 함께 쓰는 진입점. */
> function refreshNow() {
>   const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
>   spreadsheet.toast(TARGETS.length + '개를 최신화하는 중입니다. 1분 안팎 걸립니다.', MENU.title, 120);
>
>   // 일반 v3는 캐시가 없으면 첫 호출에서 계산만 시작하므로, 먼저 한 번씩 불러 예열한다.
>   // useCache=false 요청은 그 호출 자체가 새 조회를 시작하고 결과까지 기다리므로 예열하면 조회가 두 번 돈다.
>   const warmupTargets = TARGETS.filter(function (target) {
>     return isV3_(target) && !skipsCache_(target);
>   });
>   warmupTargets.forEach(function (target) {
>     fetchOnce_(target.url);
>   });
>   if (warmupTargets.length > 0) Utilities.sleep(WARMUP_WAIT_MS);
>
>   var pending = TARGETS.slice();
>   const failed = [];
>   var done = 0;
>
>   for (var round = 1; round <= MAX_ROUNDS && pending.length > 0; round++) {
>     const stillWaiting = [];
>
>     pending.forEach(function (target) {
>       const skipCache = skipsCache_(target);
>       // useCache=false를 다시 보내면 새 조회를 또 시작하므로, 기다리게 되면 다음부터 일반 요청으로 확인한다.
>       const requestUrl = skipCache && round > 1 ? withoutUseCache_(target.url) : target.url;
>       const result = fetchOnce_(requestUrl, skipCache && round === 1);
>       if (result.waiting) {
>         stillWaiting.push(target);
>         return;
>       }
>       if (!result.rows) {
>         failed.push(labelOf_(target) + ' - ' + result.message);
>         return;
>       }
>       try {
>         write_(target, result.rows);
>         done++;
>       } catch (e) {
>         failed.push(labelOf_(target) + ' - ' + e.message);
>       }
>     });
>
>     pending = stillWaiting;
>     if (pending.length > 0 && round < MAX_ROUNDS) Utilities.sleep(RETRY_WAIT_MS);
>   }
>
>   pending.forEach(function (target) {
>     failed.push(labelOf_(target) + ' - Server is still calculating');
>   });
>
>   const summary = TARGETS.length + '개 중 ' + done + '개 완료';
>   spreadsheet.toast(failed.length === 0 ? summary : summary + ' / 실패: ' + failed.join(', '), MENU.title, failed.length === 0 ? 5 : 30);
> }
>
> function isV3_(target) {
>   return /\/v3\//i.test(target.url);
> }
>
> /** v3 주소의 쿼리에 useCache=false가 있는지 확인한다. 값의 대소문자는 무시한다. */
> function skipsCache_(target) {
>   if (!isV3_(target)) return false;
>   const queryStart = target.url.indexOf('?');
>   if (queryStart < 0) return false;
>   const query = target.url.slice(queryStart + 1).split('#')[0];
>   return /(?:^|&)useCache=[Ff][Aa][Ll][Ss][Ee](?:&|$)/.test(query);
> }
>
> /** 다른 쿼리 파라미터와 fragment는 그대로 두고 useCache 파라미터만 뺀다. */
> function withoutUseCache_(url) {
>   const hashStart = url.indexOf('#');
>   const hash = hashStart < 0 ? '' : url.slice(hashStart);
>   const beforeHash = hashStart < 0 ? url : url.slice(0, hashStart);
>   const queryStart = beforeHash.indexOf('?');
>   if (queryStart < 0) return url;
>
>   const base = beforeHash.slice(0, queryStart);
>   const params = beforeHash.slice(queryStart + 1).split('&').filter(function (param) {
>     return param.split('=')[0].toLowerCase() !== 'usecache';
>   });
>   return base + (params.length > 0 ? '?' + params.join('&') : '') + hash;
> }
>
> function labelOf_(target) {
>   return target.sheet + '!' + (target.cell || 'A1');
> }
>
> /**
>  * v3는 상태 헤더로 판정한다. 헤더가 없는 v2·리포트별 주소는 본문 모양으로 판정한다.
>  * 정상 리포트는 여러 열이고, 안내 문구는 한 셀짜리로 온다.
>  */
> function fetchOnce_(url, waitOnError) {
>   var response;
>   // muteHttpExceptions: 4xx·5xx도 예외가 아니라 응답으로 받아 아래에서 상태 코드로 판정한다.
>   // 예외(catch)는 응답 자체가 안 온 경우(연결 끊김·타임아웃)뿐이다.
>   try {
>     response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
>   } catch (e) {
>     // useCache=false는 클라이언트가 먼저 포기해도 서버 조회가 계속되므로 일반 요청으로 재확인한다.
>     if (waitOnError) return { waiting: true };
>     return { message: 'No response' };
>   }
>
>   // 판정 순서. in-progress만 다음 라운드(RETRY_WAIT_MS 간격, 최대 MAX_ROUNDS회)에 다시 확인하고,
>   // 4xx·5xx는 재시도 없이 바로 실패로 알린다.
>   const state = readState_(response);
>   if (state === 'in-progress') return { waiting: true };
>   if (response.getResponseCode() === 404) return { message: 'Link not found' };
>   if (response.getResponseCode() >= 400) return { message: 'http ' + response.getResponseCode() };
>
>   const text = response.getContentText();
>   if (state && state !== 'ok') return { message: text.slice(0, 120) };
>
>   const rows = Utilities.parseCsv(text);
>   const isNotice = rows.length <= 1 && (rows.length === 0 || rows[0].length <= 1);
>   if (isNotice) return { message: text.slice(0, 120) };
>
>   return { rows: rows };
> }
>
> function readState_(response) {
>   const headers = response.getAllHeaders();
>   for (var key in headers) {
>     if (key.toLowerCase() === STATE_HEADER) return String(headers[key]);
>   }
>   return '';
> }
>
> /**
>  * 시트 전체가 아니라 지난번에 이 스크립트가 쓴 영역만 지우고 새로 쓴다.
>  * 새로 쓸 자리에 스크립트가 쓰지 않은 내용이 있으면 덮어쓰지 않고 멈춘다.
>  */
> function write_(target, rows) {
>   const width = rows.reduce(function (max, row) {
>     return Math.max(max, row.length);
>   }, 0);
>
>   // setValues는 모든 행의 길이가 같아야 한다.
>   const padded = rows.map(function (row) {
>     const copy = row.slice();
>     while (copy.length < width) copy.push('');
>     return copy;
>   });
>
>   const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
>   const sheet = spreadsheet.getSheetByName(target.sheet) || spreadsheet.insertSheet(target.sheet);
>   const start = sheet.getRange(target.cell || 'A1');
>   const top = start.getRow();
>   const left = start.getColumn();
>
>   const properties = PropertiesService.getDocumentProperties();
>   const sizeKey = SIZE_KEY_PREFIX + labelOf_(target);
>   const previous = (properties.getProperty(sizeKey) || '0,0').split(',').map(Number);
>
>   const blocker = findBlocker_(sheet, top, left, padded.length, width, previous[0], previous[1]);
>   if (blocker) throw new Error(blocker + ' has other content; not overwritten');
>
>   if (previous[0] > 0) sheet.getRange(top, left, previous[0], previous[1]).clearContent();
>   sheet.getRange(top, left, padded.length, width).setValues(padded);
>   properties.setProperty(sizeKey, padded.length + ',' + width);
>
>   const stamp = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm');
>   start.setNote('마지막으로 가져온 시각: ' + stamp);
> }
>
> /** 새로 쓸 영역 중 지난번에 쓴 영역 바깥에서, 내용이 있는 첫 셀의 주소를 돌려준다. 없으면 빈 문자열. */
> function findBlocker_(sheet, top, left, height, width, previousHeight, previousWidth) {
>   const values = sheet.getRange(top, left, height, width).getValues();
>   for (var r = 0; r < height; r++) {
>     for (var c = 0; c < width; c++) {
>       const wasOurs = r < previousHeight && c < previousWidth;
>       if (!wasOurs && values[r][c] !== '') return columnLetter_(left + c) + (top + r);
>     }
>   }
>   return '';
> }
>
> function columnLetter_(column) {
>   var letters = '';
>   for (var n = column; n > 0; n = Math.floor((n - 1) / 26)) {
>     letters = String.fromCharCode(65 + ((n - 1) % 26)) + letters;
>   }
>   return letters;
> }
>
> function autoDaily() {
>   clearTriggers_();
>   ScriptApp.newTrigger('refreshNow').timeBased().everyDays(1).atHour(DAILY_HOUR).create();
>   SpreadsheetApp.getActiveSpreadsheet().toast('매일 ' + DAILY_HOUR + '시경 자동으로 최신화합니다.', MENU.title, 5);
> }
>
> function autoOff() {
>   clearTriggers_();
>   SpreadsheetApp.getActiveSpreadsheet().toast('자동 최신화를 껐습니다.', MENU.title, 5);
> }
>
> /** 옛 버전이 남긴 트리거까지 함께 정리한다. */
> function clearTriggers_() {
>   ScriptApp.getProjectTriggers().forEach(function (trigger) {
>     ScriptApp.deleteTrigger(trigger);
>   });
> }
> ```

## 주의사항

구글 앱스 스크립트로 쉐어링크 데이터를 가져올 때 아래 사항을 확인하세요.

> [!CAUTION]
>
> **주의하세요**
>
> 예시 스크립트는 스크립트가 쓰지 않은 내용을 덮어쓰지 않습니다. 데이터가 들어갈 셀에 `importdata` 함수나 직접 입력한 값처럼 다른 내용이 있으면 'B5 has other content; not overwritten'처럼 해당 셀의 주소가 담긴 실패 알림을 표시합니다. 해당 영역을 비우거나 `cell`을 빈 셀로 바꾼 다음 다시 실행하세요.

* 자동 갱신은 \[쉐어링크]>\[매일 N시 자동 최신화 켜기]를 선택한 사용자의 구글 계정으로 실행됩니다. 이 사용자가 구글 스프레드시트 편집 권한을 잃으면 자동 갱신이 멈춥니다.
* 구글 스프레드시트를 복사하면 자동 갱신 설정은 복사되지 않습니다. 복사본에서 자동 갱신을 다시 켜세요.
* 자동 갱신이 실패해도 실패 알림이 표시되지 않습니다. 데이터가 시작되는 셀의 '마지막으로 가져온 시각' 메모가 오늘 날짜인지 확인하세요.
* `useCache=false`를 추가한 쉐어링크는 새 조회가 끝날 때까지 기다리므로 실행이 오래 걸릴 수 있습니다. `useCache=false`를 추가한 쉐어링크가 여러 개면 하나씩 차례로 기다립니다.
* `useCache=false`를 추가한 쉐어링크의 조회가 약 1분 안에 끝나지 않았고 3시간 이내에 조회한 리포트 데이터도 없으면 실패 알림에 'Server is still calculating'이 표시됩니다.

## 구글 앱스 스크립트 문제 해결하기

구글 앱스 스크립트로 쉐어링크 데이터를 가져올 때 발생할 수 있는 문제와 해결 방법은 아래와 같습니다.

| 보이는 문구나 증상 | 원인 | 해결 |
| - | - | - |
| \[쉐어링크]가 나타나지 않음 | 스크립트를 저장하지 않았거나 구글 스프레드시트를 새로고침하지 않았습니다. | 스크립트를 저장한 다음 구글 스프레드시트를 새로고침하고 몇 초 기다리세요. |
| 'Link not found' | 쉐어링크 URL 형식이 잘못됐거나, 쉐어링크를 만든 사용자가 비활성 상태입니다. | 에어브릿지 대시보드에서 쉐어링크를 다시 복사해 `url`에 입력하세요. |
| 'This Sharelink doesn't exist.' | 쉐어링크가 삭제됐거나 `url`에 잘못 입력됐습니다. 쉐어링크는 사용이 중지된 지 90일이 지나면 자동으로 삭제되고, 저장한 리포트를 삭제할 때도 함께 삭제됩니다. | 쉐어링크를 잘못 입력했다면 에어브릿지 대시보드에서 쉐어링크를 다시 복사해 `url`에 입력하세요. 쉐어링크가 삭제됐다면 저장한 리포트에서 쉐어링크를 새로 생성해 `url`에 입력하세요. 저장한 리포트도 삭제됐다면 리포트를 다시 만들어 저장한 다음 쉐어링크를 생성하세요. |
| 'This Sharelink is paused.' | 쉐어링크 사용이 중지됐습니다. '자동 삭제 끄기'를 선택하지 않은 쉐어링크는 사용을 허용한 지 90일이 지나면 자동으로 중지됩니다. | 에어브릿지 대시보드의 \[규칙 관리]>\[쉐어링크 관리]에서 해당 쉐어링크의 사용을 허용한 다음 다시 실행하세요. 쉐어링크 URL은 바뀌지 않으므로 `url`은 그대로 두세요. 자동 갱신에 쓰는 쉐어링크라면 '자동 삭제 끄기'도 선택하세요. |
| 'http 400' | 쉐어링크 URL에 추가한 파라미터 값이 잘못됐습니다. 날짜 형식이 틀렸거나, 시작일이 종료일보다 늦거나, 조회 기간이 허용 범위를 넘은 경우가 해당합니다. | `url`에 추가한 파라미터 값을 바로잡은 다음 다시 실행하세요. 파라미터 형식은 리포트별 쉐어링크 [가이드](https://help.airbridge.io/ko/guides/share-airbridge-reports.md)를 참고하세요. 파라미터를 추가하지 않았다면 에어브릿지 대시보드에서 쉐어링크를 다시 복사해 `url`에 입력하세요. |
| 'Server is still calculating' | 리포트 조회가 오래 걸리고 있거나 쉐어링크 요청 횟수가 제한을 넘었습니다. | 잠시 후 다시 실행하세요. `useCache=false`가 없는 쉐어링크에서 같은 알림이 자주 표시되면 `WARMUP_WAIT_MS`를 `40000`으로 늘리세요. 늘린 뒤에도 같은 알림이 표시되면 요청 횟수 제한을 넘었을 수 있습니다. 제한은 시간이 지나면 풀립니다. |
| 'This Sharelink uses an older version.' | 경로가 `/v2/`, `/funnel/`, `/retention/`, `/revenue/`인 기존 URL에서 리포트 조회가 오래 걸리고 있거나 쉐어링크 요청 횟수가 제한을 넘었습니다. | `url`의 `/v2/`, `/funnel/`, `/retention/`, `/revenue/`를 `/v3/`로 바꾼 다음 다시 실행하세요. |
| 'Data has not been imported yet.' | 리포트 조회에 실패했습니다. | 잠시 후 다시 실행하세요. |
| 'No response' | 구글 앱스 스크립트가 에어브릿지 서버의 응답을 받지 못했습니다. | 잠시 후 다시 실행하세요. |
| '… has other content; not overwritten' | 데이터가 들어갈 셀에 스크립트가 쓰지 않은 내용이 있습니다. `importdata` 함수, 직접 입력한 값, 데이터가 있는 시트를 복사해 온 경우가 해당합니다. | 해당 영역을 비우거나 `cell`을 빈 셀로 바꾼 다음 다시 실행하세요. |
| 데이터가 이전 실행과 같음 | 3시간 이내에 에어브릿지 대시보드나 쉐어링크로 같은 리포트를 조회한 적이 있으면, 쉐어링크는 리포트를 새로 조회하지 않고 이미 조회한 리포트 데이터를 제공합니다. | 정상 작동입니다. 실행할 때마다 새로 조회한 데이터를 받으려면 `/v3/` 쉐어링크 URL 끝에 `useCache=false`를 추가하세요. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.