지금까지는 채널을 클릭할 때마다 프론트에서 /channels/:id/sync를 불렀다. 화면을 열 때마다 API가 YouTube Analytics를 긁으러 갔다는 뜻이다.
전체 활성 채널을 한 번에 동기화하는 엔드포인트를 새로 팠다.
async syncAll(): Promise<Array<{ channelId: string; ok: boolean; analyticsError?: string; error?: string }>> {
const channels = await this.repo.findAllActiveIds();
log.info({ count: channels.length }, '전체 채널 Analytics 동기화 시작');
const results = await Promise.allSettled(
channels.map((ch) => this.syncChannel(ch.id)),
);
return channels.map((ch, i) => {
const r = results[i]!;
if (r.status === 'fulfilled') {
return { channelId: ch.id, ok: true, ...(r.value.analyticsError ? { analyticsError: r.value.analyticsError } : {}) };
}
return { channelId: ch.id, ok: false, error: r.reason?.message ?? String(r.reason) };
});
}
Promise.allSettled로 묶었다. 채널 하나가 실패해도 나머지 채널 동기화는 계속 진행돼야 한다.
라우트 등록 순서도 신경 썼다. @Post('sync-all')을 @Post(':id/sync-videos')보다 위에 둬야 한다. 안 그러면 sync-all이 :id 파라미터로 먹혀버린다.
scheduler-worker는 원래 channelId가 담긴 페이로드만 받았다. 여기에 타입 분기를 하나 추가했다.
export const handler: ScheduledHandler = async (event) => {
const payload = event as unknown as { channelId?: string; type?: string };
if (payload.type === 'daily-analytics-sync') {
await runDailyAnalyticsSync();
return;
}
const channelId = payload.channelId;
...
runDailyAnalyticsSync는 그냥 API의 sync-all을 내부 시크릿으로 호출하는 얇은 함수다.
Terraform으로 매일 KST 06:00에 이 워커를 트리거하는 규칙을 추가했다.
resource "aws_cloudwatch_event_rule" "daily_analytics_sync" {
name = "shorts-daily-analytics-sync"
description = "매일 KST 06:00 전체 채널 Analytics 동기화"
schedule_expression = "cron(0 21 * * ? *)"
}
resource "aws_cloudwatch_event_target" "daily_analytics_sync" {
rule = aws_cloudwatch_event_rule.daily_analytics_sync.name
target_id = "scheduler-worker"
arn = "arn:aws:lambda:ap-northeast-2:682251233572:function:shorts-scheduler-worker-prod-handler"
input = jsonencode({ type = "daily-analytics-sync" })
}
cron(0 21 * * ? *)는 UTC 21시, 즉 KST 06시다. 채널별 EventBridge 규칙에 Lambda invoke 권한을 이미 wildcard로 열어놨던 덕분에 별도 permission 리소스는 안 붙여도 됐다.
이제 웹에서 채널을 열 때마다 sync를 부를 이유가 없다. HomeClient와 ChannelClient에서 마운트/채널변경 시 쏘던 apiPost(.../sync) 호출을 걷어냈다.
- // 채널 변경 시 sync 호출 + 한도 상태 초기화
+ // 채널 변경 시 한도 상태 초기화
useEffect(() => {
if (!activeChannelId) return;
setDailyLimitHit(false);
setSubmitError(null);
- apiPost(`/channels/${activeChannelId}/sync`, {}, userHeaders)
- .then(() => queryClient.invalidateQueries({ queryKey: ['jobs', activeChannelId] }))
- .catch(() => {});
- }, [activeChannelId, queryClient]);
+ }, [activeChannelId]);
화면 열 때마다 API를 두드리던 구조에서, 하루 한 번 배치로 도는 구조로 바뀌었다. 데이터가 최대 하루 늦어질 순 있지만 그 정도는 감수할 만하다.