튜토리얼스프레드시트

Google Sheets에서 실시간 금 시세 가져오기

API 키를 저장하지 않고 Google Sheets로 실시간 및 과거 금 시세를 가져오세요. 참조 시세와 비교하고 4시간마다 한도 안에서 갱신할 수 있습니다.

2026년 9월 8일 · 읽는 시간 9분

Apps Script를 추가하면 어느 셀에서든 =GOLDPRICE()로 goldprice.dev의 트로이온스당 실시간 XAU/USD 시세를 가져올 수 있습니다. 무료 시세는 15분 지연됩니다. IMPORTDATA는 이 JSON 엔드포인트를 읽지 못하므로 사용자 지정 함수가 필요합니다. GOLDHISTORY()는 30일간의 일별 종가를, GOLDPRICEDELTA()는 통화와 단위를 맞춘 참조 셀 대비 가격 차이와 변동률을 부호와 함께 반환합니다. API 키는 저장하지 않습니다. installGoldRefresh()를 한 번 실행하면 갱신 셀을 연결하고 요청 한도에 맞는 네 시간 간격 트리거를 만듭니다.

시트에 API 키를 넣지 않고 시작하기

XAU 현물 엔드포인트와 30일간의 일별 과거 데이터는 모두 익명으로 이용할 수 있으므로 Apps Script에 인증 정보를 저장하지 않습니다. 익명 접근에는 IP당 분당 10회, 시간당 100회, 월 5,000회의 Free 한도가 적용됩니다. 네 시간 간격으로 갱신하는 통합 문서는 이 한도 안에서 사용할 수 있습니다.

Google에서 여러 Sheets 사용자의 요청을 같은 외부 IP로 보낼 수 있어 익명 요청의 여유 한도는 달라질 수 있습니다. 공유하거나 운영에 쓰는 통합 문서에 일정한 계정별 한도가 필요하면 자체 서버 측 프록시를 호출하고 goldprice.dev 키를 그곳에 보관하세요. 다른 편집자가 볼 수 있는 바인딩된 스크립트에 bearer 토큰을 붙여 넣지 마세요. 무료 계정 키에도 월 5,000회가 제공되며, 공유 IP가 아닌 계정 기준으로 집계됩니다.

계정 전용 요청 한도 받기

아래 단계는 키 없이도 진행할 수 있습니다. 무료 API 키를 발급받으면 계정 전용 한도가 제공됩니다. 더 긴 과거 데이터와 높은 한도는 유료 플랜에서 이용할 수 있습니다.

Apps Script에 금 시세 함수 세 개 추가하기

시트를 열고 확장 프로그램 → Apps Script를 클릭하세요. 기본 function myFunction() {}을 삭제하고 아래 코드를 붙여 넣으세요. 현재 현물 시세용 GOLDPRICE(metal, currency, refreshToken), 차트용 일별 종가를 반환하는 GOLDHISTORY(fromDate, toDate, refreshToken), 부호가 있는 가격 차이와 변동률을 반환하는 GOLDPRICEDELTA(referencePrice, metal, currency, refreshToken)를 정의합니다. 동일한 요청은 CacheService로 캐시하고, Authorization 헤더는 보내지 않으며, 예외를 던지는 대신 오류 문자열을 반환합니다.

수량, 매입 비용, 참고 평가액을 한 통합 문서에서 관리하려면 금 포트폴리오 추적 템플릿을 사용하세요.

Code.gs
// google-sheets: Apps Script Code.gsconst SPOT_CACHE_TTL = 55; // Cache successful spot responses for 55 seconds.const BARS_CACHE_TTL = 300; // Cache successful daily bars for 5 minutes.const REFRESH_RANGE_NAME = 'GOLDPRICE_REFRESH_TOKEN';const SPREADSHEET_ID_KEY = 'GOLDPRICE_SPREADSHEET_ID';const INSTALLER_EMAIL_KEY = 'GOLDPRICE_INSTALLER_EMAIL';const LAST_REFRESH_AT_KEY = 'GOLDPRICE_LAST_REFRESH_AT';const MIN_REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;/** * Returns the current spot price for a metal/currency pair. * @customfunction */function GOLDPRICE(metal, currency, refreshToken) {  const symbol = metal + '-' + currency + '-SPOT';  const cache = CacheService.getScriptCache();  const cacheKey = 'spot_' + symbol;  const cached = cache.get(cacheKey);  if (cached) return cached;  const url = `https://api.goldprice.dev/v1/spot/${symbol}`;  const resp = UrlFetchApp.fetch(url, {    muteHttpExceptions: true,  });  const code = resp.getResponseCode();  if (code === 401) return '#ACCESS_ERROR: use a server-side proxy';  if (code === 403) return '#PLAN_GATED: symbol not on your tier';  if (code === 429) return '#RATE_LIMIT: wait a minute and retry';  if (code !== 200) return '#ERROR_' + code;  const data = JSON.parse(resp.getContentText());  if (!data || !data.price) return '#NO_DATA';  // Price stays a string, same as the API wire format: Sheets displays and  // sorts it fine. Wrap in VALUE() if you need it in arithmetic.  cache.put(cacheKey, data.price, SPOT_CACHE_TTL);  return data.price;}/** * Compares live spot with a reference cell in the same currency and unit. * Spills [signed price delta, signed percentage delta] into two cells. * @customfunction */function GOLDPRICEDELTA(referencePrice, metal, currency, refreshToken) {  const reference = Number(referencePrice);  if (!Number.isFinite(reference) || reference <= 0) {    return [['#BAD_REFERENCE: enter a positive price', '']];  }  const liveResult = GOLDPRICE(metal, currency, refreshToken);  const live = Number(liveResult);  if (!Number.isFinite(live)) return [[liveResult, '']];  const signedDelta = live - reference;  return [[Number(signedDelta.toFixed(2)), signedDelta / reference]];}/** * Returns chart-ready daily closes for a date range. * @customfunction */function GOLDHISTORY(fromDate, toDate, refreshToken) {  const now = new Date();  const rollingFrom = new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000);  const from = toIsoDate_(    fromDate === '' || fromDate == null ? rollingFrom : fromDate  );  const to = toIsoDate_(toDate === '' || toDate == null ? now : toDate);  if (!from || !to) return [['#BAD_DATE: put real dates in B2/B3']];  const cache = CacheService.getScriptCache();  const cacheKey = 'bars_' + from + '_' + to;  const cached = cache.get(cacheKey);  if (cached) return JSON.parse(cached);  const url = 'https://api.goldprice.dev/v1/bars?symbol=XAU-USD-SPOT&interval=1d'    + '&from=' + from + '&to=' + to;  const resp = UrlFetchApp.fetch(url, {    muteHttpExceptions: true,  });  const code = resp.getResponseCode();  if (code === 401) return [['#ACCESS_ERROR: use a server-side proxy']];  if (code === 403) return [['#PLAN_GATED: range not on your tier']];  if (code === 429) return [['#RATE_LIMIT: wait a minute and retry']];  if (code !== 200) return [['#ERROR_' + code]];  const data = JSON.parse(resp.getContentText());  const bars = Array.isArray(data.bars) ? data.bars : [];  if (bars.length === 0) return [['#NO_DATA: empty range']];  // /v1/bars returns bars newest-first; reverse to chronological order so  // a line chart built from this range reads left-to-right in time.  const rows = bars.slice().reverse().map(function (bar) {    return [bar.bar_start.slice(0, 10), Number(bar.close)];  });  cache.put(cacheKey, JSON.stringify(rows), BARS_CACHE_TTL);  return rows;}function installGoldRefresh() {  const lock = LockService.getScriptLock();  lock.waitLock(30000);  try {    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();    if (!spreadsheet) throw new Error('Open the bound Google Sheet and run again');    const installerEmail = Session.getEffectiveUser().getEmail();    if (!installerEmail) throw new Error('Run the installer from your Google account');    const properties = PropertiesService.getScriptProperties();    const existingInstaller = properties.getProperty(INSTALLER_EMAIL_KEY);    if (existingInstaller && existingInstaller !== installerEmail) {      throw new Error('Gold refresh is already owned by another editor');    }    const refreshRange = spreadsheet.getActiveSheet().getRange('B1');    spreadsheet.setNamedRange(REFRESH_RANGE_NAME, refreshRange);    // getProjectTriggers() only returns this user's triggers. The shared    // installer-email claim above prevents a second editor from creating a    // second schedule, while this replacement keeps owner reruns idempotent.    ScriptApp.getProjectTriggers()      .filter(function (trigger) {        return trigger.getHandlerFunction() === 'refreshGoldData';      })      .forEach(function (trigger) {        ScriptApp.deleteTrigger(trigger);      });    ScriptApp.newTrigger('refreshGoldData')      .timeBased()      .everyHours(4)      .create();    properties.setProperties({      [SPREADSHEET_ID_KEY]: spreadsheet.getId(),      [INSTALLER_EMAIL_KEY]: installerEmail,    });  } finally {    lock.releaseLock();  }  refreshGoldData();}function uninstallGoldRefresh() {  const lock = LockService.getScriptLock();  lock.waitLock(30000);  try {    const properties = PropertiesService.getScriptProperties();    const installerEmail = Session.getEffectiveUser().getEmail();    const existingInstaller = properties.getProperty(INSTALLER_EMAIL_KEY);    if (existingInstaller && existingInstaller !== installerEmail) {      throw new Error('Only the trigger owner can uninstall gold refresh');    }    ScriptApp.getProjectTriggers()      .filter(function (trigger) {        return trigger.getHandlerFunction() === 'refreshGoldData';      })      .forEach(function (trigger) {        ScriptApp.deleteTrigger(trigger);      });    properties.deleteProperty(SPREADSHEET_ID_KEY);    properties.deleteProperty(INSTALLER_EMAIL_KEY);    properties.deleteProperty(LAST_REFRESH_AT_KEY);  } finally {    lock.releaseLock();  }}function refreshGoldData(event) {  const lock = LockService.getScriptLock();  lock.waitLock(30000);  try {    // Clock triggers pass an event object, but they cannot supply the metal,    // currency, date-range, or reference arguments required by sheet formulas.    // This wrapper only changes the named token cell those formulas reference.    const properties = PropertiesService.getScriptProperties();    const spreadsheetId = properties.getProperty(SPREADSHEET_ID_KEY);    if (!spreadsheetId) throw new Error('Run installGoldRefresh first');    // Script properties and the script lock are shared across editors. This    // gate coalesces legacy triggers owned by different users into one refresh    // per four hours even though Apps Script cannot enumerate their triggers.    const now = Date.now();    const lastRefreshAt = Number(properties.getProperty(LAST_REFRESH_AT_KEY));    if (lastRefreshAt && now - lastRefreshAt < MIN_REFRESH_INTERVAL_MS) return;    const spreadsheet = SpreadsheetApp.openById(spreadsheetId);    const refreshRange = spreadsheet.getRangeByName(REFRESH_RANGE_NAME);    if (!refreshRange) throw new Error('Run installGoldRefresh to restore B1');    refreshRange.setValue(new Date(now).toISOString());    properties.setProperty(LAST_REFRESH_AT_KEY, String(now));  } finally {    lock.releaseLock();  }}function toIsoDate_(value) {  const d = value instanceof Date ? value : new Date(value);  if (isNaN(d.getTime())) return null;  return d.toISOString().slice(0, 10);}

시세, 과거 데이터, 참조 셀 연결하기

B1에 갱신 토큰을 넣고, 필요하면 C1에 트로이온스당 USD 기준 참조 시세를 입력하세요. 최근 30일의 과거 데이터를 가져오려면 B2와 B3을 비워 두고, 기간을 고정하려면 날짜를 입력하세요. GOLDPRICEDELTA()는 실시간 시세에서 참조 시세를 뺀 가격 차이와 변동률을 부호와 함께 반환합니다. 두 셀의 통화와 단위가 같아야 올바르게 비교할 수 있습니다.

Google 안내에 따르면 GOOGLEFINANCE 시세는 최대 20분 지연될 수 있으며, 모든 선물을 지원하지 않고 과거 결과는 Apps Script로 읽을 수 없습니다. 별도 참조 시세를 사용한다면 C1에서 트로이온스당 USD로 맞추세요. goldprice.dev에서는 코드로 읽을 현물 및 과거 데이터를 가져옵니다. 사용자 지정 함수의 인수에 NOW()나 TODAY()를 넣지 마세요. B2/B3이 비어 있으면 GOLDHISTORY() 내부에서 조회 날짜를 계산합니다.

Sheets 수식 · B1/B2/B3/C1 연결
// B1: refreshGoldData()가 기록하는 갱신 토큰// B2/B3: 최근 30일 범위를 사용하려면 비워 두고, 기간을 고정하려면 날짜를 입력하세요// C1: 트로이온스당 USD로 환산한 참조 시세, 선택 사항// 현재 USD 현물가: B1이 바뀔 때마다 다시 계산합니다=GOLDPRICE("XAU", "USD", $B$1)// 실시간 현물가에서 C1을 뺀 결과: [부호가 있는 USD 차이, 부호가 있는 백분율 차이]를 여러 셀에 표시합니다// 두 번째 결과 셀의 표시 형식을 백분율로 설정하세요.=GOLDPRICEDELTA($C$1, "XAU", "USD", $B$1)// 현재 EUR 현물가: 두 수식 모두 같은 토큰 셀로 갱신합니다=GOLDPRICE("XAU", "EUR", $B$1)// 차트에 바로 사용할 수 있는 30일간의 일별 종가 (날짜, 종가)=GOLDHISTORY($B$2, $B$3, $B$1)

과거 데이터로 꺾은선 차트 만들기

GOLDHISTORY() 결과가 펼쳐진 행을 선택한 뒤 삽입 → 차트에서 꺾은선 차트를 선택하세요. 행 수는 요청 기간과 제공되는 일별 데이터에 따라 달라집니다. 함수가 API의 최신순 데이터를 시간순으로 바꾸므로 추가 정렬 없이 가로축이 과거부터 최신순으로 표시됩니다.

4시간 간격 갱신 트리거 추가하기

예약 실행을 관리할 Google 계정을 하나 정하세요. 이전 튜토리얼을 따라 한 편집자는 각자 Apps Script → 트리거에서 자신의 refreshGoldData 항목을 먼저 삭제해야 합니다. 그런 다음 installGoldRefresh를 한 번 실행하고 권한을 허용하세요. 설치 함수가 B1을 연결하고 네 시간 간격 트리거를 하나 만듭니다. 공유 잠금과 타임스탬프가 접근할 수 없는 기존 트리거의 실행도 네 시간당 한 번의 갱신으로 합칩니다.

관리자를 바꿀 때는 기존 관리자가 uninstallGoldRefresh를 실행한 뒤 새 관리자가 설치하세요. 기존 관리자가 작업할 수 없다면 프로젝트 설정 → 스크립트 속성에서 GOLDPRICE_* 항목 세 개를 삭제하고 다시 설치하세요. 공유 간격 검사로 중복 호출은 계속 방지됩니다. 수식 네 개 × 갱신 여섯 번이면 하루 최대 24회, 월 약 720회로, IP당 시간당 100회 및 월 5,000회의 익명 Free 한도 안에 들어갑니다.

예상 결과

JSON
{  "symbol": "XAU",  "quote_currency": "USD",  "unit": "troy_ounce",  "contract_type": "spot",  "price": "4012.75",  "is_stale": false,  "computed_at": "2026-10-23T07:00:18Z"}

GOLDPRICE는 위 price 필드를 텍스트로 표시합니다(예: "4012.75"). GOLDPRICEDELTA는 부호가 있는 가격 차이와 소수 형태의 변동률을 두 셀에 펼칩니다. 두 번째 셀은 백분율로 지정하세요. GOLDHISTORY는 요청 기간에 제공되는 [date, close] 행을 채웁니다.

자주 발생하는 오류

코드증상해결 방법
401셀에 #ACCESS_ERROR: use a server-side proxy가 표시됩니다익명 요청이 허용되지 않았습니다. 먼저 Apps Script → 실행에서 확인하세요. 통합 문서에 인증된 요청 한도가 필요하면 서버 측 프록시를 통해 요청하세요. 바인딩된 스크립트에 키를 붙여 넣지 마세요.
403셀에 #PLAN_GATED: symbol not on your tier가 표시됩니다Free는 금(XAU), 은(XAG), 구리(HG) 현물을 지원합니다. 심볼을 확인하세요. 백금과 팔라듐은 제공하지 않습니다. /pricing을 확인하세요.
429셀에 #RATE_LIMIT: wait a minute and retry가 표시됩니다공유 IP가 분당 10회, 시간당 100회 또는 월 5,000회의 Free 한도에 도달했습니다. 보통은 캐시로 방지됩니다. TTL 상수를 늘리거나, 서로 다른 수식 수를 줄이거나, 서버 측 프록시를 통해 인증된 요청을 보내세요.
N/AGOLDHISTORY에 #BAD_DATE: put real dates in B2/B3가 표시됩니다B2 또는 B3에 값 대신 라벨처럼 Apps Script가 날짜로 해석하지 못하는 텍스트가 있습니다. 실제 날짜를 입력하거나 서식 → 숫자 → 날짜로 지정한 셀을 참조하세요.
N/AGOLDPRICEDELTA에 #BAD_REFERENCE: enter a positive price가 표시됩니다C1이 비어 있거나 텍스트, 영 또는 음수입니다. 실시간 현물 수식과 같은 통화 및 트로이온스당 단위로 양수인 참조 시세를 입력하세요.
N/A셀에 Loading…이 계속 표시됩니다Apps Script에 외부 데이터 요청 권한이 필요합니다. 편집기 도구 모음에서 함수 이름을 선택하고 수동으로 한 번 실행한 뒤 권한 요청을 허용하세요.

더 활용하기

  • 시세 알림 수식 만들기: =IF(GOLDPRICE("XAU","USD",$B$1) > 4800, "ALERT", "OK")
  • 더 오래된 일별 데이터는 키를 보관한 서버 측 프록시로 가져오세요. Basic은 1년, Plus와 Pro는 전체 과거 데이터를 지원합니다
  • Free에서 금과 은을 함께 추적하고 같은 갱신 토큰 셀을 사용하세요
  • 차트용 GOLDHISTORY 행으로 백테스트하세요. OHLC 데이터로 금 전략 백테스트하기를 확인하세요
  • 평가액 또는 비용 모델에서 =GOLDPRICE(...)를 참조해 실시간 데이터로 값을 갱신하세요

자주 묻는 질문

시세는 얼마나 자주 갱신되나요?

현물 엔드포인트는 실시간 현물 데이터를 60초마다 갱신합니다. Free 및 키 없는 시세는 15분 지연되며 유료 시세는 실시간입니다. GOLDPRICE의 캐시도 이 주기에 맞춰져 있어 TTL 안에 반복 호출하면 API를 새로 호출하지 않고 같은 값을 반환합니다.

4시간 간격 갱신은 무료 한도 안에 들어가나요?

네. $B$1 갱신 셀에 GOLDPRICE 두 개, GOLDPRICEDELTA 하나, GOLDHISTORY 하나가 연결됩니다. 4시간 트리거는 하루 6번 × 4회 = 하루 최대 24회, 월 약 720회를 호출합니다. GOLDPRICEDELTA가 USD 현물 요청을 재사용하므로 캐시 적중 시 더 줄어듭니다. IP당 분당 10회, 시간당 100회, 월 5,000회의 익명 한도 안에 들어갑니다.

금 시세에 GOOGLEFINANCE를 사용하지 않는 이유는 무엇인가요?

GOOGLEFINANCE는 지원되는 증권과 통화 데이터를 위한 기능이며, 문서화된 XAU/USD 현물 API가 아닙니다. Google 안내에 따르면 시세는 최대 20분 지연될 수 있고, 모든 선물을 지원하지 않으며 과거 데이터는 Apps Script로 읽을 수 없습니다. 현물은 GOLDPRICE(), 스크립트용 과거 데이터는 GOLDHISTORY()로 가져오세요. 별도 참조 시세는 통화와 단위를 맞춘 뒤 GOLDPRICEDELTA()로 비교하고 상품의 차이도 확인하세요.

트리거가 GOLDPRICE 대신 refreshGoldData를 실행하는 이유는 무엇인가요?

Apps Script는 선택한 처리 함수에 시간 이벤트 객체를 전달하지만, 시트 수식에 필요한 금속, 통화, 날짜 범위, 참조 인수는 전달할 수 없습니다. 따라서 installGoldRefresh()는 이름이 지정된 B1 범위에 새 타임스탬프를 쓰는 refreshGoldData(event)용 트리거를 만듭니다. 그러면 수식 네 개가 모두 다시 계산됩니다.

다른 편집자와 시트를 공유해도 되나요?

네. 이 예제는 익명 접근을 사용하고 API 키를 저장하지 않으므로 편집자가 인증 정보를 받지 않고 바인딩된 스크립트를 보거나 복사할 수 있습니다. 나중에 인증된 요청 한도가 필요해지면 bearer 토큰은 서버 측 프록시에 보관하고 시트에서 그 서비스를 호출하세요.

상업적으로 이용해도 되나요?

모든 플랜에서 상업적 이용이 가능합니다. 유료 플랜은 표시 의무가 없으며, Free는 시세 근처에 goldprice.dev로 연결되는 링크를 눈에 보이게 표시해야 합니다.

어떤 금속을 지원하나요?

Free를 포함한 모든 플랜에서 금(XAU), 은(XAG), 구리(HG) 현물을 지원합니다. 백금과 팔라듐은 어떤 플랜에서도 제공하지 않습니다. 자세한 내용은 API 참조 문서를 확인하세요.

무료 플랜은 정말 무료인가요?

네. 익명 XAU 현물과 30일간의 일별 과거 데이터는 키 없이 이용할 수 있습니다. IP당 분당 10회, 시간당 100회, 월 5,000회의 Free 한도가 적용됩니다. 서버 측 연동에서 계정별 한도가 필요하면 무료 계정 키를 발급받으세요. 월 한도는 같으며 카드와 만료 기한이 없습니다.

Claude 같은 AI 도구에 작성을 맡겨도 되나요?

네. Claude, Cursor 또는 ChatGPT에 goldprice.dev MCP 서버나 llms.txt를 연결하고 다음과 같이 요청하세요. api.goldprice.dev를 호출하는 Google Sheets 사용자 지정 함수와, 반복 실행해도 중복 없이 이름이 지정된 갱신 셀을 연결하고 네 시간 간격 트리거를 하나 만드는 설치 함수를 작성하세요. 최신 스키마를 읽으므로 처음부터 올바른 필드명과 오류 코드를 사용해 코드를 생성합니다.

Apps Script를 Excel에서도 사용할 수 있나요?

아니요. Excel에서는 Apps Script 대신 Power Query M을 사용합니다. Excel에서 연결하는 방법은 Excel 튜토리얼을 확인하세요.

계속 읽기

무료 API 키로 시작하기

무료 플랜으로 월 5,000회 요청할 수 있습니다. 신용카드는 필요하지 않습니다.

무료 키 받기