🐾 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가 생기니 서버사이드 필터링이 필요했습니다. 처음엔 DB만 붙이면 끝날 줄 알았는데, 파이프라인 전체가 연쇄적으로 바뀌었습니다. 다음 단계가 뭔지는 이전 단계가 끝나야 보이더라고요.

30% 안전장치는 이론적으로 맞다고 생각해서 넣었는데, 실제로 버그가 터졌을 때 DB를 구했습니다. 정상 흐름만 생각하고 짰다면 그날 데이터를 날렸을 겁니다.

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