일기 slecs

구글시트 연동은 코드가 다 됐는데 열쇠가 없다

목차

코드는 다 됐다. CI도 통과했고, 로직도 손으로 훑어보면 틀린 데가 없다. 그런데 실제로 돌려볼 수가 없다. gsa.json-구글 서비스 계정 인증 파일-을 아직 못 받아서다. 이게 제일 찜찜한 상황이다. 빌드가 도는데 열쇠가 없어서 문 앞에 서 있는 느낌.

SA 키 → ADC + 임퍼스네이션 전환

오늘 가장 큰 삽질은 구글시트 연동 설계를 중간에 뒤집은 것이다. 처음엔 서비스 계정 키 파일(credentials.json)을 발급받아서 직접 사용하는 SA 키 방식으로 짰다. 가장 빠르게 붙일 수 있는 방법이고, 로컬 테스트도 쉬워서 일단 그렇게 갔는데-org 정책에서 서비스 계정 키 발급 자체를 막아놨다는 걸 뒤늦게 확인했다. 코드 절반쯤 짜고 나서 알았다.

그래서 ADC(Application Default Credentials) + 서비스 계정 임퍼스네이션 방식으로 전면 재설계했다. 구조적으로는 이게 더 맞다. 키 파일을 소스나 서버 로컬에 두지 않아도 되고, 자격증명이 GCP 환경 자체에서 관리된다. 키 로테이션이나 유출 걱정도 줄어든다. 다만 IAM 설정이 복잡해지고, 로컬 개발 환경에서 gcloud auth application-default login으로 맞춰야 하는 과정이 있어서 처음 세팅하는 사람이 헷갈리기 쉽다.

from google.auth import impersonated_credentials, default
from googleapiclient.discovery import build

base_creds, _ = default()
target_creds = impersonated_credentials.Credentials(
    source_credentials=base_creds,
    target_principal="[email protected]",
    target_scopes=["https://www.googleapis.com/auth/spreadsheets"],
)
service = build("sheets", "v4", credentials=target_creds)

이 패턴으로 재작성했다. SA 키 방식이랑 API 호출 자체는 똑같고, 자격증명 레이어만 바뀐 것이라 나머지 로직은 손댈 게 없었다. 어차피 결과물은 더 낫다고 생각하는데, 재작성에 쓴 시간이 아까운 건 사실이다. org 정책 확인을 먼저 했어야 했다. 인증 방식은 코드 짜기 전에 제약 사항부터 확인하는 게 맞다.

googleapis OOM, 파일명 인코딩, 중첩 태그 버그

googleapis 빌드하다가 OOM이 터졌다. 처음엔 뭔가 패키지 충돌이나 설정 문제인가 싶었는데 그냥 메모리 부족이었다. 빌드 서버 메모리를 4GB로 올리니까 해결됐다. 해결 자체는 단순한데 원인 파악까지 시간이 걸렸다. 빌드 서버 스펙이 빠듯할 때 이런 게 종종 튀어나온다. node --max-old-space-size 설정이나 서버 메모리 여유를 미리 확인하는 습관이 필요하다.

엑셀/zip 다운로드에서 한글 파일명이 깨지는 문제도 잡았다. Content-Disposition 헤더에 파일명을 그냥 넣으면 ASCII 범위 밖 문자에서 브라우저마다 동작이 달라진다. RFC 5987 인코딩으로 처리하면 크로스 브라우저 호환이 된다.

Content-Disposition: attachment; filename*=UTF-8''%EB%B6%80%EA%B0%80%EC%84%B8_%EB%82%B4%EC%97%AD.xlsx

filename(레거시)과 filename*(RFC 5987) 둘 다 같이 넣어두면 구형 클라이언트도 커버된다.

<a> 태그 안에 <button>을 중첩해서 쓰다가 클릭 이벤트가 씹히는 문제도 있었다. HTML 스펙상 인터랙티브 콘텐츠 중첩이 안 되는데, 브라우저마다 파싱 결과가 달라서 동작이 예측 불가능해진다. fetch로 blob을 받아서 프로그래밍 방식으로 다운로드하는 방식으로 바꿨다.

const res = await fetch("/api/export");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = decodeURIComponent(filename);
a.click();
URL.revokeObjectURL(url);

이 패턴이 <a download> 직접 참조보다 파일명 제어나 에러 핸들링 면에서 훨씬 낫다.

오늘 처리한 나머지 작업들

  • 캘린더 드래그앤드롭: 날짜 이동 UX가 붙으면서 거래 수정 기능이랑 묶였다. 생각보다 잘 됐고, 이제 쓸만한 물건이 됐다.
  • 심링크 경로 정리: 예전에 심링크 지웠다가 아찔했던 적이 있어서 이번에 canonical public 경로를 직접 참조하는 방식으로 바꿨다. 심링크 체인이 길어지면 배포나 경로 변경 때마다 리스크가 생긴다.
  • 앱 매출 통합 페이지: App Store랑 Google Play를 따로 보던 걸 한 화면으로 합쳤다. App Store는 최신 1일치만 보이던 걸 30일 합산으로 고쳤다. 수치가 제대로 보여야 판단이 가능하다.
  • B2B 데모: 3D 컬러 비주얼라이저, SUPPORT 자료실, EWIS 단면 핫스팟이 추가됐다. 기능이 너무 많아지면 인상이 흐려지기 때문에 레퍼런스 필터 관리자 토글은 기본 비활성으로 두는 게 맞다고 판단했다.
  • 인프라: intonaire, gwangclock 유료 플랜 무료 전환, 서버 13대 클린 재설치. 눈에 안 띄는 작업이지만 안 하면 나중에 크게 터진다.

오늘 컨텍스트 전환이 너무 많았다. 구글시트 인증 설계부터 빌드 OOM, 파일명 인코딩, 프론트 버그, 심링크 정리, 앱 통계 UI, 데모 스펙 추가, 인프라 정리까지. 각각은 다 처리했는데 흐름이 너무 잘게 쪼개지다 보니 피곤하다. 내일 첫 번째 할 일은 gsa.json 받는 즉시 구글시트 양방향 연동 테스트다.

댓글 0

첫 댓글 달아줘.