Day 15는 새 기능을 만드는 날이라기보다 "지금까지 만든 걸 안 부수고 계속 만들 수 있게 다지는 날"이었어요. pytest 테스트를 처음 도입하고, 사용량 카운터를 Redis로 옮기고, 다음에 할 만한 기능 8개를 브레인스토밍한 다음 그중 첫 번째(PR 자동 리뷰)까지 실제로 만들었어요.
review.py/ask.py/build_index.py/usage.py(코드 리뷰 에이전트)와 bot.py(디스코드 봇)까지, 지금까지 손으로만 확인해온 로직에 처음으로 테스트를 붙였어요. 핵심은 Gemini API를 실제로 부르지 않는 것 — genai.Client를 가짜로 바꿔치기해서 "이 입력을 넣으면 이 시스템 프롬프트로 이렇게 호출한다"까지만 검증해요.
# tests/test_usage.py
@pytest.fixture(autouse=True)
def isolated_usage_file(tmp_path, monkeypatch):
monkeypatch.setattr(usage, "USAGE_FILE", str(tmp_path / "usage.json"))
monkeypatch.setattr(usage, "PUBLIC_USAGE_FILE", str(tmp_path / "usage_public.json"))
yield tmp_path
usage.py처럼 로컬 파일에 상태를 쓰는 모듈은 tmp_path로 실제 파일 경로를 갈아끼워서 테스트가 내 진짜 usage.json을 건드리지 않게 했어요. 이런 식으로 5개 파일에 걸쳐 31개 테스트를 추가했어요.
포트폴리오 사이트 챗 위젯의 "하루 호출 한도"는 원래 서버 메모리에 숫자를 들고 있었는데, Vercel 서버리스 특성상 인스턴스가 재시작되면 그 숫자가 그냥 사라져요. Upstash Redis(Vercel 마켓플레이스 연동, 환경변수는 옛 이름인 KV_REST_API_URL/KV_REST_API_TOKEN 그대로 씀)로 옮겨서 이 문제를 없앴어요.
// src/lib/kodex-chat-limit.ts
const redis =
process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN
? new Redis({ url: process.env.KV_REST_API_URL, token: process.env.KV_REST_API_TOKEN })
: null;
export async function recordDailyUsage(): Promise<void> {
if (!redis) {
syncFallback(); // 로컬 개발 등 Redis 미연결 시 인메모리로 자동 폴백
fallbackDaily.count += 1;
fallbackTotal += 1;
return;
}
const key = dailyKey();
const newCount = await redis.incr(key);
if (newCount === 1) await redis.expire(key, DAILY_TTL_SECONDS);
await redis.incr(TOTAL_KEY); // 전체 기간 누적 — About 섹션 대시보드용
}
덤으로 이 위젯 자체의 누적 호출 수까지 기록되기 시작해서, Day14에서 "CLI+봇만" 잡히던 사용량 대시보드에 사이트 챗 위젯 몫도 더할 수 있게 됐어요.
품질을 다졌으니 다음엔 뭘 만들지 정할 차례였어요. 세 방향(에이전트 기능 확장 / 콘텐츠·기록 자동화 / 방문자 상호작용)으로 나눠서 아이디어를 던지고, 백로그에 8개를 정리했어요.
에이전트 기능 확장: PR 자동 리뷰, 커밋 메시지 자동 제안, 리뷰 심각도 태그
콘텐츠·기록 자동화: 인스타 캡션 초안 자동 생성, 주간 요약 자동 발행
방문자 상호작용: 챗 위젯 라이브 코드 리뷰 데모, FAQ quick-reply, 방명록
한 번에 다 만들지 않고 대화할 때마다 하나씩 순서대로 처리하기로 했어요.
④ 그중 첫 번째: PR 자동 리뷰
이미 있는 CI 패턴(GitHub Actions가 저장소를 체크아웃해서 스크립트를 돌리고 결과를 git에 반영하는 구조)을 그대로 재사용할 수 있어서 제일 먼저 골랐어요. PR이 열리거나 커밋이 추가되면 base 브랜치 대비 diff를 review.py --diff와 같은 로직으로 리뷰하고 PR에 코멘트를 남겨요.
# pr_review.py
MARKER = "<!-- kodex-pr-review -->"
def find_existing_comment(repo, pr_number, token):
comments = _api_request("GET", f"{API_ROOT}/repos/{repo}/issues/{pr_number}/comments", token)
for comment in comments:
if comment.get("body", "").startswith(MARKER):
return comment
return None
새로 만든 건 GitHub API를 부르는 부분뿐이었는데, requests 같은 라이브러리를 새로 추가하는 대신 이 프로젝트가 fetch_posts.py에서 이미 쓰던 stdlib urllib로 통일했어요. 커밋을 더 추가해서 워크플로가 다시 돌아도 코멘트가 계속 쌓이지 않도록, 마커 주석으로 이전 코멘트를 찾아서 새로 남기는 대신 갱신하게 만든 게 이번에 신경 쓴 부분이에요. diff가 너무 길면(15,000자 초과) 앞부분만 리뷰하고 잘렸다는 안내도 붙여요. 테스트도 9개 같이 추가했어요.
pytest 도입(31개), Redis 전환, 8개 기능 브레인스토밍, 그중 1번 PR 자동 리뷰까지 끝냈어요. 백로그의 "인프라 개선" 항목은 이제 커스텀 도메인만 남았고(우선순위 낮아서 보류), "새로 해볼 만한 기능"은 8개 중 7개가 남았어요.
백로그에 정해둔 순서대로 하나씩: 다음은 커밋 메시지 자동 제안이에요.