프로젝트 배경
처음에는 GameJob 공고를 놓치지 않기 위해 C# WPF 데스크톱 앱(트레이 상주 + 알림)으로 운영했다.
기능 자체는 잘 동작했지만, 실제 사용에서는 몇 가지 불편이 분명했다.
- 알림을 받으려면 앱이 켜진 PC가 항상 켜져 있어야 함
- 외부에서 모바일로 확인하기가 번거로움
- OS/실행 환경에 의존적인 관리 포인트가 늘어남
그래서 “앱을 켜두지 않아도”, “데스크톱/모바일 어디서나” 알림을 받을 수 있는 방식으로 구조를 바꿨다.
핵심 아이디어는 서버리스 폴링 + Discord Webhook 조합이다.
- Firebase Functions(2nd gen)가 5분마다 자동 실행
- Firestore에 이전 상태를 저장해 변경 감지
- 신규 공고(Added)만 Discord로 전송
결과적으로 클라이언트 앱 없이도, Discord만 켜져 있으면 동일한 알림 경험을 유지할 수 있게 됐다.
사용 기술 스택
- Runtime/Language: Node.js 20 + TypeScript
- Serverless: Firebase Functions v2 (
onSchedule) - Database: Firestore
- Crawling/Parsing: HTTP Fetch + Cheerio
- Notification: Discord Webhook
- Secret: Firebase Secret Manager
- Region/Cost Config:
asia-northeast3,256MiB,timeoutSeconds=60,maxInstances=1
핵심 기술 포인트
1. 상태 비교 기반 변경 감지
이번 프로젝트도 단순 목록 수집이 아니라, “이전 상태와 현재 상태의 비교”를 중심으로 설계했다.
- Added: 이번 실행에서 처음 발견된 공고
- Hidden: 이전에는 보였지만 이번엔 사라진 공고
- Restored: 숨김 상태였다가 다시 노출된 공고
이 구조 덕분에 스크래핑 결과를 단순 데이터가 아닌 이벤트로 해석할 수 있었다.
2. 스코프 분리(필터/URL 기반 해시)
동일한 사이트라도 필터가 다르면 결과 집합이 완전히 달라진다.
그래서 URL + 필터 조합을 해시해 scopeKey를 만들고, 스코프 단위로 상태를 분리 저장했다.
jobScopes/{scopeKey}: 스코프 메타jobScopes/{scopeKey}/jobs/{jobId}: 공고 상태
이렇게 하면 필터 변경 시 데이터가 섞이는 문제를 피할 수 있다.
3. 알림 노이즈 제어 + 중복 방지
알림 정책은 “Added만 전송”으로 단순화했다.
- Hidden/Restored는 저장만 하고 알림은 보내지 않음
notificationLedger로 idempotency key를 관리해 중복 전송 차단- 동일 공고에 대해 재실행/재시도 상황에서도 한 번만 전송되도록 보장
4. 운영성 중심 설계
서버리스는 실패를 전제로 운영해야 한다. 그래서 아래 항목을 기본으로 넣었다.
- 스케줄 중복 실행 방지 락(
pollLocks) - HTTP/Discord 전송 재시도(간단 백오프)
- 구조화 로그(
fetched,matched,added,hidden,restored,notified) - 비정상 종료 시에도 다음 주기에 복구 가능한 TTL 기반 락/상태 처리
구현 방법 요약
- Cloud Scheduler가 5분마다
pollGameJobs실행 - 스코프 락 획득 후 목록 HTML 수집
- Cheerio 파싱으로 공고 모델 정규화
- 필터(직종/경력/조건) 매칭
- Firestore 기존 상태와 diff 계산(Added/Hidden/Restored)
- 상태 upsert 저장
- Added만 idempotency 선점 후 Discord 전송
- 실행 메트릭/로그 기록 후 락 해제
알림 연결 방법
1. Discord Webhook
- Discord 채널 Webhook URL 발급
- Secret Manager에
DISCORD_WEBHOOK_URL저장 - Functions에서
defineSecret()으로 주입 받아 전송
하드코딩을 피해서 보안/운영성을 동시에 확보했다.
2. 메시지 포맷
신규 공고 알림은 아래 형식으로 고정했다.
[신규 공고] 제목
회사: ...
경력: ...
지역: ...
링크: ...
모바일에서도 빠르게 읽히는 형태를 우선했다.
3. 실패 처리
- 전송 실패 시 에러 로그 + ledger 상태
error로 기록 - 다음 실행에서 stale claim 기준으로 재시도 가능
- 함수가 실패해도 전체 상태 일관성은 유지되도록 분리 저장
배포/운영 포인트
1. 배포 구성
- Firebase Functions 2nd gen, 리전
asia-northeast3 - 스케줄:
every 5 minutes - 리소스 제한:
256MiB,60s,maxInstances=1
작게 시작하고 안정화하는 데 집중한 설정이다.
2. 비용 최적화
- 불필요한 고사양 인스턴스 사용 억제
- Added 중심 알림 정책으로 외부 호출 최소화
- Ledger TTL/인덱스 최적화로 Firestore 저장/인덱스 비용 관리
3. 관측성과 트러블슈팅
- 스코프 시작/완료 로그로 실행 흐름 추적
- 필터 실패 샘플 로그(
duty_mismatch,career_mismatch) 제공 - 운영 중 파싱 구조 변경 시에도 로그 기반으로 빠르게 원인 파악 가능
학습 포인트
이번 전환에서 가장 크게 얻은 포인트는 아래 4가지다.
- 데스크톱 자동화 문제도 서버리스 이벤트 파이프라인으로 단순화할 수 있다.
- 스크래핑 품질은 “파싱 정확도”보다 “상태 모델 + diff 설계”가 더 중요하다.
- 알림 시스템은 전송 성공보다 중복 방지와 노이즈 제어가 핵심이다.
- 운영 환경에서는 기능 구현보다 로그/복구/비용 설계가 장기 품질을 좌우한다.
마무리
이 프로젝트는 기존 WPF 알림 앱의 한계를 실제 사용 관점에서 다시 정의하고,
클라이언트 의존형 구조를 서버리스 이벤트 구조로 치환한 작업이었다.
핵심은 “수집”이 아니라 “판단(변화 감지)”이고,
완성도는 “알림 전송”이 아니라 “운영 가능한 구조”에서 나온다는 점을 체감했다.
다음 단계에서는 다중 스코프 운영, 알림 채널 다변화(예: Slack/Email), 대시보드화까지 확장할 수 있다.