
연결이 끊겼다고 앱 전체가 죽어서는 안 된다 — 그런데 우리 앱은 죽었다
백엔드가 회의 도중에 오프라인이 됐을 때, 그것은 단지 전사를 멈춘 게 아니었습니다 — 앱 전체를 죽였습니다. 원인은 처리되지 않은 단 하나의 이벤트였고, 수정은 열 줄이었습니다. 이것은 GeekBye가 예전에 자신을 죽이던 상황들을 통과하면서도 로그인을 유지하고 연결을 유지하도록 만든 릴리스 묶음의 이야기입니다.
어떤 버그는 하나의 기능을 방해합니다. 최악의 버그는 그 주변의 모든 것을 무너뜨립니다. GeekBye의 초기 빌드에서, 당신이 회의 한복판에 있을 때 백엔드가 오프라인이 되면 전사는 단지 멈추는 게 아니라 — 앱 전체가 크래시했습니다. 연결 하나가 끊겼을 뿐인데, 창 전체가 사라졌습니다.
이것을 고친 릴리스 묶음 — v1.6.8부터 v1.6.11까지 — 은 "로그인 유지, 연결 유지"라는 화려하지 않은 작업의 걸작입니다. 대표 수정은 열 줄이었습니다. 그 열 줄이 무엇이었는지, 그리고 그것과 나란히 출시된 나머지 모든 것을 여기 적습니다.
끊김과 크래시 사이의 열 줄
크래시는 오디오 코드 속에 숨어 있었습니다. 전사 세션이 자신의 WebSocket을 해체할 때, 그것들에 대해 removeAllListeners()를 호출했습니다 — 합리적인 정리입니다. 하지만 여기, 정리를 재앙으로 바꾸는 Node.js의 세부 사항이 있습니다: 리스너가 없는 error 이벤트는 무시되지 않습니다. 잡히지 않은 예외로 다시 던져집니다.
그러니 백엔드가 오프라인이 되는 순간의 순서를 그려 보세요: 소켓에는 발화를 기다리는 error 이벤트가 큐에 쌓여 있습니다. 정리가 removeAllListeners()를 실행해 그것을 잡았을 핸들러를 벗겨냅니다. 잠시 후 큐에 쌓인 에러가 발화합니다 — 허공으로. Node는 아무도 듣고 있지 않은 error 이벤트를 보고, 자신의 계약이 허락하는 유일한 일을 합니다: 그것을 잡히지 않은 예외로 던지고, 프로세스를 크래시시킵니다. 사용자의 백엔드가 2초 딸꾹질했을 뿐인데 그의 앱은 사라졌습니다.
수정(v1.6.8)은 김이 빠질 만큼 간단합니다. 리스너를 벗겨낸 직후, 의도적인 아무 일도 하지 않는 error 핸들러를 다시 붙입니다:
this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events
그 빈 함수가 핵심의 전부입니다. 대기 중인 에러가 착지할 곳 — 싱크 — 을 주므로, Node에는 리스너가 있고 결코 다시 던지지 않습니다. 같은 커밋은 또한 연결 시도가 도중에 실패했을 때 반쯤 열린 소켓도 정리했으니, 잘못된 연결이 error 발화를 기다리는 살아 있는 소켓을 매달아 둘 수 없게 되었습니다. 커밋 코멘트는 분명히 말합니다: "이것이 없으면, 대기 중인 에러는 잡히지 않은 예외가 되어 앱을 크래시시킨다." 열 줄이 완전한 크래시를 조용하고 회복 가능한 끊김으로 바꿨습니다.
교훈은 수정 그 자체보다 값집니다: 아직 해체하는 중일지도 모르는 소켓이나 스트림에 대해 removeAllListeners()를 호출할 때는 언제나, 먼저 아무 일도 하지 않는 error 싱크를 다시 붙이세요. 처리되지 않은 error 이벤트는 로그 한 줄이 아니라 크래시입니다.
로그인 유지: 미리 갱신하고, 튕겨 나올 때도 갱신한다
"연결 유지"의 절반에는 "로그인 유지"라는 쌍둥이가 있었습니다. 이 묶음 이전에는 GeekBye의 인증 토큰이 그저 만료되었습니다 — 한동안 지나면, 워크플로 도중에, 당신이 볼 수 있는 아무 이유도 없이, 무례하게 로그인 화면으로 내던져졌습니다.
v1.6.8은 이것을 양방향에서 고쳤습니다:
- 선제적: 타이머가 토큰을 만료 전 에 갱신합니다. 만료에서 똑똑한 버퍼(토큰 수명의 일부로, 하한과 상한이 정해진)를 뺀 시점에 예약됩니다. 당신이 눈치채기도 전에 갱신됩니다.
- 반응적: 그래도 요청이
401로 돌아오면 — 시계 오차, 놓친 타이머, 차가운 노트북 — 클라이언트는 한 번 갱신하고 원래 요청을 정확히 한 번만 재시도합니다. 단일 가드 플래그가 그 재시도에 상한을 걸어, 끈질긴 401이 결코 무한 루프로 돌 수 없게 합니다.
미묘하지만 결정적인 부분: 두 경로 모두 뮤텍스 뒤에 있습니다. 열 개의 요청이 같은 순간에 401을 맞아도, 열 번의 갱신을 일으키지 않습니다 — 그것들은 모두 진행 중인 하나의 갱신을 기다렸다가 재시도합니다. 그리고 갱신이 정말로 구제될 수 없을 때, 앱은 세션을 우아하게 만료시킵니다 — 명확한 10초짜리 "세션이 만료되었습니다. 다시 로그인해 주세요" 토스트 — 로그인 화면으로의 말없는 내쫓김이 아니라.
재연결: 클라이언트에 추가하고, 그다음 백엔드로 옮긴다
v1.6.9은 끊긴 전사 연결을 치명적인 것이 아니라 회복 가능 하게 만들었습니다. 백엔드가 상류의 음성 연결이 끊겼다고 보고하면, 클라이언트는 치명적 에러를 겉으로 드러내기를 멈추고 대신 재연결했습니다 — 의도적으로 차분한 UX와 함께: 하나의 호박색 "재연결 중…" 토스트(시도마다 하나가 아니라), 돌아왔을 때의 초록색 "재연결됨" 토스트, 그리고 종단적인 "다시 시작해 주세요" 에러는 모든 시도를 소진한 뒤에만.
그리고 이 묶음에서 내가 좋아하는 것: 한 릴리스 뒤인 v1.6.10에서, 우리는 그 클라이언트 측 재연결 로직을 삭제했습니다 — 약 113줄 — 그리고 재연결을 백엔드로 옮겼습니다. 백엔드는 이제 조용한 "재연결 중" 상태 메시지를 내보내고, 클라이언트는 그것을 그저 반영합니다. 우리는 클라이언트에 회복을 만들고, 백엔드가 옳은 주인이라고 판단해, 그것을 옮겼습니다. 이것은 우유부단함이 아닙니다. 어려운 문제의 정직한 생애 주기입니다. 안정성 로직이 연결의 양쪽 끝에 나뉘어 있는 것은 나쁜 냄새입니다 — 주인을 고르세요. (백엔드는 결국 이것의 결정적 주인이 되었고, 그것이 AI 노트테이커는 왜 나쁜 Wi-Fi에서 멈추는가에서 이야기되는 재연결의 서사입니다.)
같은 릴리스는 녹음 시간 제한을 인간적으로 만들었습니다: 날것의 에러 대신, 녹음을 계속하게 해 주는 경고 토스트, 그리고 실제로 멈출 때의 명확한 "녹음 한도 도달" 메시지 — 친근한 문장을 겉으로 드러내고, 내부 접두사를 벗겨서.
모두를 다스릴 단 하나의 재시도
v1.6.11에 이르러, 같은 백오프 재시도 로직이 세 군데 서로 다른 곳에 복사-붙여넣기 되어 있었습니다 — 인증, 업데이트, 세션 해체입니다. 그래서 그것은 애초에 무엇이 재시도할 가치가 있는가 에 대한 공유된 하나의 규칙을 지닌 단일 모듈로 통합되었습니다: 일시적 네트워크 실패(연결 리셋, 타임아웃, DNS 딸꾹질, 소켓 끊김)는 지수 백오프로 재시도되고, 진짜 서버 에러는 즉시 실패합니다. 왜냐하면 진짜 4xx를 재시도하는 것은 그저 사용자의 시간을 낭비하는 것이기 때문입니다. 하나의 분류기, 몇 개의 이름 붙은 프리셋, 그리고 그 위로 옮겨진 세 호출자.
이 묶음이 가르쳐 준 것
- 처리되지 않은
error이벤트는 로그 한 줄이 아니라 크래시다. Node는 리스너가 없는 error 이벤트를 다시 던진다. 아직 해체하는 중일지도 모르는 소켓의 리스너를 벗겨낸다면, 아무 일도 하지 않는 error 싱크를 다시 붙여야 한다 — 그러지 않으면 끊긴 연결 하나가 앱 전체를 데리고 간다. - 만료 전 과 401에 대한 반응적 방식으로 자격 증명을 갱신하라 — 둘 다에 뮤텍스를 얹어서. 선제적은 로그인을 유지하고, 반응적은 예외 사례를 잡으며, 뮤텍스는 N개의 동시 호출자가 정확히 한 번만 갱신을 일으키게 하고, 재시도 상한은 나쁜 토큰이 영원히 돌 수 없게 한다.
- 일시적 에러일 때만 재시도하고, 분류기를 중앙에 모아라. "이것은 재시도할 가치가 있는가?"는 하나의 결정이며, 한 곳에 속한다. 진짜 에러를 재시도하는 것은 그저 더 느린 실패일 뿐이다.
- 누가 재연결을 소유할지 정하라. 우리는 그것을 클라이언트에 추가했다가, 백엔드로 옮겼다. 회복 로직이 양쪽 끝에 사는 것은 버그 생성기다. 단일한 집을 주어라.
이것은 GeekBye v2가 되는 안정성 이야기의 세 번째 장입니다. 이전 장은 음성 코드 5,000줄을 삭제했더니 전사가 두 번씩 나타나기 시작했다(v1.6.0)를. 재연결이 결국 정착한 곳은 AI 노트테이커는 왜 나쁜 Wi-Fi에서 멈추는가를. 그리고 전체 궤적에 대해서는 소프트웨어를 완벽하게 출시하는 것의 해부학을 보세요.