keepmhwn's blog
← 목록으로
문제해결ChromeMQTT

Chrome의 백그라운드 타이머 제한이 MQTT 연결을 끊었던 이유

2023년 8월 28일

상담원이 사용하는 웹 상담 앱은 전화기와 실시간으로 상태를 동기화한다. 상담원은 웹에서 전화를 수신하거나 발신할 수 있고, 전화기의 현재 상태 역시 실시간으로 확인할 수 있다.

이를 위해 전화기와 웹은 MQTT를 통해 연결되어 있다. 브로커 서버가 전화기의 상태를 토픽으로 발행하면 웹이 이를 구독하여 화면을 갱신하고, 현재 상태에 따라 전화 관련 기능을 활성화한다.

하지만 어느 날부터 상담원이 장시간 자리를 비웠다가 돌아오면 웹에서 더 이상 전화를 받을 수 없는 문제가 발생했다. 전화기는 정상적으로 동작하고 있었지만, 웹은 여전히 이전 상태를 유지하고 있었기 때문이다.

처음에는 MQTT 서버나 네트워크 문제를 의심했지만, 실제 원인은 전혀 다른 곳에 있었다. 이번 글에서는 브라우저의 백그라운드 최적화 정책이 어떻게 실시간 통신에 영향을 주었는지, 그리고 이를 어떻게 해결했는지 이야기해보려 한다.


원인 분석

상담 앱은 mqtt.js를 사용해 MQTT 브로커와 연결한다.

MQTT는 연결을 유지하기 위해 Keep Alive라는 기능을 제공한다. 클라이언트는 일정 주기마다 Ping 패킷을 브로커에 전송하고, 브로커는 일정 시간 동안 Ping을 받지 못하면 연결이 끊어진 것으로 판단한다.

우리 서비스에서는 Keep Alive를 30초로 설정했다. 즉, 30초마다 Ping을 보내 연결을 유지하도록 구성되어 있었다.

그런데 상담원이 장시간 자리를 비우면 브라우저 탭은 대부분 숨겨진 상태(Background) 가 된다.

문제는 Chrome이 숨겨진 페이지의 JavaScript 실행을 제한한다는 점이다. Chrome 88부터는 일정 조건을 만족하는 백그라운드 페이지의 타이머를 1분 단위로 묶어서 실행하는 Intensive Wake Up Throttling 정책이 적용된다.

평소에는 30초마다 실행되던 setInterval이 백그라운드에서는 1분 뒤에 실행되면서 Keep Alive 패킷 역시 1분마다 전송되기 시작했다.

결국 브로커는 Ping을 제때 받지 못해 연결을 종료했고, 웹은 전화기의 최신 상태를 더 이상 전달받지 못했다.

더 큰 문제는 연결 복구 과정에서도 발생했다.

mqtt.js는 연결이 끊어지면 기본적으로 1초마다 재연결을 시도한다. 하지만 이 재연결 로직 역시 setInterval 기반으로 동작하기 때문에 브라우저의 타이머 제한을 그대로 받았다.

즉, 연결이 끊어진 후에도 즉시 복구되지 않고 최대 1분 동안 재연결을 시도하지 못하는 상황이 발생했다.

상담원이 자리에 돌아왔을 때 웹과 전화기의 상태가 달라져 있었던 이유가 바로 이것이었다.


해결하기

상담 앱에서 가장 중요한 것은 전화기의 상태를 안정적으로 동기화하는 것이다. 상담원이 오랫동안 자리를 비우더라도 복귀했을 때 바로 현재 상태를 확인할 수 있어야 하며, 부재중 전화 역시 놓치지 않아야 한다. 이를 위해 두 가지 방향으로 문제를 해결했다.

Keep Alive 시간을 조정하기

먼저 Keep Alive를 30초에서 65초로 변경했다. Chrome이 백그라운드에서 타이머를 약 1분 단위로 실행하더라도 Ping이 허용 시간 안에 전송될 가능성을 높여 연결이 끊어질 확률을 줄일 수 있었다.

물론 이것만으로는 모든 상황을 해결할 수는 없었다. 브라우저가 더 오랫동안 JavaScript 실행을 지연시키거나 네트워크 환경이 좋지 않다면 여전히 연결이 끊길 수 있기 때문이다.

화면이 다시 활성화되면 즉시 재연결하기

그래서 두 번째로 visibilitychange 이벤트를 활용했다. 사용자가 다시 브라우저 탭으로 돌아왔을 때 MQTT 연결 상태를 확인하고, 연결이 끊어져 있다면 즉시 재연결하도록 구현했다.

기존에는 브라우저의 타이머 제한 때문에 최대 1분 동안 연결이 복구되지 않을 수 있었지만, 이 로직을 추가한 이후에는 상담원이 복귀하는 즉시 MQTT 연결을 복원할 수 있게 되었다.

그 결과 웹과 전화기의 상태 불일치가 발생하는 시간을 크게 줄일 수 있었고, 부재 중 전화 역시 정상적으로 확인할 수 있게 되었다.


마치며

이번 경험을 통해 실시간 서비스를 개발할 때는 애플리케이션 코드뿐 아니라 브라우저의 실행 환경도 함께 고려해야 한다는 점을 배웠다.

처음에는 MQTT나 네트워크 문제를 의심했지만, 실제 원인은 Chrome의 백그라운드 최적화 정책이었다. 라이브러리가 정상적으로 동작하더라도 그 기반이 되는 브라우저의 동작 방식이 바뀌면 예상하지 못한 장애가 발생할 수 있다는 사실을 경험했다.

또한 장애를 해결할 때는 단순히 끊어지지 않도록 만드는 것보다 끊어질 수 있다는 전제에서 얼마나 빠르게 복구할 수 있는지를 함께 설계하는 것이 중요하다는 점도 배울 수 있었다.

실시간 시스템은 항상 안정적인 연결을 유지하는 것이 이상적이지만, 현실에서는 연결이 끊어지는 상황을 완전히 막을 수는 없다. 중요한 것은 장애가 발생했을 때 사용자가 이를 거의 느끼지 못하도록 복구 전략까지 함께 설계하는 것이라고 생각한다.