배경
기능이 다 들어온 다음부터 운영 중 깨지는 자리가 보이기 시작했습니다. 장시간 회의 끝에 메모리가 천천히 새고, 백그라운드 복귀 후 재연결 시점에 stale closure가 리스너를 누적시키고, WebSocket이 비정상 페이로드를 받을 때 JSON.parse가 던지는 예외가 그대로 화면을 죽이는 일이 산발적으로 발생했습니다.
문제는 어디서 시작해야 하는지가 모호하다는 점이었습니다. 한 번에 다 잡으려고 하면 끝이 없었고, 한 자리만 잡으면 다른 자리가 다시 났습니다.
접근
먼저 그물부터 던졌습니다. ErrorBoundary를 앱 트리 최상위에 두어, 예외가 화면을 죽이는 대신 fallback으로 떨어지도록 했습니다. 그 다음 라이프사이클 정책을 한 곳에 모았습니다 — 라이프사이클 매니저 싱글톤이 7.5분 백그라운드 후 자동으로 연결을 끊고, 네트워크 모니터가 재연결을 책임지도록.
누수 자체는 원인 4개를 한 PR에 동시에 잡았습니다. 연결 서비스의 타임아웃을 추적 가능하게 만들고, 스토어 구독의 타임아웃을 명시적으로 해제, BackHandler 구독을 컴포넌트 unmount 시 해제, 알림 스토어의 타이머를 회의 leave 시 클리어.
트랙 객체에 핸들러와 타임아웃을 직접 저장해 두는 식으로, 해제 시점에 정확히 같은 참조를 다시 잡을 수 있게 만들었습니다.
마지막으로 코드 전체의 콘솔 노이즈를 정리했습니다. 166개 console.* 호출에 __DEV__ 가드를 일괄 적용해 프로덕션에서 stdout이 비어 있게 했고, 같은 커밋에서 미사용 의존성 4종을 제거해 package-lock을 872라인 줄였습니다.
결과
__DEV__가드 적용: 166개 console 호출, 55개 파일.- 메모리 누수 4종 동시 수정 (타임아웃 추적 · 구독 해제 · BackHandler 정리 · 타이머 클리어).
- 오디오 레벨 리스너 누수 제거 — 초당 5,000회 이벤트 디스패치 차단 (100명 × 초당 50회 기준).
- 미사용 의존성 4종 제거 → package-lock −872 라인.
- WebSocket
JSON.parsetry-catch ·Number()NaN 가드 ·Promise.all순서 보장 등 런타임 크래시 가드 5종 추가.
배운 점
안정성 작업의 어려운 점은 결과가 보이지 않는다는 점입니다. 잘하면 잘할수록 사용자에게는 아무 일도 일어나지 않습니다. 그래서 깨졌던 자리의 이름을 PR과 커밋 메시지에 남겨 두는 일이 중요했습니다 — “메모리 누수 4종”이라고 적어 두는 순간, 그 4종이 다시 같은 방식으로 깨질 수 없는 기록이 됩니다.