🐾 Pet-Pass Dev Blog

바이브 코딩 개발기 · 이슈 #4 (2) 📖 약 8분

매일 알아서 돌아가게 하기 (2)

#Supabase #자동화 #데이터파이프라인 #GitHubActions #바이브코딩

목차

  1. stores.json의 한계
  2. DB를 도입하기로 했다
  3. 연쇄 작업들
  4. sync 파이프라인 고도화
  5. 30% 안전장치
  6. 품질 리포트 자동화
  7. 지금

stores.json의 한계

지난 편에서 GitHub Actions가 드디어 정상 실행됐다는 데서 끝났다. 엑셀을 받고, 파싱하고, data/stores.json을 새 데이터로 덮어쓰고, 커밋했다. 자동화가 됐다고 생각했다.

문제의 구조는 단순했다. stores.json은 소스코드 레포 안에 있었다. Vercel은 레포 파일이 바뀌면 자동으로 재빌드·재배포를 트리거했다. sync가 돌아서 파일이 커밋되면 → 재배포 → 그 시간 동안 서비스는 이전 데이터를 보여줬다.

하루에 두 번 sync가 돌면, 하루에 두 번 재배포가 일어났다. 데이터가 바뀔 때마다 코드도 같이 재배포되는 구조. 데이터와 코드가 같은 곳에 있으면 생기는 일이었다.

당시 stores.json은 1700여 개 매장 데이터에 500KB도 안 됐다. 프론트에서 이 파일을 받아 검색·필터링·표시 모두 처리했고, 속도는 무지하게 빨랐다. 구조적으로는 나쁘지 않았다. 문제는 그게 자동으로 갱신되면서부터였다.

DB를 도입하기로 했다

해결책은 데이터와 배포를 분리하는 것이었다. 데이터는 DB에, 코드는 레포에. sync가 DB를 직접 업데이트하면, 앱은 DB에서 읽는다. 파일이 바뀌지 않으니 재배포가 일어나지 않는다.

DB로 Supabase를 선택했다. Vercel 서버리스 함수에서 바로 붙을 수 있고, PostgreSQL 기반이라 나중에 복잡한 쿼리도 가능했다. 이 규모에서는 무료 티어가 충분했다.

결정은 빠르게 됐다. 실행은 그렇지 않았다.

연쇄 작업들

DB가 없던 서비스에 DB가 생기면, 그냥 DB만 생기는 게 아니다.

원래 서비스의 검색·필터링 로직은 전부 프론트에 있었다. stores.json을 한 번 받아서 메모리에 올려두고, 검색어가 바뀔 때마다 JavaScript로 필터링했다. 파일이 로컬에 있으니 빨랐다.

DB로 넘어가면 이 구조가 그대로일 수 없었다. 1800개 전체를 매번 API로 받아서 프론트에서 필터링하면 트래픽이 늘수록 나빠진다. 검색과 필터를 서버에서 처리해야 했다.

그러면 /api/stores 엔드포인트가 필요했다. 카테고리 필터, 지역 필터, 키워드 검색, 지도 영역 필터, 페이지네이션. 서버가 Supabase에 쿼리를 만들어서 결과를 반환하는 구조다. 지역 통계 계산도 서버로 옮겼다.

"파일 덮어쓰기 문제" 하나를 해결하려다 서비스 구조 전체가 바뀌었다. 프론트는 가벼워졌고, 서버 쪽 코드가 생겼다. 이 과정에서 CORS 이슈, 서버사이드 필터 버그, 페이지네이션 오프셋 문제 등이 줄줄이 따라왔다. 각각 별도 PR이 됐다.

"파일 하나 때문에 DB가 생겼고, DB 때문에 API가 생겼고, API 때문에 서버 코드가 생겼다. 자동화를 붙이는 일이 서비스를 다시 설계하는 일이 됐다."

Before
GitHub Actions엑셀 전체 파싱 · 전체 upsert
↓ commit
stores.json레포 안
↓ 파일 변경 감지
Vercel 재배포
브라우저
sync마다 재배포 발생
After
GitHub Actions엑셀 diff · 변경분만
↓ upsert
Supabase DB레포 밖
↓ 쿼리
/api/storesVercel 서버리스
브라우저
재배포 없음

sync 파이프라인 고도화

DB가 생기고 나니, sync 스크립트가 해야 할 일도 달라졌다.

우선 현재 가지고 있는 데이터를 밀어 넣는 스크립트를 작성했다. 당시 1712개의 데이터가 간단하게 들어갔다. 이제는 앞서 발견했던 비효율을 고쳐야할 시간이었다.

diff를 도입했다. sync 시작 시점에 DB의 현재 데이터를 Map으로 불러온다. 엑셀에서 읽은 데이터와 비교해서, 이름·주소가 같고 type·region 등 주요 필드에 변경이 없으면 skip한다. 이미 좌표가 있는 주소는 geocoding을 건너뛴다. 실제로 변경되거나 새로 추가된 것만 upsert한다.

하루 두번씩 돌아가게 되어 있는 지금 기준 매번 10개 가량의 데이터가 추가되고 있다. KAKAO API의 호출 횟수가 크게 줄었고, sync 속도도 매우 빨라졌다.

30% 안전장치

diff를 쓰면서 생긴 고민이 있었다. DB에 있는 데이터가 엑셀에 없으면 어떻게 할 것인가. 폐업한 매장이라면 지워야 맞다. 근데 파싱이 잘못돼서 데이터가 0건이 나온 경우라면 — 멀쩡한 매장 전부가 삭제 대상이 된다.

그래서 안전장치를 넣었다. DB 전체 데이터의 30% 이상을 한 번에 삭제하려 하면, sync 자체를 중단한다.

30%라는 기준은 뭐랄까 12시간 사이에 그만한 수의 매장이 폐업 처리되는 일은 없지 않겠어? 라는 느낌으로 가볍게 넣었다.

이 안전장치는 post6에서 실제로 작동했다. "?" 인코딩 버그로 파싱이 엉망이 됐을 때, 엑셀에서 정상 처리된 키 집합이 거의 비어있었다. DB에 있는 1700여 개 레코드 대부분이 삭제 대상으로 잡혔다. 삭제 직전에 안전장치가 발동해서 동기화가 중단됐고, DB는 무사했다.

나중에 생각해보면 당연한 장치인데, 처음에 넣었을 때는 그냥 방어적으로 추가한 코드였다. 그게 실제로 DB를 구한 날이 있었다.

품질 리포트 자동화

sync가 안정화되면서 다음 관심사가 생겼다. 잘 돌아가고 있는지 어떻게 아는가.

매 sync마다 결과를 docs/schedule_history.md에 자동으로 기록하도록 했다. GitHub Actions가 sync 결과를 커밋할 때 이 파일도 함께 업데이트한다. 실행 시간, 성공·실패 여부, 업데이트·삭제 건수, 품질 지표가 표 형식으로 쌓인다.

품질 지표는 네 가지다. 주소나 이름이 비어있는 누락 건수, 엑셀 내 중복 건수, 좌표를 못 구한 건수, 이름에 ?가 있어서 verified: false로 격리된 미검증 건수.

무언가 이상하면 이 히스토리를 보면 된다. 지난주와 비교해서 미검증 건수가 갑자기 늘었다면, 새로운 ? 매장이 추가됐다는 신호다. 수동으로 들여다봐야 할 타이밍을 데이터가 먼저 알려주는 구조다.

지금

하루 2회 자동 실행. Map 기반 diff sync. geocoding 캐시. 30% 안전장치. 품질 지표 자동 기록. Supabase 기반 서버사이드 검색·필터링.

처음 "API 하나 붙이면 되겠지"에서 시작했다. 공식 API가 구조적으로 안 된다는 걸 확인하고, 엑셀 파일로 방향을 바꿨다. GitHub Actions 환경과 씨름했다. 재배포 이슈로 DB를 도입했고, 그 덕에 서비스 구조 전체가 바뀌었다. sync가 돌아가기 시작한 뒤에도 diff, 안전장치, 품질 리포트가 차례로 추가됐다.

지금은 충분히 안정화됐다고 느낀다. 매일 알아서 돌아가고, 이상이 생기면 기록이 남는다. 자동화 자체가 목표였는데, 자동화를 만드는 과정이 서비스를 실질적으로 더 나은 구조로 바꿔놓았다.


이 문제에서 배운 것

데이터와 코드는 다른 곳에 있어야 한다. sync될 때마다 재배포가 일어나는 구조는 서비스가 커질수록 감당하기 어렵다. 처음에 이 분리를 고려했으면 중간에 DB를 끼워 넣는 큰 작업이 없었을 것이다.

하나를 고치면 다음 것이 보인다. DB가 생기니 API가 필요했고, API가 생기니 서버사이드 필터링이 필요했다. 처음부터 모든 걸 설계할 수는 없지만, 각 단계에서 가장 다음에 해야 할 것이 드러났다.

방어 코드는 내가 실수할 때 가장 필요하다. 30% 안전장치를 넣은 건 이론적인 이유였는데, 실제로 버그가 터졌을 때 DB를 구했다. 정상 흐름을 위한 코드만큼, 뭔가 잘못됐을 때를 위한 코드가 중요하다.

← 이전 글: 매일 알아서 돌아가게 하기 (1)