매일 알아서 돌아가게 하기 (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는 됐는데, 반영까지 기다려야 했습니다. 그리고 매번 재배포가 일어나는 건 배포 횟수 제한이나 빌드 시간 측면에서도 좋지 않았고요.
자동화는 됐지만 반쪽짜리 자동화가 아닌가 싶었습니다. 새로운 문제의 탄생이었습니다...