두 소켓을 하나로, 브릿지 서버로
지난 글에서 SharedWorker로 탭마다 중복되던 소켓을 정리했다. 탭이 몇 개든 거래소 연결은 워커 한 곳이 쥐도록 모은 것이다. 그래도 그 워커가 쥔 소켓은 여전히 브라우저당 두 개였다 — 업비트와 바이낸스. 이번엔 그 둘마저 브라우저 밖으로 들어내 하나로 만든 이야기다.
줄였어도, 브라우저당 둘이었다
SharedWorker 덕에 탭이 몇 개든 거래소 연결은 워커 한 곳에서만 쥐게 됐다. 그런데 그 워커가 쥐고 있던 건
업비트 WS 하나, 바이낸스 WS 하나 — 모두 두 개였다. 김프를 계산하려면 두 거래소가 다 필요하니 당연한
일이었지만, 문제는 그 두 소켓에 딸려 오는 짐이었다.
프론트엔드가 떠안고 있던 것들을 적어보면 이렇다.
- 포맷이 거래소마다 달랐다. 업비트는 바이너리 프레임으로 JSON을 던지고, 바이낸스는 combined stream으로 온다. 등락률만 해도 업비트는 소수(0.0123), 바이낸스는 퍼센트(1.23) 로 단위가 달랐다. 두 모양을 하나로 맞추는 정규화 코드가 클라이언트에 그대로 박혀 있었다.
- 재연결·하트비트도 브라우저 몫이었다. 거래소가 끊으면 백오프로 다시 붙고, 유휴 종료를 막으려 핑을 보내는 로직이 전부 클라이언트 코드였다.
- “공유”의 범위가 좁았다. SharedWorker는 결국 한 브라우저 안에서의 공유다. 사용자가 100명이면 거래소로 나가는 연결도 100×2개. 한 사용자의 탭은 줄였지만, 서비스 전체로 보면 여전히 사용자 수만큼 늘어났다.
“탭마다 소켓”을 “브라우저마다 소켓”으로 줄였더니, 이번엔 “사용자마다 소켓”이 눈에 들어온 것이다.
브릿지 서버라는 한 칸 더
그림은 지난번과 똑같았다. 연결을 한 군데가 쥐고, 나머지는 거기서 나눠 받는 구조. 다만 그 “한 군데”를
브라우저에서 서버로 한 칸 더 밀어냈다. 작은 브릿지 서버(Bun + Hono)가 거래소에 딱 한 번만 연결하고,
두 거래소의 다른 포맷을 하나의 모양으로 정규화해서, 클라이언트에는 단일 WebSocket 하나로 흘려보낸다.

브라우저가 받는 건 거래소가 어디든 똑같이 생긴
UnifiedTicker 하나다.거래소 프로토콜의 차이(바이너리 프레임, 퍼센트 vs 소수, 재연결)는 전부 브릿지 안에 갇히고, 클라이언트는
이 통합 모양만 안다. 클라이언트 소켓은 둘에서 하나로 줄었고, 정규화 코드는 통째로 서버로 이사했다.
브릿지가 떠안은 일들
정규화만 넘어간 게 아니다. 브라우저가 들고 있던 귀찮은 일들이 이참에 다 서버로 따라왔다
- 거래소 연결·재연결 - 업비트 바이너리 프레임 디코딩과 PING, 바이낸스 combined stream, 끊기면 지수 백오프로 다시 붙는 것까지 전부 서버 몫이다. 이제 클라이언트는 브릿지 하나가 끊겼을 때 자기 재연결 만 신경 쓰면 된다.
- 검증 - 두 포맷을 `UnifiedTicker`로 맞추면서, NaN·누락 같은 비정상 시세는 클라로 흘리기 전에 버린다.
- 최신값 스냅샷 - 심볼별 현재가를 들고 있다가, 새로 붙은 연결엔 곧바로 한 벌을 던져준다. 덕분에 접속 직후 첫 화면이 비지 않는다.
- 느린 클라이언트 보호(backpressure) - 수신이 밀리는 탭에는 중간 틱을 버리고 최신값만 보낸다. 한 느린 클라이언트 때문에 서버 메모리가 새는 걸 막는다.
여기에 연결 수 상한, 토큰·CORS 게이트, 거래소 상태 점검(`/health`) 같은 운영용 처리도 같은 자리에 모인다.
무슨 마켓을 중계할지도 서버가 정한다
처음엔 중계할 심볼을 환경변수 고정 목록(
BTC,ETH,XRP,…)으로 박아뒀다. 그런데 거래소가 새 코인을 상장하거나 폐지할 때마다 목록을 고치고 재배포해야 했다. 결국 그 판단도 서버에 맡겼다. 부팅 시, 그리고 주기적으로 업비트 마켓 목록과 바이낸스 심볼 목록을 조회해 교집합을 자동으로 중계한다.
양쪽에 다 상장된 심볼만, 각 거래소의 원화·USDT 마켓(+양쪽에 BTC 마켓이 있으면 BTC 마켓까지) 형태로 여기에 원/테더 환율(업비트
KRW-USDT)은 김프 환산에 필요하니 교집합과 무관하게 항상 끼워 넣었다. 클라이언트는 어떤 코인이 중계되는지조차 신경 쓸 필요가 없어졌다.
프론트는 거의 안 바뀌었다 — 매핑 한 겹
큰 변화처럼 보이지만, 정작 화면을 그리는 코드는 거의 손대지 않았다. 비결은 매퍼 한 겹이었다. 브릿지가
주는
UnifiedTicker를, 기존 UI가 쓰던 { upbit, binance } 모양으로 바꿔주는 변환 한 겹만 끼워 넣으니 소비자 코드는 그대로 돌아갔다.재미있는 건, 예전에 여기저기 흩어져 있던 거래소별 별난 규칙들이 이 매퍼 한 곳에 모였다는 점이다.
“바이낸스만 100을 곱한다”, “업비트 USDT는 KRW를 복사한다” 같은, 알고 보면 별것 아니지만 모르면 한참 헤매는 규칙들이 한 파일에 정리되니 마음이 놓였다.
지난 글의 3단 폴백(SharedWorker → Dedicated Worker → 메인 스레드)도 그대로 살아 있다. 다만 이제 그 세 방식이 감싸는 대상이 “거래소 소켓 두 개”가 아니라 “브릿지 소켓 하나”로 바뀌었을 뿐이다.
셋 다 똑같은
UnifiedTicker를 돌려주도록 인터페이스를 맞춰놨으니, 윗단은 지금 어느 방식으로 무엇에 붙어 있는지 여전히 신경 쓰지 않는다.막상 서버를 띄우니 — 1GB의 복수
브릿지를 오라클 클라우드(OCI)의 무료 1GB 박스에 올렸다. 되도록 돈은 쓰고 싶지 않았다 ㅋㅋ. 사이드
프로젝트에 매달 서버비를 태우기는 아까웠고, 마침 OCI의 Always Free 등급이 VM을 기한 없이 공짜로
내주기 때문이었다.
사실 처음엔 더 넉넉한 Ampere(A1, ARM) 로 가려 했다 — Always Free로 최대 4 OCPU·24GB까지 공짜로 주니까. 그런데 막상 만들려니 남는 풀이 없어 생성 자체가 불가능했다(
Out of host capacity) 한국 리전에선 A1 무료 용량이 자주 동난다고 한다. 결국 차선책으로, 역시 Always Free인 AMD 1 OCPU·1GB 박스로 내려왔다. “공짜로 굴린다”는 선택의 대가로, 원하지도 않은 1GB라는 빠듯한 메모리를 받아 든 셈이다. 잘 돌았다 — 처음 몇 시간은… 그런데 주기적으로 박스가 “포트는 열려 있는데 아무 응답도 없는” 상태로 죽었다. SSH도 안 붙고 HTTPS도 먹통인데, 정작 브릿지
프로세스는 메모리 64MB에 CPU 2%밖에 안 쓰고 있었다. 범인이 브릿지가 아닌 건 분명한데, 정체를 알 수 없었다.
처음엔 “451개 마켓이 1GB엔 무리인가” 하고 추측만 했다. 그러다 방향을 바꿨다. 추측 대신 기록을 남기기로.
30초마다 메모리·load·프로세스 상위 목록을 디스크 파일에 적는 “블랙박스”를 심어두고, 다음 freeze를 기다렸다.

다음에 죽고 나서 기록을 열어보니 답이 또렷했다. 평온하던 메모리가 어느 순간
top에 dnf 48%가 뜨면서
2분 만에 swap을 가득 채우고 박스를 얼리고 있었다. 범인은 dnf-makecache.timer — OS가 주기적으로
패키지 메타데이터를 받아두는 백그라운드 작업이, 1GB 박스에선 메모리를 통째로 먹어 데몬들을 굶긴 것이다.
타이머 하나를 끄니 거짓말처럼 멈췄다. 브릿지는 처음부터 무죄였다.돌이켜보면 순서가 틀렸다. 범인을 느낌으로 찍기 전에, 죽기 직전을 남길 장치부터 심었어야 했다. 그
블랙박스가 없었으면 지금도 애먼 브릿지 코드를 의심하고 있었을 거다. 로그에서 dnf를 짚어내기까지 이
추적도 Claude를 옆에 끼고 같이 굴린 덕이 컸다. 혼자였으면 며칠 헤맸을 일을 빠르게 좁혔다.
정리하며
연결을 한 칸씩 바깥으로 밀어낸 여정이었다. 클라이언트가 들던 거래소 소켓은 둘에서 하나가 됐고, 정규화·재연결·거래소별 차이는 전부 서버로 넘어갔다. 그러면서도 프론트는 매핑 한 겹 덕에 거의 그대로 갈아끼웠다.
공짜는 아니다. 서버 한 대를 직접 운영하는 부담이 생겼고(물론 토큰과 CORS로 입구를 잠갔다), 브릿지가 죽으면
모두가 끊긴다는 단일 장애점도 떠안았다. 그건 자동 재시작과 헬스 모니터링으로 덜어내는 중이다.
돌아보면 SharedWorker가 “한 브라우저 안의 공유”였다면, 브릿지는 “서비스 전체의 공유”다. 같은 고민 —
두 거래소 가격을 같은 시점에 끊김 없이 비교하기 — 을 한 칸 더 밀고 가니, 클라이언트는 가벼워지고 거래소를
상대하는 까다로운 일은 한 곳으로 모였다.
남은 숙제는 그 한 곳을 더 편히 돌보는 일이다. 결국 더 넉넉한 박스가 답이고, 목표는 앞서 자리가 없어 못 썼던 Ampere다. 유료 계정으로 전환하면 자원을 우선 배정받아 생성이 된다는 얘기가 있어 — 전환한 뒤 다시 시도해볼 생각이다.