googleapis 패키지 없이 Node.js에서 Google 서비스 계정 JWT 만들기

발행: (2026년 6월 9일 PM 12:52 GMT+9)
8 분 소요
원문: Dev.to

출처: Dev.to

googleapis npm 패키지는 Node.js에서 Google API를 호출할 때 기본적인 답변입니다. 작동은 하지만 약 380KB 정도의 용량을 차지하고 450개가 넘는 전이 의존성을 가져옵니다. CI 스크립트에서 단일 API(검색 콘솔 URL 검사 API)만 사용할 경우, 기본 인증 흐름이 충분히 단순해서 직접 구현해도 됩니다.

나는 scripts/gsc-inspect.mjs 파일을 만들어 공개된 URL들의 인덱스 상태를 확인했습니다. 약 60줄 정도이며 Node.js 기본 모듈(crypto, fetch, URL) 세 가지만 사용하고, 레포에 추가되는 패키지는 전혀 없습니다.

서비스 계정 인증 흐름

Google의 서비스 계정 인증은 RFC 7523—OAuth2의 JWT Bearer Grant 프로필을 따릅니다. 단계는 다음과 같습니다.

  • 서비스 계정의 client_email과 개인 키를 사용해 JWT를 생성한다
  • 그 JWT를 https://oauth2.googleapis.com/token에 POST한다
  • 짧은 수명의 액세스 토큰(유효 기간 3600초)을 받는다
  • API 요청 시 Bearer 헤더에 액세스 토큰을 넣어 사용한다

JWT 클레임은 다음과 같습니다.

const claims = {
  iss: sa.client_email,
  scope: "https://www.googleapis.com/auth/webmasters.readonly",
  aud: "https://oauth2.googleapis.com/token",
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + 3600,
};

미리 알아두면 좋은 점: searchconsole이 아니라 webmasters 스코프를 사용하세요. URL 검사 API는 webmasters.readonly 스코프가 필요합니다. 최신 searchconsole 스코프는 이 API에 대한 접근 권한을 부여하지 않습니다.

Node의 crypto 모듈로 서명하기

Base64url 인코딩만이 다소 헷갈릴 수 있는 부분입니다. 일반 Base64를 Base64url 형태로 바꾸려면 세 가지 문자 교체가 필요합니다.

import { createSign } from "node:crypto";

const b64url = (obj) =>
  Buffer.from(JSON.stringify(obj))
    .toString("base64")
    .replace(/=+$/, "")     // 패딩 제거
    .replace(/\+/g, "-")    // + → -
    .replace(/\//g, "_");   // / → _

const unsigned = `${b64url(header)}.${b64url(claims)}`;
const signer = createSign("RSA-SHA256");
signer.update(unsigned);
signer.end();
const sig = signer
  .sign(sa.private_key)
  .toString("base64")
  .replace(/=+$/, "")
  .replace(/\+/g, "-")
  .replace(/\//g, "_");
const jwt = `${unsigned}.${sig}`;

sa.private_key는 Google Cloud Console에서 다운로드한 서비스 계정 JSON 파일에 들어 있는 RSA 개인 키 문자열입니다. 이미 PKCS#8 PEM 형식(-----BEGIN PRIVATE KEY-----...)이므로 createSign("RSA-SHA256").sign(key)만으로 바로 사용할 수 있습니다. 키 변환이나 외부 라이브러리가 전혀 필요하지 않습니다.

JWT를 액세스 토큰으로 교환하기

토큰 교환은 URL‑encoded form 형태의 POST 요청입니다.

const res = await fetch("https://oauth2.googleapis.com/token", {
  method: "POST",
  headers: { "content-type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
    assertion: jwt,
  }),
});
if (!res.ok) {
  const body = await res.text();
  throw new Error(`Token exchange failed (${res.status}): ${body.slice(0, 300)}`);
}
const { access_token } = await res.json();

여기서 오류 처리가 중요합니다. Google에서 반환하는 invalid_grant 오류는 종종 "Token must expire within 3600 seconds of the issued time" 혹은 "Service account not found"와 같은 유용한 error_description을 포함합니다. 원시 응답 본문을 (300자까지) 로그에 남기면 프레임워크의 추상화 레이어를 거치지 않고도 원인을 바로 확인할 수 있습니다.

URL 검사 엔드포인트 호출하기

토큰을 확보한 뒤에는 다음과 같이 요청합니다.

const res = await fetch(
  "https://searchconsole.googleapis.com/v1/urlInspection/index:inspect",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      inspectionUrl: url,
      siteUrl: `https://${siteHost}/`,
    }),
  }
);
const data = await res.json();
console.log(JSON.stringify({
  url,
  coverageState: data.inspectionResult?.indexStatusResult?.coverageState,
  lastCrawlTime: data.inspectionResult?.indexStatusResult?.lastCrawlTime,
}));

siteUrl 필드는 Google Search Console에 검증해 둔 속성값과 정확히 일치해야 합니다(끝 슬래시 포함). Cloudflare DNS TXT 레코드로 세 도메인을 검증한 뒤 서비스 계정의 client_email을 Search Console 사용자(Owner 또는 Full user)로 추가해야 API가 정상적으로 응답합니다.

coverageState 값에는 INDEXED, SUBMITTED_AND_INDEXED, CRAWLED_CURRENTLY_NOT_INDEXED 등 여러 종류가 있습니다. 게시 후 검증용이라면 URL당 한 줄의 JSON만 출력하면 충분합니다—CI 로그에서 바로 grep하고, 별도 도구 없이도 쉽게 확인할 수 있습니다.

이 방법을 사용하면 안 되는 경우

단일 API를 CI 스크립트에서 호출할 때는 직접 구현하는 것이 적합하지만, 다음과 같은 상황에서는 부적절합니다.

  • 여러 Google API를 동시에 호출하면서 통합된 인증 처리를 원할 때
  • 장시간 실행되는 프로세스에서 자동 토큰 갱신이 필요할 때
  • 재시도 로직, 배치 처리, 혹은 타입‑안전한 API 응답이 필요할 때
  • 프로덕션 서버 코드를 배포하고, 검증된 라이브러리의 무게가 정당화될 때

이 프로젝트에서는 한 CI 파이프라인을 다섯 개의 자동화 워크플로에 재사용하고(관련 글) 불필요한 npm 의존성을 피하고자 했습니다. 읽기 쉽고 검증 가능한 60줄 정도의 코드는 450개가 넘는 전이 의존성보다 훨씬 효율적입니다.

또한 이 구현은 문서화 용도로도 유용합니다. googleapis 패키지는 JWT 흐름을 너무 추상화해서 실제 인증 교환 과정이 어떻게 이루어지는지 모르는 개발자가 많습니다. 순수 흐름—JWT → 토큰 엔드포인트 → Bearer 헤더—을 이해하면 어떤 라이브러리를 사용하든 인증 오류를 더 빠르게 디버깅할 수 있습니다.

사이트는 아직 초기 단계입니다. 의미 있는 데이터가 쌓이면 30일 뒤에 실제 URL별 인덱스 커버리지 정보를 공개할 예정입니다.

6개월 동안 세 개의 AI‑큐레이션 디렉터리 사이트를 운영하면서 진행한 실험의 일환입니다. 여기서 다루는 기술적 내용은 실제이며, 본 글은 AI의 도움을 받아 작성되었습니다.

0 조회
Back to Blog

관련 글

더 보기 »

Eidentic 소개

Today we're releasing Eidentic, an open-source TypeScript SDK for building AI agents with self-improving memory and the production fundamentals built in — not b...

Typescript의 타입

Introdução Tipos são uma forma de definir a “forma” ou o contrato dos dados que estamos usando no código. Pensando em Javascript puro, ele é dinâmico: você pode...