Background
duckzip(덕집)은 좋아하는 작품과 캐릭터를 중심으로 모이는 팬 커뮤니티입니다. 작품 게시판, 캐릭터 투표, 이상형 월드컵, 굿즈·콜라보 일정, 1:1 상담 채팅 등을 제공합니다. 4명이 함께 개발하는 프로젝트에서 PM을 맡아 저장소와 모노레포 구조를 처음 만드는 것부터 환경별 인프라, 배포 파이프라인, 팀 협업 자동화, 모든 API가 거쳐 가는 공통 기반까지 개발 환경 전체를 직접 구축했습니다.
Key Work
- 01인프라 초기 세팅: Vercel(웹), Supabase(DB·인증·Realtime), Cloudflare R2(파일 스토리지)를 연동하고 환경변수 체계와 .env.example 정비
- 02local / dev / preview / prod 4단계 환경 설계와 브랜치별 자동 배포(preview, main만 빌드)
- 03GitHub Actions: main 갱신 시 하위 브랜치 자동 동기화, 충돌·PR 발생 시 Discord 알림
- 04DB 마이그레이션 스크립트(dev/prod 분리 실행)와 운영 반영 규칙
- 05API 공통 기반: Route Handler 공통 에러 처리·CORS, JWT 인증 레이어, 중복 요청 방지(idempotency)
- 06공통 파일 업로드·다운로드·썸네일 API, 실시간 채팅 공통 모듈(Supabase Realtime)
- 07관리자용 인프라 사용량 모니터링 대시보드(Vercel·Supabase·R2 API 연동)
- 08SEO(robots, sitemap, OG 메타데이터, Search Console), PWA, 앱 WebView 셸
- 09Claude Code 팀 개발 환경: 프로젝트 규칙(CLAUDE.md), 스킬, 브랜치 보호 훅
- 10AI 자연어 검색(Gemini + 벡터 하이브리드 검색)
Problem & Solution
Problem 01
local, dev, preview, prod 네 환경이 필요했지만 Supabase 무료 플랜은 프로젝트가 2개로 제한되었고, 어느 브랜치에 push하든 Vercel이 빌드를 돌려 빌드 자원이 낭비되었습니다.
Solution
local/dev/preview는 개발 DB를 공유하고 운영 DB만 분리하는 2-프로젝트 전략을 세우고, 파일 스토리지(Cloudflare R2)도 staging/prod 버킷으로 나눴습니다. Vercel ignoreCommand로 preview와 main만 빌드되게 했고, bash 전용 문법 때문에 조건이 무시되던 문제를 POSIX 호환 문법으로 고쳐 의도대로 동작하게 만들었습니다.
Problem 02
팀원들이 각자 feature 브랜치에서 작업하다 보니 main과의 차이가 금방 벌어져 충돌이 커졌고, 보호해야 할 main/dev 브랜치에 직접 작업하는 실수도 생길 수 있었습니다.
Solution
GitHub Actions로 main이 갱신될 때마다 dev, preview, 모든 feature/* 브랜치에 자동 merge하고, 충돌이 나면 merge를 되돌린 뒤 해결 방법과 함께 Discord로 알리게 했습니다. PR이 열리면 커밋 목록과 테스트 체크리스트를 Discord로 보냈고, Cloudflare가 요청을 차단하는 문제는 curl 요청과 User-Agent 지정으로 해결했습니다. Claude Code 훅으로 main/dev 직접 수정을 막고, 세션을 시작할 때 main보다 뒤처진 커밋 수를 알려주도록 했습니다.
Problem 03
DB 스키마 변경이 개발 DB에만 적용되고 운영 DB에는 빠진 채 배포되면 운영 서비스가 바로 깨질 위험이 있었습니다.
Solution
SQL 마이그레이션 파일을 대상 환경(dev/prod)을 골라 실행하는 migrate 스크립트를 만들고, main으로 PR을 올리기 전에 운영 DB 마이그레이션을 먼저 적용하는 규칙을 문서화했습니다. 현재 70여 개의 마이그레이션이 이 흐름으로 관리되고 있습니다.
Problem 04
외부에서 API를 마음대로 호출할 수 있었고, 버튼을 연달아 누르거나 네트워크가 재시도하면 같은 요청이 중복으로 처리될 수 있었습니다.
Solution
모든 Route Handler가 거쳐 가는 공통 래퍼(withHandler)에 IP·Origin에 묶인 JWT 검증을 넣고, 공통 apiClient가 토큰을 자동으로 붙이고 만료되면 재발급 후 재시도하게 했습니다. 클라이언트는 진행 중인 같은 요청을 재사용하고, 서버는 Idempotency-Key 캐시로 중복 처리를 막아 이중 방어했습니다.
Problem 05
처음에는 웹과 앱이 공유 UI 패키지를 함께 쓰는 모노레포로 설계했지만, 기능이 늘수록 앱에도 같은 화면을 따로 맞춰야 해서 개발 속도가 떨어졌습니다.
Solution
앱을 Next.js 웹앱을 띄우는 WebView 셸로 전환해 웹에 추가한 기능이 앱에 자동으로 반영되게 했습니다. 웹 쪽에는 WebView 환경 감지 유틸을 두어 앱에서만 다르게 보여야 하는 부분을 처리했습니다.
Problem 06
로그인한 사용자의 거의 모든 요청마다 인증 확인 뒤 users 테이블을 한 번 더 조회해, 요청당 DB 왕복이 불필요하게 하나씩 더 붙어 있었습니다.
Solution
계정 상태, 정지 기간, 가입 완료 시각을 인증 토큰의 app_metadata에 동기화해 두고 미들웨어가 그 값을 바로 읽게 바꿔 요청당 DB 왕복을 절반으로 줄였습니다. 값을 바꾸는 세 곳(제재 처리, 소셜/이메일 가입 확정)에 동기화를 넣고, 기존 계정 데이터 누락으로 생긴 소셜 로그인 리다이렉트 루프도 함께 해결했습니다.
Problem 07
AI 자연어 검색을 넣을 때, LLM이 존재하지 않는 작품이나 글을 지어내거나 SQL을 직접 만들면 보안과 신뢰성 문제가 생길 수 있었습니다.
Solution
Gemini 호출을 '질문 분류'와 '답변 생성' 두 단계로 나누고, 그 사이 DB 조회는 서버 코드가 파라미터 바인딩으로 수행하게 했습니다. 답변에 인용된 작품과 글은 실제 검색 결과와 대조해 유효한 것만 보여줍니다. 이미지 캡션은 업로드 시점에 미리 만들고 벡터 임베딩을 더해 하이브리드 검색을 구현했으며, 기존 API 로그를 재사용해 시간당 호출 수를 제한했습니다.
Results
- ✓첫 인프라 세팅부터 운영 배포까지 전 과정을 직접 구축하고 서비스 운영 중
- ✓4단계 환경을 무료 플랜 안에서 분리하고 브랜치별 자동 배포 구성
- ✓GitHub Actions와 Discord 알림, Claude Code 훅으로 4인 팀의 브랜치·PR 흐름 자동화
- ✓JWT 인증, 중복 요청 방지, 공통 에러 처리 등 모든 API가 공유하는 공통 기반 제공
- ✓인증 요청당 DB 왕복 50% 감소
- ✓README 아키텍처 설계도, 온보딩 순서, DB 접근 가이드 등 신규 개발자 문서 정비
Learnings
- →PM으로서 기능보다 '팀이 같은 방식으로 일하게 만드는 기반'을 먼저 세우는 것이 전체 속도를 높인다는 점
- →무료 플랜의 제약 안에서 환경을 분리하고 비용과 배포를 통제하는 인프라 설계
- →초기 설계가 맞지 않을 때 빠르게 구조를 바꾸는 판단(공유 UI에서 WebView 셸로 전환)
- →LLM 기능은 모델에 맡길 일과 서버 코드가 책임질 일을 나눠야 안전하다는 점