배경
회의 인원이 50–100명까지 늘면서 RN 클라이언트가 흔들리기 시작했습니다. 입장 폭풍이 발생할 때마다 페이지 전환에서 검은 화면이 길게 잡혔고, mute 한 번에 화면 전체가 리렌더됐으며, 핫패스에서 분당 100회+ 불필요 리렌더가 관찰되는 상태였습니다.
문제를 더 어렵게 만들었던 건 트랙 이벤트가 멤버 전체를 선형 탐색하는 O(n²) 구조였다는 점입니다. 100명 입장 = 100개 트랙 × 100명 비교 = 10,000회 비교가 모든 입장 이벤트마다 반복되고 있었습니다.
접근
먼저 자료구조 모양을 바꾸는 일부터 시작했습니다. 멤버 store와 트랙 store에 각각 보조 Map 인덱스를 추가해, 자주 호출되는 룩업을 O(n)에서 O(1)로 전환했습니다.
리렌더 쪽은 전체 스토어 구독을 셀렉터 단위로 쪼갰습니다(8곳). 영상·헤더·푸터·채팅·참가자 5개 주요 컴포넌트에 React.memo를 걸고, 필요한 곳은 커스텀 비교 함수로 prop 동등성을 명시했습니다.
입장 100개를 개별
addTrack콜백 100번으로 받는 대신, 마이크로배치로 묶어 1–2회의 호출로 전달하게 했습니다. 한 프레임 안에 끝나는 일이 되도록.
페이지 전환의 검은 화면은 두 가지로 풀었습니다. SFU lastN을 인원 구간별로 재설계(≤9명 720p, 26명 이상 180p)하고, 현재 페이지뿐 아니라 이전과 다음 페이지의 저해상도 트랙도 양방향으로 프리페치했습니다. 전환 debounce는 250ms → 100ms로 줄여 체감 응답을 150ms 단축했습니다.
결과
- 100명 입장 시 핫패스 비교 횟수 10,000회 → 100회.
- 입장 폭풍 처리량: 개별
addTrack100회 → 배치 호출 1–2회. - 멤버·트랙 룩업 함수가 모두 O(n) → O(1) 로 전환.
- 페이지 전환 검은 화면 → 양방향 프리페치 후 제거. debounce 250 → 100ms로 응답 150ms 단축.
- mute / 카메라 토글 시 리렌더 범위 — 화면 전체 → 해당 영상 셀 하나로 축소.
배운 점
성능 작업에서 가장 큰 교훈은 측정 가능한 자기 증명을 만들어 두는 것이었습니다. “느려요”는 검증 불가능한 보고지만, “비교 횟수 10k → 100”은 다음 사람이 회귀를 잡을 수 있는 기준점입니다. 핫패스마다 알고리즘 복잡도를 코멘트로 박아 두는 습관이 이때 생겼습니다.
또 한 가지: RN 환경에서는 자료구조 한 단계 → 메모이제이션 한 단계 → 자원 정책 한 단계 가 같은 무게로 묶여야 효과가 납니다. Map 인덱스만으로는 리렌더가 살아 있고, React.memo만으로는 핫패스가 살아 있으며, lastN만으로는 클라이언트 비용이 살아 있습니다. 세 층을 같이 흔들지 않으면 어느 하나도 충분치 않다는 걸 이 작업에서 확인했습니다.