Case Study · 09

터지지 않는 회의실 — 런타임 가드와 자원 해제 캠페인

장시간 회의·재연결·반복 입퇴장 시 산발적으로 발생하던 메모리 누수와 stale closure 크래시를, ErrorBoundary와 라이프사이클 정책 분리, 콘솔 가드 일괄 적용, 메모리 누수 4종 동시 정리로 끝까지 닫았습니다.

Product
MeetMate (Mobile)
Role
Frontend Engineer
Period
2026.02 — 2026.03
Stack
React Native 0.77 React ErrorBoundary AppState / BackHandler Zustand subscriptions
Implementation AI-paired (Claude agent)
  • 메모리 누수 4종을 한 PR에 동시 수정. 타임아웃 추적, 구독 해제, BackHandler 정리, 타이머 클리어를 묶음.
  • `__DEV__` 콘솔 가드를 166개 호출, 55개 파일에 일괄 적용. 프로덕션 stdout 비움.
  • 오디오 레벨 리스너 누수 제거. 초당 ~5,000회 이벤트 디스패치 차단 (100명 × 초당 50회 기준).
  • ErrorBoundary를 App 트리 최상위에. 라이프사이클은 7.5분 백그라운드 후 자동 disconnect 정책으로 분리.
  • 미사용 의존성 4종 제거. package-lock 라인 -872, 빌드 시간도 같이 줄어듦.

배경

기능이 다 들어온 다음부터 운영 중 깨지는 자리가 보이기 시작했습니다. 장시간 회의 끝에 메모리가 천천히 새고, 백그라운드 복귀 후 재연결 시점에 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.parse try-catch · Number() NaN 가드 · Promise.all 순서 보장 등 런타임 크래시 가드 5종 추가.

배운 점

안정성 작업의 어려운 점은 결과가 보이지 않는다는 점입니다. 잘하면 잘할수록 사용자에게는 아무 일도 일어나지 않습니다. 그래서 깨졌던 자리의 이름을 PR과 커밋 메시지에 남겨 두는 일이 중요했습니다 — “메모리 누수 4종”이라고 적어 두는 순간, 그 4종이 다시 같은 방식으로 깨질 수 없는 기록이 됩니다.