3 분 소요

1. 수집 대상

분석에 필요한 6종 데이터를 정의하고 각각 적절한 API 소스를 매핑했습니다.

데이터 소스 주기 BQ 테이블
게임 메타정보 Steam Store API (appdetails) 초기 + 매일 신규만 games
가격 스냅샷 Steam Store API 매일 1회 price_history
가격 백필 (과거) IsThereAnyDeal API 신규 게임만 1회 price_history
뉴스/패치노트 Steam News API (GetNewsForApp) 매일 (KST 전날분) news
유저 리뷰 Steam Reviews API (appreviews) 매일 (KST 전날분) reviews, review_summary
동시접속자 Steam Web API (GetNumberOfCurrentPlayers) 3시간마다 player_counts

게임 목록은 SteamSpy all API로 소유자 수 기준 상위 500개를 초기 적재하고, 이후엔 Steam Korea 스토어의 트렌딩 상위 50개를 매일 확인해 DB에 없는 신규만 추가하는 방식으로 관리합니다. 시간이 지날수록 자연스럽게 DB가 풍부해지는 구조입니다.


2. GCP 아키텍처

┌──────────────────────┐
│  Cloud Scheduler     │  매일 / 3시간 트리거
└──────────┬───────────┘
           ↓ HTTP (OIDC token)
┌──────────────────────┐
│  Cloud Functions × 5 │  Python functions-framework
│                      │  ┌───────────────────────────┐
│                      │  │ collect_daily             │
│                      │  │ process_new_games         │
│                      │  │ translate_daily           │
│                      │  │ analyze_daily             │
│                      │  │ collect_player_counts     │
│                      │  └───────────────────────────┘
└──────────┬───────────┘
           ↓ Steam API / ITAD API 호출
           ↓ insert_rows_json
┌──────────────────────┐
│  BigQuery steam_data │  8개 테이블
└──────────────────────┘

설계 원칙은 “중간 저장소 없이 직접 BQ에 적재”입니다. RDS에 INSERT하듯이 client.insert_rows_json()을 직접 호출해서 데이터 흐름을 단순화했습니다. 별도 ETL 도구(Airflow 등)는 규모상 오버킬이라 제외.


3. Cloud Functions 5종

함수별 역할과 스케줄을 명확히 분리했습니다.

함수 스케줄 (KST) 역할
collect_daily 매일 01:00 트렌딩 신규 게임 등록 + KST 전날 reviews/news/price 수집
process_new_games 매일 02:00 신규 게임의 ITAD 가격 이력 백필 + 전체 뉴스 수집
translate_daily 매일 03/09/13/17시 deep-translator로 미번역 리뷰 한국어 번역 (일 8000건 capacity)
analyze_daily 매일 20:00 번역 완료 리뷰의 Kiwi 감성·키워드 분석 → category/keyword_analysis 적재
collect_player_counts 3시간마다 전체 게임 동시접속자 수 append (하루 8회)

시간 분산의 이유

  • collect 후 즉시 분석하면 번역 안 된 리뷰가 많음 → 번역 4회 분산 후 마지막 단계로 분석
  • 동접은 게임 활동의 시간대 패턴을 보려면 3시간 bin이 적절 (24시간 단위로는 너무 거칠어서 히트맵에 의미 없음)

4. 멱등성 처리 — dedup 헬퍼

같은 함수를 두 번 트리거해도 같은 데이터가 두 번 들어가지 않도록, 각 적재 함수마다 dedup 단계를 둡니다.

def _dedup_reviews(rows: list[dict]) -> list[dict]:
    """기존 recommendation_id와 겹치는 리뷰 제거"""
    rids = [r["recommendation_id"] for r in rows]
    rid_list = ", ".join(f"'{r}'" for r in rids)
    query = f"SELECT DISTINCT recommendation_id FROM reviews WHERE recommendation_id IN ({rid_list})"
    existing = {row.recommendation_id for row in bq_client.query(query).result()}
    return [r for r in rows if r["recommendation_id"] not in existing]

비슷한 패턴으로 _dedup_news (gid 기준), _dedup_price_history ((app_id, snapshot_date) 기준), _append_review_summary (KST 같은 날 app_id 기준) 헬퍼들을 둡니다.

review_summary는 원래 매일 TRUNCATE 후 재적재 방식이었는데, AI 코멘트의 4주 추세 분석을 위해 일별 누적 모드로 변경했습니다. (포스트 5에서 자세히)


5. KST 기준 처리

Steam API는 보통 UTC Unix timestamp를 반환합니다. 한국 사용자 기준 분석이라 모든 수집·분석을 KST(Asia/Seoul) 전날 기준으로 통일했습니다.

from datetime import datetime, timezone, timedelta
KST = timezone(timedelta(hours=9))

# KST 전날 = (KST 자정 - 1초) 부터 거슬러 24시간
yesterday_end_kst = datetime.now(KST).replace(hour=0, minute=0, second=0)
yesterday_start_kst = yesterday_end_kst - timedelta(days=1)

# Steam API 비교는 Unix timestamp로
yesterday_start_ts = int(yesterday_start_kst.timestamp())
yesterday_end_ts = int(yesterday_end_kst.timestamp())

BQ 쿼리 측에서도 일관됩니다:

SELECT DATE(TIMESTAMP_SECONDS(timestamp_created), "Asia/Seoul") AS d
FROM reviews WHERE app_id = ...

6. 배포 — 단일 스크립트

5개 함수 + 5개 스케줄러를 한 번에 배포·갱신할 수 있는 deploy.sh를 작성했습니다. idempotent하게 동작해서 재배포가 부담스럽지 않습니다.

# 함수 + 스케줄러 모두
bash cloud_functions/deploy.sh

# 함수만
bash cloud_functions/deploy.sh functions

# 스케줄러만
bash cloud_functions/deploy.sh schedulers

스케줄러는 OIDC 토큰을 발급해서 Cloud Functions의 --no-allow-unauthenticated 보호 아래에서도 정상 호출되도록 구성했습니다.


7. 비용

GCP 무료 한도 안에서 운영 가능합니다.

항목 무료 한도 우리 사용량
Cloud Scheduler 월 3 잡 5 잡 (저렴)
Cloud Functions 월 200만 호출 일 ~50회 (1500/월)
BigQuery 저장 10GB < 1GB
BigQuery 쿼리 월 1TB 스캔 월 ~100GB (100쿼리/일 가정)

총 월 비용은 거의 $0. 신규 계정 $300 크레딧으로도 1년 가까이 운영 가능.


다음 포스트에서는 적재된 데이터의 BQ 구조와 분석 로직(번역·Kiwi·24축 카테고리 분류)을 다룹니다.