본문으로 건너뛰기
← All Projects
Featured Project

App Review Tracker

Apple·Google 양대 스토어의 앱 순위·리뷰를 매일 수집해 적재하고, React 대시보드에서 순위 추이·별점 분포·키워드를 분석하는 파이프라인. Claude Code 기반 AI 페어 개발로 진행하며, 코드 작성 주체는 AI이고 본인은 검증 기준 설계와 리뷰·반증을 담당했다.

App Review Tracker
App Review Tracker
App Review Tracker

Project Overview

역할

검증 기준·게이트 도입 판단, AI 산출물에 대한 리뷰·반증, 조사의 비용 판단과 중단 결정. 코드 작성 주체는 AI이며, 이는 저장소 PR 이력에도 남아 있는 사실이다.

기간·상태

2026.07~ 진행 중 — 개인 프로젝트. 로컬 cron으로 수집이 매일 계속되고 있다. 저장소는 비공개이며 요청 시 열람을 제공한다.

Project Story

프로젝트 이야기

배경과 과정, 그리고 판단의 기록

배경

2022년, 재직 중이던 회사에서 같은 문제의식으로 기획했다가 스캐폴딩 단계에서 중단한 프로젝트가 있었다. 퇴사 후 그 문제의식만 이어받아 처음부터 다시 시작했다 — 앱의 순위와 리뷰는 매일 흘러가 버리는 데이터이고, 쌓아두지 않으면 추이를 볼 수 없다.

AI 페어 개발에서 사람의 일

이 프로젝트의 코드 작성 주체는 AI다. 그래서 본인의 역할은 코드를 치는 것이 아니라, AI의 산출물과 진단을 믿을 수 있는지 검증하는 것이었다.

대표적인 사례가 9일간의 수집 실패다. AI는 원인을 rate limit으로 진단하고 예외명까지 그렇게 굳혀둔 상태였다. 브라우저에 URL을 직접 입력하는 A/B 실험으로 실제 원인이 URL 세그먼트 순서임을 규명했고, 같은 초에 두 순서를 비교해 순서가 결정적임을 확인했다. 진단이 틀렸음이 확인된 뒤에는 예외명을 관측 사실만 서술하도록 바꿨다.

반대 방향의 사례도 있다. 실패율이 이틀 연속 급등했을 때 AI는 수집 간격 복귀를 권했지만, 동일 조건에서 낮은 실패율이 관측된 날이 이미 있다는 반례를 들어 설정을 유지하고 하루 더 관측하기로 했다. 다음 날 실패율은 정상으로 돌아왔다. 그때 되돌렸다면 "간격 복귀 덕분에 회복됐다"는 틀린 귀속이 기록에 남았을 것이다.

기준을 정하고, 지키기

실험 게이트는 관측하기 전에 문서로 고정하고, 결과가 게이트를 벗어나면 게이트를 고치는 대신 결과를 보류했다. 수집 간격 실험은 채택 조건을 완화하지 않은 채 채택된 값 없이 종료했고, 현재 운영값이 기준선보다 실패율이 높다는 사실도 기록에 남겼다. 데이터 파이프라인에서 신뢰할 수 있는 것은 완벽한 수치가 아니라, 한계까지 적힌 수치라고 판단했다.

Technology Stack

이 프로젝트를 구성한 기술들

PythonPython 3.12
FastAPIFastAPI
SQLAlchemySQLAlchemy
MySQLMySQL
Kkiwipiepy
ReactReact 19
ViteVite
TypeScriptTypeScript
Tailwind CSSTailwind CSS
React QueryTanStack Query
RRecharts
DockerDocker Compose
NGINXNginx
Ccron

Key Features

What makes this project special

일일 수집 파이프라인

Apple·Google 스토어의 앱 순위와 리뷰를 cron 야간 자동화로 매일 수집해 MySQL에 적재한다.

분석 대시보드

순위 추이·별점 분포를 React 대시보드에서 조회하고, kiwipiepy 형태소 분석 기반으로 한국어 리뷰 키워드를 분석한다.

Technical Implementation

Architecture decisions and code highlights

Architecture

System Design

4-패키지 모노레포 · docker-compose 4서비스 · cron 야간 자동화

Key Highlights

  • AI가 9일간 "rate limit"으로 오진한 Apple 수집 실패를, 브라우저에 URL을 직접 입력하는 A/B 실험으로 반증 — 실제 원인이 URL 세그먼트 순서임을 같은 초의 비교 실측으로 규명하고, 예외명을 관측만 서술하도록 개명

  • 이틀 연속 실패율 급등에 AI가 권한 설정 복귀 대신, 반례 근거를 들어 설정 유지와 1일 추가 관측을 결정 — 다음 날 정상 복귀해 틀린 원인 귀속이 기록에 남는 것을 방지

  • 관점을 지정한 구조화 리뷰 패스를 PR 단위로 정례화 — 사양 누락(재시도 정책), 크롤 이력 식별 단위 결함, 유니크 키 누락 등을 병합 전에 발견

  • 수집 간격 실험에서 채택 게이트를 관측 전에 문서로 고정하고, 결과가 게이트를 벗어나도 조건을 완화하지 않은 채 종료 — 운영값의 한계(기준선 대비 높은 실패율)도 숨기지 않고 기록

Code Snippet

Implementation

9일간의 오진 끝에, 원인을 단정하던 예외명을 관측 사실만 서술하도록 바꾼 자리

코드 작성 주체는 AI입니다. 본인 기여는 오진을 반증하고 예외명 개명을 결정한 것입니다.

class AppleEmptyFeedError(RuntimeError):
    """Raised when the Customer Reviews RSS returns HTTP 200 with a valid
    body that has no 'entry' key at all.

    The name describes only what was observed, and that is deliberate. This
    class used to be called AppleThrottledError, which named a *cause* the
    response cannot actually tell us — the shape (same feed keys, empty
    pagination hrefs, no 'entry') is byte-for-byte what Apple returns for a
    nonexistent app id, and there is no header or body signal separating the
    possibilities. Naming it after one guess cost this project nine days: a
    malformed review URL produced this response, the name said "throttled",
    and so the response was read as a rate limit that would lift with time.
    Every plan built on that reading (wait 24h, wait a week,
    change storefront) was aimed at the wrong thing. Keep the name
    descriptive so the next reader has to ask which cause it is.

    Known causes, roughly in the order worth checking:

    1. **A URL Apple does not accept.** Legacy RSS answers a malformed or
       wrongly ordered path with this empty feed rather than a 4xx. See
       REVIEW_URL_TEMPLATE above — segment order matters.
    # ...
    """
if "entry" not in feed:
    # This message lands verbatim in crawl_run.error_message, so it
    # lists the candidates rather than asserting one — the previous
    # wording said "throttled" and that single word steered nine days
    # of diagnosis toward waiting instead of checking the URL.
    saved_path = _save_empty_feed_body(response, app_id)
    last_link = _last_link_state(feed)
    body_note = (
        f" — body saved: {saved_path}"
        if saved_path and len(response.content) not in KNOWN_EMPTY_FEED_BYTES
        else ""
    )
    raise AppleEmptyFeedError(
        f"apple customer reviews: HTTP 200 but no 'entry' in feed for "
        f"app_id={app_id} ({len(response.content)}B) last={last_link} "
        f"(url={url}) — "
        f"possible causes: malformed URL (segment order matters), rate "
        f"limiting, or an app id with no reviews in this storefront"
        f"{body_note}",
        response_bytes=len(response.content),
        body_saved_path=saved_path,
        last_link_state=last_link,
    )
Impact Analysis

Project Impact & Results

Real-world outcomes and measurable results

Key Metrics

누적 리뷰
약 24만 건
일일 크롤 대상
2,100+ 앱
수집 카테고리
25개

Achievements

  • 누적 리뷰 약 24만 건(Apple 53,653 / Google 190,524), 일일 크롤 대상 Apple 1,013 / Google 1,171 — 수치는 2026-09-01 기준

  • 두 질문("순위에 안 든 앱도 수집하나", "50건 오는데 30건만 취하나")으로 리뷰 대상 선정 결함과 페이지 상한 결함을 드러내 수집 범위를 정상화

Next Project

mono-lab

Monorepo & AI Workflow