[언더스코어] 21대 대선토론 실시간 팩트체크

2025/05/13 ~ 2025/05/28배포 링크

역할 프론트엔드, 스트리밍 서버, UI 디자인 (1인 / 언더스코어 외주 / 백엔드 2인과 협업)

요약 실시간 스트림을 서버(Hono)부터 React까지 RxJS 단일 패러다임으로 설계함. 라이브 중 피크 트래픽에 대응해 인스턴스를 확장함.

프로젝트 개요

대선토론 라이브의 음성인식 스크립트와 팩트체크 결과를 실시간 제공하는 서비스입니다. 프론트엔드와 스트리밍 기능, UI 디자인(Figma)까지 담당했습니다.

구현 화면

데스크탑 메인 화면
데스크탑 메인 화면
팩트체크 카드 - 상세보기 모달
팩트체크 카드 - 상세보기 모달
모바일 화면과 컨트롤러, 유튜브 임베드
모바일 화면과 컨트롤러, 유튜브 임베드

주요 구현

  • 음성인식 스크립트팩트체크 카드를 폴링해 추가된 부분을 감지, 이벤트 스트림을 통해 전송합니다.

  • RxJS로 클라이언트와 서버의 데이터 로직을 모두 작성했습니다. (폴링, 이벤트 스트림, 배치 처리…)

  • react-window로 크기가 수천인 배열이 이벤트 스트림으로 인해 계속해서 리렌더될 때의 최적화를 진행했습니다.

주요 설계 포인트

1. 폴링 주기 결정

유튜브 화면과의 싱크를 위해 원본 스크립트의 갱신 주기를 살펴 결정했습니다. 원본 스크립트는 1~3초 간격으로 불규칙하게 갱신되었습니다. 0.5초 폴링은 갱신 시점과 어긋나도 갱신을 위한 재요청까지 0.5 초 미만이 걸립니다. 이 폴링은 동시 접속자가 아무리 많아도 1회씩만 수행하고 브라우저에는 스트림으로 전달하기에, 시청자가 수천 명으로 늘어도 원본 스크립트를 가져오는 부하는 하나로 유지됩니다.

2. RxJS를 서버부터 React까지 사용한 이유

서버에서 폴링 결과의 초기값과 증분량을 따로 두어 각각을 스트림으로 전달하고, 클라이언트에서 같은 어휘로 받아 React까지 연결했습니다.

데이터 생성 → 전송 → 가공 → 렌더의 전 구간을 하나의 패러다임으로 작성해 데이터의 흐름을 잘 파악할 수 있었고, Observable의 특성상 다른 라이브러리를 사용하지 않고 React와 쉽게 통합이 가능했기 때문입니다.

3. 라이브 스트림과 스크롤

항목이 초 단위로 추가되는 리스트에서 "마지막 항목이 뷰포트에 보이는가"로 유저가 과거의 스크립트와 카드를 보고 있는지, 실시간으로 팔로우하고 있는지를 판단했습니다.

사용자가 위로 스크롤해 읽는 중이면 자동 스크롤을 멈추고, 최하단 이동 버튼을 제공했습니다. react-window의 스크롤 및 뷰포트 API를 이용해 컨트롤했습니다.

4. 브라우저에서 왜 폴링이 아니라 SSE?

초기값과 증분량을 각각 전송해 주는 Rest API를 이용해도 똑같이 구현할 수 있지만, SSE의 경우 push 방식이기 때문에 실제로 스크립트가 업데이트되었을 때만 보내줄 수 있다는 장점이 있습니다. 아래와 같은 단점들 때문에 SSE로 구현했습니다:

  1. 자원 낭비: 브라우저에서 0.5초마다 폴링한다고 할 때, 업데이트되지 않은 상황에서도 요청과 응답이 오가게 됩니다.

  2. 실시간성: 같은 가정에서 스크립트의 싱크 오차가 커집니다. 서버의 json 폴링 오차 ≤0.5초, 브라우저의 폴링 오차 ≤0.5초가 합산되어 ≤1초로 늘어납니다.

다만 SSE의 경우 연결 유지 비용이 있기 때문에 서버리스 환경에서는 그대로 사용하기 어렵고, 이 프로젝트의 경우 서버리스 환경이 아니었기 때문에 SSE로 결정한 면도 있습니다. (실제 라이브 중 접속 급증 시 인스턴스 확장으로 대응했습니다)

성과 및 러닝 포인트

  • 실시간 서비스를 모니터링하며 장애 및 피드백에 대응해본 경험

  • 라이브 진행 (8:00PM) 부터 종료 30분 후(10:30PM)까지, 사이트 트래픽 약 800/min (05/23)

  • 토론 중간부터 트래픽이 급증(트위터에도 같이 팩트체크 보드를 업로드한 효과로 추정)해, 인스턴스 확장으로 대응

피크 리퀘스트 수 4.54K (9:16 PM)
피크 리퀘스트 수 4.54K (9:16 PM)