탭 다섯 개에 WebSocket 열 개, 이게 맞을까 — SharedWorker 도입기

Coinat을 만들면서 가장 오래 붙잡고 있던 고민 중 하나를 정리해 둔다. 결론부터 말하면 SharedWorker를 쓰기로 했고, 그 결정에 이르기까지 생각이 어떻게 흘러갔는지 적어본다.

왜 이 고민을 시작했나

Coinat은 같은 코인의 Upbit 가격과 Binance 가격을 나란히 놓고, 그 차이로 김치 프리미엄(김프)을 실시간으로 보여주는 서비스다. 김프를 계산하려면 두 거래소 가격이 같은 시점에 있어야 하니, Upbit과 Binance WebSocket을 동시에 구독한다.
처음엔 단순하게 만들었다. 페이지가 뜨면 두 소켓을 연결한다. 탭이 하나일 때는 잘 동작했다. 그런데 시세 비교 서비스 특성상 사용자는 한 화면만 보지 않는다. 차트 탭 따로, 목록 탭 따로, 관심 코인 탭 따로… 탭을 켤 때마다 Upbit·Binance 연결이 두 개씩 더 생겼다. 탭 다섯 개면 연결이 열 개다. 같은 데이터를 다섯 번 받는 셈이었다.
“탭마다 똑같은 소켓을 또 여는 게 맞나?” 이 의문이 출발점이었다. 거래소 입장에서도 한 사용자가 연결을 열 개씩 잡는 건 달갑지 않을 테고, 클라이언트 입장에서도 같은 데이터를 중복으로 받느라 네트워크와 배터리를 낭비하게 된다.
notion image

SharedWorker라는 선택

원하는 그림은 분명했다. 연결을 한 군데가 쥐고, 모든 탭이 그곳에서 데이터를 나눠 받는 구조. 탭은 늘어나도 거래소 연결은 그대로 하나여야 했다.
이걸 가능하게 해주는 게 SharedWorker였다. 같은 origin에서 열린 모든 탭이 하나의 워커 인스턴스를 공유한다는 점이 정확히 내가 원하던 바였다. 워커가 거래소 소켓을 딱 한 번만 열어두면, 탭이 몇 개가 되든 그 하나를 함께 쓰면 된다.
그래서 워커가 처음 로드될 때 소켓을 각각 한 번만 만들도록 했다. 이 코드가 사실상 이 글의 핵심이다.
나머지는 단순했다. 탭이 워커에 접속하면 그 탭의 포트를 배열에 담아두고, 데이터를 요청하면 연결된 모든 탭에 한꺼번에 뿌린다. 한 탭이 요청한 데이터가 모두에게 전달되니, 각 탭이 따로 요청할 필요도 없었다.

막상 해보니 걸렸던 것들

깔끔하게 끝날 줄 알았는데, 만들고 나서야 보이는 것들이 있었다.
첫째, 닫힌 탭이 배열에 남았다.
탭을 닫아도 워커는 그 포트를 계속 들고 있었다. 브로드캐스트할 때마다 이미 죽은 탭에도 메시지를 던지는 꼴이었다. 처음엔 단순하게 생각했다. 탭이 닫힐 때 배열에서 그 포트만 빼면 되지 않나. 실제로 탭이 정상적으로 닫힐 때는 disconnect 신호를 보내 정리하게 해뒀다. 그런데 브라우저를 강제 종료하면 그 신호가 아예 오지 않는다는 게 문제였다. 내가 빼주기를 기다리는 방식으로는 새는 구멍을 다 막을 수 없었다. 그래서 발상을 바꿔 포트를 WeakRef로 감쌌다. 참조가 끊긴 포트는 가비지 컬렉터가 알아서 회수하도록. 결국 "탭이 알려주면 바로 정리, 못 알려주면 GC가 정리"라는 두 겹의 안전망이 됐다.
둘째, SharedWorker가 모든 환경에서 되는 건 아니었다.
당연히 다 될 줄 알았는데 모브라우저나 버전에 따라 SharedWorker를 지원하지 않는 경우가 있었다. 특히 모바일 쪽은 확신하기 어려웠다. 지원 여부를 가정하고 그냥 쓰면, 미지원 환경에선 앱이 통째로 깨진다.
여기서 방향을 하나 더 잡았다. SharedWorker가 안 되면 탭별 워커(Dedicated Worker)로, 그것도 안 되면 메인 스레드에서 직접 소켓을 돌리는 식으로 단계적으로 내려가게 했다.
notion image
셋 다 똑같은 형태의 데이터를 돌려주도록 인터페이스를 맞춰서, 윗단 코드는 지금 어느 방식으로 돌고 있는지 신경 쓸 필요가 없게 했다. 여기에 “정해둔 시간 안에 첫 데이터가 안 오면 다음 단계로 넘어간다”는 타임아웃(4초, 위 점선)을 붙여, 워커 스크립트 로드 자체가 실패하는 경우까지 흘려보내도록 했다.
물론 이 fallback 단계에서는 다시 탭별 연결이 생긴다. “연결 하나”라는 원래 목표는 SharedWorker가 되는 환경에서의 이점이고, 안 되는 환경에서는 적어도 깨지지 않게 하는 게 목표였다. 둘을 구분해 받아들이고 나니 마음이 편했다.

정리하며

돌아보면 이 모든 고민은 결국 한 문장으로 수렴한다. 두 거래소 가격을, 같은 시점에, 끊김 없이 비교하기. 김프라는 숫자 하나를 정확하게 보여주려고 연결을 하나로 모으고, 죽은 탭을 정리하고, 미지원 브라우저까지 챙긴 셈이다.
얻은 건 명확하다. 탭을 몇 개 켜든 거래소 연결은 하나, 닫힌 탭은 알아서 정리, 그리고 어떤 브라우저에서도 일단 화면은 뜬다. 대신 미지원 환경에선 연결이 탭마다 생기고 첫 데이터까지 최대 몇 초가 걸릴 수 있다는 비용은 그대로 안고 간다.
실시간 데이터를 다룰 때 “탭마다 연결”이라는 기본 동작이 생각보다 빨리 비용이 된다는 걸 이번에 직접 부딪히며 배웠다. SharedWorker는 그 비용을 단번에 줄여준 도구였고, 그걸 안전하게 쓰기 위한 곁가지 고민들이 오히려 더 오래 기억에 남는다.

© 2026 dan.dev.log, All right reserved.

Built with NextJS