무료 게임 소식은 이상하게도 찾기 쉽지 않은 곳에 숨어 있습니다. 매주 목요일마다 Epic을 한번씩 확인하고, 어쩌다 GamerPower에서 재밌는 딜을 발견하고, FreeToGame에서 F2P를 뒤적이다가… 결국은 “아 맞다, 그거 어제까지였지”로 마무리되는 일이 반복됐습니다. 그때 들었던 생각은 딱 하나였습니다.
무료 게임 정보가 흩어져 있으니, 내가 쓰기 편한 형태로 한 번에 모아두자.
그래서 만든 앱이 게임 득템 헌터(Game Loot Hunter) 입니다.
앱 소개 페이지(Landing page)
다운로드
- Android: https://play.google.com/store/apps/details?id=com.archmond.freeGameRadar
- iOS: https://apps.apple.com/kr/app/game-loot-hunter/id6755240774

문제 정의: 정보는 많은데, 확인 비용이 크다
무료 게임/혜택은 한 군데에서 끝나지 않습니다.
- Epic Games Store: 주간 무료(기간 한정)
- GamerPower: 다양한 기브어웨이/프로모션
- FreeToGame: F2P 목록
- Luna(Amazon Prime Gaming): Prime 구독자라면 놓치기 쉬운 추가 혜택
문제는 “데이터가 없다”가 아니라, 확인 루틴이 번거롭다는 점이었습니다. 그래서 목표를 이렇게 잡았습니다.
- 여러 소스를 한 화면에 모으기
- 검색/필터로 탐색 비용 줄이기
- 알림으로 “놓침” 자체를 줄이기
- 그런데 운영은… 가능하면 가볍게 유지하기
방향 선택: 푸시/DB 없이도 되는 구조로
처음엔 흔히 떠오르는 구성이 있었습니다.
- DB에 저장해두고
- FCM 같은 푸시로 알리고
- 캐시는 Redis로 분리하고
- 운영은 Docker로 정리하고…
그런데 개인 프로젝트에선 이 방식이 곧장 운영 복잡도로 돌아옵니다. 그래서 이번엔 반대로 갔습니다.
- DB 없이 (정규화/영속 저장을 포기)
- Redis 없이 (인메모리 TTL 캐시)
- 푸시 없이 (로컬 알림)
- 단일 Node 프로세스로 운영
대신 사용자가 체감하는 품질은 지키고 싶어서,
- 서버는 6시간마다 자동 갱신(하루 4회)
- 응답은 캐시에서 빠르게
- 앱은 당겨서 새로고침으로 언제든 갱신 가능
앱이 하는 일
앱에서 사용자가 할 수 있는 일은 단순합니다.
- 오늘/이번 주 무료 게임을 한 화면에서 보기
- Epic / GamerPower / FreeToGame / Luna 로 소스 필터
- 제목/키워드로 검색
- 카드/그리드/리스트 보기 전환
- 하루 1번, 원하는 시간에 새 무료 게임 알림
- 링크 공유
그리고 Luna는 Prime 구독이 필요한 경우가 있어, 헷갈리지 않도록 “무료(구독자 한정)” 라벨을 붙였습니다.
구현에서 가장 중요했던 것: “표준화 + 중복 제거”
여러 소스를 한 화면에 섞는 순간부터 진짜 일이 시작됩니다.
- 소스마다 필드가 다릅니다(제목/이미지/기간/링크 형태)
- 같은 게임이 여러 소스에 동시에 떠서 중복이 생깁니다
- 소스 하나가 터져도 전체가 멈추면 안 됩니다
그래서 백엔드에서 아래 3가지를 중심으로 정리했습니다.
- Deal 표준화: 소스별 데이터를 앱이 쓰기 좋은 공통 구조로 변환
- 중복 제거: 제목/링크 등 기준으로 최대한 겹침을 줄임
- 실패 격리: 특정 소스가 실패해도 나머지는 정상 제공
기술 스택: 왜 이 조합을 골랐나
Flutter(앱)
한 번의 코드로 iOS/Android를 동시에 가져가고 싶었습니다. UI/상태/라우팅은 안정적으로 검증된 조합을 택했습니다.
- Flutter
- Riverpod
- go_router
- Dio
- SharedPreferences(보기 모드/설정 유지)
- flutter_local_notifications(로컬 알림)
- share_plus(공유)
- flutter_localizations(다국어)
Node.js + Fastify(백엔드)
백엔드는 “가볍고 빠르게”가 목표였습니다. Fastify는 성능/플러그인 생태계(helmet, rate-limit) 면에서 잘 맞았고, TypeScript로 수집/정규화 로직의 안정성을 끌어올렸습니다.
- Node.js 20+
- Fastify
- TypeScript
- node-cron(6시간 주기 갱신)
- Zod(스키마 검증)
- cheerio(HTML 파싱)
- (소스에 따라) Puppeteer 기반 수집
- Jest(테스트)
만들면서 부딪힌 포인트들
1) 스크래핑은 생각보다 잘 깨진다
API가 없는 소스는 HTML 스크래핑이 필요했습니다. 문제는 “지금은 된다”가 “내일도 된다”를 보장하지 않는다는 점입니다. 그래서 전략을 이렇게 잡았습니다.
- 스크래핑은 어댑터로 격리(깨져도 영향 범위 최소화)
- 캐시/스케줄러로 외부 호출 빈도 줄이기
- 소스 하나가 실패해도 전체 API가 실패하지 않게 하기
2) 알림은 ‘푸시’보다 ‘로컬’이 현실적
푸시 알림은 강력하지만, 서버/토큰/권한/운영을 함께 가져옵니다. 이번 프로젝트에서는 “놓치지 않기”라는 목적에만 집중하고 싶어서 하루 1회 로컬 알림으로 충분하다고 판단했습니다. 대신 알림 시간은 사용자가 바꿀 수 있고, iOS/Android 권한 요청 흐름을 앱에서 처리하도록 했습니다.
3) “빠름”은 결국 캐시에서
여러 소스를 실시간으로 매번 호출하면 느리고 불안정해집니다. 그래서 백엔드는 인메모리 TTL 캐시로 응답을 고정시키고, 갱신은 스케줄러가 담당하도록 분리했습니다.
4) 다국어는 초반에 넣는 게 싸다
나중에 붙이면 화면마다 문자열을 뜯어고치게 되니, 초반부터 한국어/영어/일본어를 같이 염두에 두고 구성했습니다.
5) Luna 수집은 ‘비싼 작업’이었다: 크롤링 비용을 캐시로 흡수
Luna(Prime Gaming)는 API가 아니라 크롤링 성격이 강하다 보니, “그때그때 새로 가져오자”로 가면 곧장 서버 부담으로 이어질 수 있었습니다. 그래서 저는 방향을 이렇게 정했습니다.
- Luna는 기본적으로 하루 1회 수준으로만 갱신(캐시 TTL을 길게)
- 스케줄러가 갱신을 담당하고, API는 캐시를 읽는 쪽에 집중
- 캐시가 아직 유효하면 기존 값을 재사용해서 “불필요한 크롤링”을 줄이기
결과적으로 사용자는 느려짐 없이 목록을 보고, 서버는 안정적으로 유지되는 쪽으로 균형을 맞출 수 있었습니다.
6) Region(지역)은 Epic만 다르게 가져가고, 나머지는 재사용
처음엔 “region 파라미터가 있으면 모든 소스를 지역별로 따로 캐시해야 하나?” 고민이 있었습니다. 그런데 그렇게 하면 지역 수가 늘어날수록 전체 수집 비용이 선형으로 커집니다. 그래서 실제 특성에 맞춰 전략을 나눴습니다.
- Epic은 region별 무료 구성이 달라질 수 있어 region별 캐시
- 그 외 소스는 global 캐시를 재사용해서 region 수가 늘어도 수집 비용이 폭증하지 않게
로드맵(다음은 무엇을 할까)
- 오프라인 캐싱: 마지막으로 본 목록을 네트워크 없이도 확인
- 웹 버전: 가볍게 브라우저에서도 접근
- 리뷰/평점(또는 메모): 관심도 기반 필터링 고도화
누가 쓰면 좋은가
- “주간 무료”를 놓치기 싫은 라이트 게이머
- 여러 소스를 돌아다니기 귀찮은 사람
- Prime/Luna 혜택이 있는데 자주 까먹는 구독자
- 세일/기브어웨이를 즐기는 딜 헌터
예상 피드백(그리고 제가 하고 싶은 개선)
- 알림 빈도 옵션이 필요하다 → 주 1회/매일/커스텀 옵션 검토
- 더 많은 소스가 있으면 좋겠다 → 정책/안정성/유지비를 보고 선별적으로 확장
- 지역(Region)별 차이가 보인다 → 지역 설정/표시 고도화
- 즐겨찾기/위시리스트가 필요하다 → 북마크 + 만료 알림
- 중복이 가끔 보인다 → 중복 제거 로직 튜닝(제목/기간/URL 기준 개선)
마무리: 이 프로젝트에서 얻은 것
게임 득템 헌터는 “무료 게임을 놓치지 않게 해주는 앱”이기도 하지만, 제게는 ‘최소 인프라로도 운영 가능한 제품 형태’를 끝까지 밀어붙여보는 실험이었습니다. 직접 써보시고 여기서 막혔다 / 이건 진짜 편했다 같은 피드백을 주시면, 다음 업데이트 우선순위를 정하는 데 큰 도움이 됩니다.
댓글 하나
핑백: 앱(Apps) – 아크몬드