매일 알아서 돌아가게 하기 (1)
왜 자동화인가
Pet-Pass의 핵심은 반려동물 동반 가능 매장 데이터다. 초기에는 이걸 수동으로 관리했다. 식품안전나라에서 엑셀을 받아서 직접 변환하고 서비스에 올렸다.
문제는 데이터가 살아있다는 것이다. 새 가게가 생기고, 폐업하고, 정보가 바뀐다. 식품안전나라도 정기적으로 갱신된다. 바뀔 줄은 알았지만 하루에도 몇번씩 바뀌는 데이터를 계속 모니터링 하고 있자니 자동화가 시급하다는 생각이 들었다.
공식 API가 나왔다
2026년 3월 23일, 타이밍이 좋아 보이는 공지가 하나 떴다.
식품의약품안전처에서 '반려동물 동반출입 음식점' 관련 정보를 신규 제공함을 안내드립니다.
서비스명: 식품접객업정보 / 신규속성: 반려동물출입여부 / 제공일자: 2026. 03. 10.
공식 OpenAPI에 반려동물 관련 데이터가 추가됐다는 뜻이었다. 이걸 주기적으로 호출하면 자동화가 깔끔하게 될 것 같았다. API가 있으면 아무래도 API를 쓰는게 개발자 다운일이니까.
API를 뜯어보니
API 명세를 들여다봤다. 요청 파라미터는 이게 전부였다. 인증키, 서비스명, 요청 타입, 시작·종료 위치, 변경일자, 인허가번호, 데이터생성일, 업소명.
반려동물출입여부는 없었다. 응답 필드에는 있다. PET_OUTIN_YN이라는 이름으로. 하지만 이걸 요청 시점에 파라미터로 넘길 방법이 명세 어디에도 없었다.
이 API의 용도가 명확해졌다. 업소명이나 인허가번호 같은 식별 정보를 이미 알고 있을 때, 해당 업소의 상세 정보를 조회하는 도구다. "반려동물 가능한 가게 목록 전체를 주세요"를 요청하는 구조가 아니었다.
전체를 다 받아서 앱에서 걸러내는 방법이 불가능한 건 아니었다. 전국 식품접객업소가 수십만 건이라는 게 문제였다. 그걸 매일 페이지 단위로 전부 받아서 Y인 것만 추리는 건 현실적인 설계가 아니었다.
그래도 해봤다
포기하기 전에 일단 시도는 했다. 시도 과정이 결과보다 더 험난했다.
서비스 ID를 I1200이 아닌 I1250으로 입력하고 있었다. 숫자 두 자리 차이. 응답이 계속 이상하게 돌아오는데 한참 동안 이유를 몰랐다. 파라미터를 이리저리 바꿔보다가, 결국 URL 자체를 처음부터 다시 따라가고 나서야 발견했다. 코드 어딘가에서 잘못 입력된 값이 그대로 굳어진 경우였다.
API 키도 제대로 안 먹혔다. 이건 post3에서 다뤘던 빌드 타임 vs 런타임 문제의 연장이었다. 카카오 API 키는 빌드 시점에 HTML에 주입해서 곧바로 동작했는데, 정부 OpenAPI 키는 런타임에 서버가 읽어야 했다. 그 구조가 처음엔 맞지 않았고, 키가 있어도 인식이 안 됐다.
이 과정을 거치고 나서 결론을 내렸다. 이 API로는 원하는 걸 만들 수 없다.
엑셀 파일로 전환
방법을 바꿨다. 식품안전나라에는 엑셀 다운로드 기능이 있었다. 반려동물 동반 가능 업소 목록 전체를 엑셀로 내보내는 URL이 존재했다. 이 URL에 직접 요청을 보내면 파일이 내려왔다.
API보다 단순한 방식이었다. 인증 없이 URL 하나로 파일을 받고, SheetJS로 파싱하면 1800행짜리 JSON이 됐다. 원하는 데이터가 거기 다 있었다. 이 방식이 나중에 post6에서 다룬 인코딩 문제들의 출발점이 되기도 하지만, 그건 이 다음에 생긴 일이다.
방향이 잡혔다. 이제 이걸 자동으로 돌리는 일만 남았다.
GitHub Actions 구성
자동화 수단으로 GitHub Actions를 선택했다. 별도 서버 없이 레포에 YAML 파일 하나로 주기 실행이 된다는 게 이유였다. daily_sync.yml을 만들고, cron으로 하루에 두 번 실행하도록 설정했다.
워크플로우의 설계는 간단했다. 체크아웃 → 엑셀 다운로드 → 파싱 → 저장 → 커밋. 흐름만 보면 다섯 줄이다.
실제로는 이 환경을 세팅하는 데 코드보다 시간이 더 걸렸다.
환경의 벽
첫 번째는 Node.js 버전 문제였다. GitHub Actions 실행 환경에서 Node.js 버전에 따라 지원 중단 경고가 붙거나 패키지가 제대로 동작하지 않았다. FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 환경변수를 추가하고, actions 버전을 v4로 고정하고, Node.js 실행 버전을 맞추는 작업이 줄줄이 따라왔다. 각각 별도 PR이 됐다.
두 번째가 더 허탈했다. 워크플로우를 실행했더니 Cannot find module 'axios'가 났다. 코드에 require('axios')가 있고, package.json에도 있다. 그런데 실행 환경에 패키지가 없었다. 워크플로우 단계에 npm install을 넣지 않았던 것이다. 로컬에서는 당연히 설치된 상태라 신경도 안 썼던 단계였다. GitHub Actions의 실행 환경은 매번 새로 시작한다. 아무것도 없다.
세 번째는 secrets 관리였다. Kakao API 키, Supabase URL, Supabase Secret Key — 셋 다 GitHub Secrets에 등록했다. 등록까지는 됐는데, 스크립트가 받는 환경변수 이름과 워크플로우에서 주입하는 이름이 일치하지 않았다. SUPABASE_ANON_KEY로 쓰던 걸 스크립트에서 SUPABASE_SECRET_KEY로 받고 있다거나 하는 식의 불일치가 여러 곳에 있었다. 실행해봐야 알 수 있는 종류의 오류들이라 PR이 한 번에 안 끝났다.
"코드가 틀린 게 아니라 실행 환경이 빈 것들이었다. 로컬에서는 드러나지 않는 종류의 오류들."
드디어 돌아갔다 — 근데
처음으로 Actions 로그에 성공 표시가 떴다.
이 시점의 데이터 저장 방식은 data/stores.json이었다. 1700여 개 매장 데이터가 하나의 파일에 담겼다. 500KB도 안 됐다. 프론트에서 이 파일을 받아 검색·필터링·표시를 모두 처리했다. 별도 API 서버 없이도 됐고, 무엇보다 속도가 빨랐다. 파일을 한 번 받으면 이후 모든 검색이 로컬에서 처리됐다.
어쨋든 성공은 했다. 그런데 GitHub Actions가 sync를 돌릴 때마다, stores.json 전체를 새 데이터로 덮어썼다.
엑셀을 받고 파싱하고 데이터를 처리하는 과정이 너무나도 오래걸리는 문제가 발생했다. 카카오 지오코딩을 수천건씩 호출하고 있는 모습이란... 수백만건이면 모를까 겨우 2천개 남짓한 데이터를 처리하는데 10분이 넘는 모습은 소위 '올바른' 모습이 아니었다.
파일이 바뀌면 Vercel이 재배포를 트리거했다. 재배포가 완료되어야만 변경된 데이터가 사용자에게 반영됐다. sync가 됐는데, 반영까지 기다려야 했다. 그리고 매번 재배포가 일어나는 건 배포 횟수 제한이나 빌드 시간 측면에서도 좋지 않았다.
자동화가 되었지만 반쪽짜리 자동화가 아닌가 생각이 들었다. 새로운 문제의 탄생이었다.