5.5MB 규모의 Python 런타임을 브라우저로 배포하던 한 팀은 최근 스프린트 동안 기록된 오류의 69%가 하나의 오해의 소지가 있는 제목으로 분류되었으며, 그중 89%가 실제로는 네트워크 타임아웃이라는 사실을 발견했습니다. 잘못된 보고는 개발자들을 잘못된 디버깅 경로로 이끌었고, 상당수의 사용자가 다운로드 실패를 인지하지 못하는 상황을 초래했습니다. 이는 대용량 에셋을 번들링하는 모든 웹 앱에서 곧 겪을 수 있는 문제입니다.
대시보드가 오도했다
오류 추적 시스템은 오류가 처음 발생하는 코드 위치를 기준으로 인시던트를 자동으로 그룹화합니다. 그 결과 생성된 제목은 런타임 로더의 단순한 버그처럼 보였고, 팀은 타임아웃이 발생하지 않는 코드 경로를 찾는 데 스프린트 시간을 허비했습니다. 팀이 기반 메타데이터를 샘플링했을 때 진실이 드러났습니다. 대부분의 실패는 버그가 아니라 타임아웃을 유발한 네트워크 연결 지연이었습니다.
교훈: 오류 제목은 편의를 위한 것일 뿐, 진단 결과가 아닙니다. 헤드라인이 실제로 무엇을 나타내는지 확인하기 위해 주기적으로 로우 데이터(raw data)를 심층 분석하십시오.
브라우저 연결 API가 플레이스홀더를 제공했다
속도가 느린 사용자가 5.5MB를 다운로드하느라 고생하는 것을 방지하기 위해, 개발자들은 브라우저의 Network Information API(navigator.connection)를 참고했습니다. 이 API는 모든 첫 방문자에게 1.7 Mbps의 일정한 대역폭을 보고했습니다.
브라우저는 새로운 사용자에 대한 과거 데이터가 없을 때 기본값을 내보냅니다. 이 기본값은 힌트일 뿐, 확정적인 속도가 아닙니다. 모든 새로운 세션에서 동일한 플레이스홀더가 나타난다면, 이는 해당 API가 아직 해당 사용자층에 맞춰 조정(calibrate)되지 않았음을 의미합니다.
교훈: 전혀 변하지 않는 네트워크 신호는 확정적인 지표가 아닌 폴백(fallback)으로 취급하십시오.
일회성 스냅샷은 신뢰할 수 없다
신뢰할 수 없는 대역폭 힌트를 버린 후, 팀은 테스트 스위트에서 잘 작동하는 것처럼 보이는 다른 신호로 전환했습니다. 한 번의 테스트 실행은 통과했지만, 테스트를 세 번 반복하자 매번 실패가 발생했습니다. 네트워크 속도는 지속적으로 변동합니다. 기존 코드는 단 한 번의 스냅샷을 찍어 영구적인 결정을 내린 뒤, 잠시 후 연결 상태가 바뀌더라도 그대로 진행해 버렸습니다.
교훈: 움직이는 타겟에 대한 단 한 번의 측정값으로 영구적인 동작을 결정하지 마십시오. 한 번만 확인(polling)하는 대신 변경 이벤트(change events)를 구독하십시오.
팀이 구현한 실질적인 해결책
- 연결 변경 사항 구독.
navigator.connection을 한 번만 읽는 대신, 이제 코드는change이벤트를 감지하여 다운로드 중에 대역폭이 떨어지거나 상승하면 이에 대응합니다. - “진행 없음” 와치독(watchdog) 추가. 타이머를 사용하여 짧은 간격 동안 진전이 없는 요청을 중단시키고, 브라우저가 재시도하거나 폴백할 수 있도록 합니다.
- 다운로드 도중 CDN 전환 중단. 느린 연결 상태에서 대용량 파일의 소스를 전환하면 전송이 처음부터 다시 시작되어 이미 받은 바이트를 낭비하게 됩니다. 이제 다운로드는 전체 과정 동안 처음에 선택한 CDN을 유지합니다.
- 무거운 캐싱 작업 지연. 대량의 데이터를 캐시에 쓰는 작업은 런타임 로딩이 완료된 후로 미루어 크리티컬 패스(critical path)를 짧게 유지합니다.
만약 대시보드가 지나치게 깔끔한 모습만 보여준다면, 더 깊이 파고드십시오. 네트워크 측정값이 전혀 움직이지 않는다면 플레이스홀더로 취급하십시오. 그리고 단 한 번의 스냅샷이 수 메가바이트 규모의 다운로드 운명을 결정한다면, 당신은 신기루에 도박을 걸고 있는 것입니다. 그러한 도박은 사용자의 신뢰를 갉아먹는 '조용한 실패'로 나타나며, 이는 사후에 아무리 영리한 코드를 작성하더라도 완전히 복구할 수 없는 문제입니다.
