토스증권은 왜 느릴까? - 느린 4G에서 CSR, 6MB 번들, SSE까지

Published:

요약 — Slow 4G에서 토스증권 첫 화면은 ~14초, 네이버 금융은 ~3초 걸린다. 추적해보니 원인은 CSR 구조와 6MB JS 번들이었고, 난독화된 번들 안에서 SSE 기반 실시간 시세와 Leader-Follower 패턴까지 발견했다. 결론은 ‘토스가 못 만들었다’가 아니라, 첫 진입을 희생하고 진입 후 반응성을 사는 의도된 트레이드오프라는 것 — 다만 그 전략이 무너지는 환경(느린 네트워크)이 따로 있다.

Part 1. 체감에서 탐구로

계기

업무상 주가를 자주 봐야 한다. 평소엔 tossinvest.com을 켜두고 쓴다. UI도 익숙하고, 차트도 깔끔해서 따로 이유를 찾지 않았다.

그런데 어느 날, 창을 여는데 한참이 걸렸다. 체감상 10초 이상. 빈 화면만 덩그러니 떠 있었다.

처음에는 “인터넷이 이상한가?” 싶었다. 그래서 같은 창에서 finance.naver.com을 열어봤는데, 이건 1~2초 만에 콘텐츠가 다 떴다.

인터넷 문제는 아니네.

정확히는 느린 네트워크 환경에서 한쪽만 심하게 느렸던 것이다. 빠른 Wi-Fi에서는 둘 다 멀쩡하다. 그래도 “같은 네트워크에서 왜 이렇게 차이가 나지?”라는 호기심이 생겼다.

재현

크롬 개발자 도구를 열었다.

  • Network 탭
  • Throttling → Slow 4G
  • 캐시 비활성화

이 상태에서 각각 새로고침.

 tossinvest.comfinance.naver.com
첫 화면까지 체감 시간~14초~3초

5배 차이. 단순히 “서버가 느리네” 같은 설명으로는 부족한 격차였다.

첫 관찰

Network 탭을 보니 요청 수부터 크게 달랐다. curl로 HTML만 딱 받아봤다.

$ curl -s -o /dev/null -w "size: %{size_download}, ttfb: %{time_starttransfer}s\n" https://www.tossinvest.com/
size: 48540, ttfb: 0.103s

$ curl -s -o /dev/null -w "size: %{size_download}, ttfb: %{time_starttransfer}s\n" https://finance.naver.com/
size: 179796, ttfb: 0.165s

역설이었다. HTML이 작은 쪽이 더 느렸다. 토스의 HTML은 48KB인데, 네이버는 180KB. 그런데 체감은 반대다.

HTML 크기가 작은데 느리다는 건, HTML 밖에서 뭔가 많이 받아오고 있다는 뜻이다. 그 “뭔가”를 찾아보기로 했다.


Part 2. 뜯어보기

원인 1 - CSR vs SSR

두 사이트의 HTML 본문을 실제로 열어봤다.

네이버 금융:

HTML 안에 이미 네비게이션, 뉴스 리스트, 주가 테이블, 링크가 죄다 들어있었다. <td>, <tr>, <div> 태그만 700개가 넘는다. JS가 전혀 없어도 콘텐츠는 그대로 보인다.

이게 SSR(Server-Side Rendering) 이다. 서버가 완성된 HTML을 내려주고, 브라우저는 받자마자 그림을 그린다.

토스증권:

반대였다. body 안에는 Google Tag Manager iframe, 브라우저 지원 여부 체크 코드, 그리고 #unsupported-device-section을 숨기는 CSS 정도가 전부였다. 콘텐츠는 한 줄도 없었다.

이게 CSR(Client-Side Rendering) 이다. HTML은 껍데기고, JS를 다 받아 실행해야 화면이 그려진다.

Critical Rendering Path를 그려보면 이렇다.

=== 네이버 금융 (SSR) ===
HTML (180KB, 콘텐츠 포함) → CSS → 첫 화면 표시
                                   ↓
                          나중에 JS로 인터랙티브 추가

=== 토스증권 (CSR) ===
HTML (빈 껍데기) → CSS → JS 31개 다운로드 → JS 파싱/실행
                                              ↓
                                     API 호출 → 데이터 수신 → 렌더링

Slow 4G에서 이 차이는 곧 “콘텐츠 보이는 시점”의 차이다.

원인 2 - 6MB JS 번들

JS를 얼마나 받는지 측정했다.

$ curl -s https://www.tossinvest.com/ \
    | grep -oE '/assets/v2/_next/[^"]*\.js' > urls.txt
$ wc -l urls.txt
31 urls.txt

$ xargs -I{} curl -sI "https://www.tossinvest.com{}" < urls.txt \
    | grep -i content-length \
    | awk '{sum+=$2} END{print sum/1024 " KB"}'
6017.09 KB

JS 파일 31개, 합쳐서 약 6MB. Slow 4G(1.5Mbps)에서 이걸 다 받으려면 이론적으로 30초 이상이다. 실제로는 병렬 다운로드 + gzip 압축 덕에 더 빠르지만, 파싱/실행까지 포함하면 14초가 설명이 된다.

네이버 쪽 JS는 9개, 총합 ~200KB. 약 30배 차이.

청크별로 뭐가 들어있는지 키워드를 분석해봤다.

파일크기뭘 담고 있나
_app.js2.3MB전체 앱 공통 로직 + 614개 API 엔드포인트
1840.js1.4MB주문/매매 엔진 (order 1,136건, stock 957건)
5381.js620KB차트 렌더링 엔진 (SVG path 158개, d3, framer)
index.js102KB실제 홈페이지 코드
기타 27개~1.6MB관심종목, 대시보드, 인증 등

홈페이지 하나 보는데 주문/매매/차트 엔진이 통째로 딸려온다. Next.js의 코드 스플리팅이 제대로 안 되고 있거나, _app.js에 전역 의존성이 너무 많이 엮여 있는 상태다.

코드 속으로 - SSE 발견

_app.js를 prettier로 포매팅해서 열어봤다. 2.3MB짜리 파일이라 한눈에 파악은 불가능하지만, WebSocket, EventSource, stomp(WebSocket 위에 얹어 쓰는 pub/sub 메시징 프로토콜) 같은 실시간 통신 키워드로 검색했더니 흥미로운 게 나왔다.

setupEventSource() {
  this.#x = new EventSource(this.#S, { withCredentials: true });
  this.addEventSourceEvent(this.#x);
}

EventSource. 즉 SSE(Server-Sent Events) 를 쓰고 있었다. 실시간 시세 받는 부분이다.

여기서 멈추지 않는다. 그 밑에 꽤 복잡한 클래스가 이어진다.

class l {
  static #P = {};
  static getInstance(e) {
    return (null == l.#P[e] && (l.#P[e] = new l(e)), l.#P[e]);
  }
  // ...
  switchRole(e) {
    this.detachRole();
    e ? this.executeLeaderRole() : this.executeFollowerRole();
  }
  executeLeaderRole() { /* SSE 연결 직접 맺고 브로드캐스트 */ }
  executeFollowerRole() { /* 리더로부터 메시지 수신 */ }
}

Leader-Follower 패턴이다. 브라우저에서 탭을 여러 개 열어도 SSE 연결은 한 탭(리더)만 맺고, 나머지는 BroadcastChannel로 데이터를 전달받는다. 리더가 죽으면 다른 탭이 승격된다.

에러 시 재연결 로직도 별도로 있다.

reconnect() {
  if (this.#k >= 3) return;
  this.clearReconnectTimer();
  this.#C = setTimeout(() => {
    this.#k += 1;
    this.connect();
  }, 1000);
}

왜 이렇게까지 복잡해졌을까? 짧게 말하면 SSE의 구조적 한계 때문이다.

SSE는 연결을 오래 유지한다. 탭마다 연결을 맺으면 서버 리소스가 빨리 차고, 사용자 한 명이 브라우저 탭 5개 띄워두면 연결도 5개가 된다. 그걸 한 개로 줄이려고 탭 간 공유를 직접 구현한 것이다.

답 맞추기 — SLASH 22 “토스증권 실시간 시세 적용기”

번들에서 SSE를 발견하고 나니 다음 질문이 생겼다. 실시간 시세라면 보통 WebSocket을 떠올리는데, 토스는 왜 SSE일까? 마침 토스증권이 2022년 SLASH 22에서 발표한 토스증권 실시간 시세 적용기에 그 답의 실마리가 있었다.

발표에 나온 선택의 타임라인이 흥미롭다.

  1. 런칭 전: 실시간이 아니라 API 폴링으로 구현을 끝내둔 상태였다. “주식 초심자 타겟이라 실시간성에 덜 민감할 것”이라는 가설 + 로빈후드 벤치마킹.
  2. 런칭 직전: “가장 최신 정보를 빠르게 줘야 한다”로 팀 의견이 모여 실시간으로 선회. 향후 양방향 확장을 고려해 WebSocket + STOMP를 채택했다. 기존 폴링 구현은 버리지 않고 폴백 시스템으로 남겼다.
  3. 운영 후 회고: 발표자가 직접 말한다 — “SSE가 더 방화벽 친화적이고, 패킷 검사 기능이 있는 방화벽에서 WebSocket을 처리하는 데 문제가 있다는 것을 나중에서야 알게 되었다. 단방향 통신 시스템에는 SSE도 충분히 고려해볼 만하다.”

그리고 몇 년 뒤 내가 웹 번들에서 발견한 건 EventSource였다. 단정할 수는 없지만(발표는 앱 MTS 기준이고 내 관측은 웹이다), 저 회고가 웹에서는 실제 선택으로 이어진 것처럼 보인다. 시세 수신은 단방향이고, 주문 같은 클라이언트 → 서버 요청은 어차피 별도 HTTP 호출로 처리하면 되니까.

발표에는 “실시간 연결은 인프라 전체의 문제”라는 걸 보여주는 실전 사례도 줄줄이 나온다.

  • 방화벽·보안장비 처리량: 주식 1주 선물 받기 이벤트로 한 달 만에 200만 계좌가 열리며 트래픽이 급증했는데, 시세 서버는 DMZ에 분리해뒀지만 인터넷 회선·방화벽·보안장비는 전체 서버 군이 공유하고 있었다. 시세 트래픽이 전체 시스템의 응답 지연으로 번졌고, 회선 분리 + 방화벽 교체로 해소했다.
  • 커넥션 릭: 모바일 네트워크 전환 같은 비정상 종료 시 STOMP 세션은 정리되는데 하부 WebSocket 세션이 안 닫혀서, 배포 후 며칠 지나면 커넥션 수가 동접자 수와 크게 벌어졌다. 핸들러 데코레이터에서 명시적으로 close()를 호출해 해결.
  • 로드밸런싱: least-connection 방식이 장 시작 트래픽(3분 안에 그날 최대치)에서 특정 서버 몰림을 일으켜, 라운드 로빈과 하이브리드로 교체.

파이프라인 전체는 이렇다: 거래소/연합인포맥스 → C++ 원장 → Kafka(acks=1, LZ4 압축으로 튜닝, 평균 레이턴시 수십 ms) → WebSocket 서버(DMZ) → 클라이언트. 폴링 API는 지금도 폴백으로 살아 있다.

그러고 보면 Part 2에서 본 Leader-Follower + BroadcastChannel 코드도 같은 맥락이다. transport가 WebSocket이든 SSE든, “연결을 오래 유지한다”는 사실 자체가 비용이고, 그 비용을 줄이려는 코드가 계속 쌓인다. 토스의 실시간 시세는 폴링 → WebSocket → (웹에서는) SSE로, 매번 그 비용을 다시 계산하며 움직여온 셈이다.

실시간 통신 방식 비교

내친김에 실시간 통신 옵션을 전부 정리했다.

 HTTP PollingSSEWebSocketStreamable HTTP
방향클라이언트 → 서버서버 → 클라이언트진짜 양방향 (full-duplex)요청-응답 스트림 (POST + SSE 응답)
프로토콜HTTPHTTPws://HTTP (내부적으로 SSE 활용)
연결 유지매번 새로유지유지요청 단위 스트림
인프라 호환완벽완벽별도 설정완벽
스케일아웃쉬움어려움 (세션 고정)어려움 (세션 고정)설계에 따라 다름
재연결필요 없음브라우저 자동직접 구현요청별 독립 가능

HTTP Polling은 가장 단순하다. 1초마다 “시세 줘” 요청. 실시간성은 떨어지지만 서버가 stateless라 확장이 쉽다. 뉴스 피드나 5분봉처럼 즉시성이 덜 중요한 곳에 맞다.

SSE는 서버 → 클라이언트 한 방향으로만 데이터가 흐른다. HTTP 기반이라 기존 인프라(nginx, CDN, 로드밸런서)를 그대로 쓸 수 있고, 브라우저가 자동 재연결도 해준다. 토스가 웹에서 쓰는 방식이다. 단점은 양방향이 안 된다는 것, 그리고 연결이 오래 유지되다 보니 서버 세션 고정이 필요하다는 것.

WebSocket은 진짜 양방향 통신. 채팅, 게임, 초저지연 트레이딩 같은 곳에 쓴다. 토스가 앱 MTS에서 쓰는 방식이다. 대신 ws://라는 별도 프로토콜이라 nginx Upgrade 설정, 로드밸런서 sticky session, 방화벽 등 인프라 레이어마다 신경 쓸 게 늘어난다.

Streamable HTTP는 요즘 나온 중간 지점이다. POST 기반이라 요청 body에 뭐든 담을 수 있고, 서버가 단건 JSON과 SSE 스트림 중 응답 모드를 고르며, stateless 설계도 가능하다. stateless로 설계하면 위에서 본 Leader-Follower 복잡도를 상당 부분 덜어낼 잠재력이 있다. 분량이 커서 별도 글: Streamable HTTP 딥다이브로 분리했다.


Part 3. 잠깐 — 토스가 잘못 만든 건가?

여기까지만 읽으면 “토스가 느리네”로 결론낼 뻔했지만, 한 발짝 물러서서 봐야 한다. 토스증권은 정보 조회 사이트가 아니라 거래 앱이다. 매수·매도, 호가, 체결, 차트, 보유 현황이 한 세션 안에서 쉴 새 없이 돌아간다. 이런 앱에서 진짜 중요한 건 첫 화면 진입 속도가 아니라 진입한 다음의 모든 것이다.

  • 시세는 ms 단위로 갱신돼야 한다
  • 호가창이 주문 도중에 깜빡이면 안 된다
  • 종목 → 차트 → 주문 → 보유 페이지 전환은 즉시 반응해야 한다
  • 차트 봉 단위 전환에 깜빡임 0
  • 주문 버튼은 누르자마자 떠야 한다

이걸 SSR로 만들면 페이지 이동마다 서버를 한 번씩 더 다녀와야 하고, 그때마다 사용자 컨텍스트(보유 종목, 미체결 주문, 세션)를 다시 꾸려야 한다. 거래 중 깜빡임 한 번에 사용자 신뢰가 무너지는 도메인이라, 토스는 “한 번 비싸게 부팅하고, 그다음은 클라이언트가 전부 처리한다” 는 쪽을 의식적으로 골랐다. 6MB JS와 31개 청크는 그 선택의 결과물이지 사고가 아니다.

그래서 네이버 금융과의 단순 비교는 부분적으로 불공정하다. 네이버 금융은 읽기 전용 정보 사이트라 “페이지 로드 = 콘텐츠 표시”가 곧 끝이다. SSR이 압도적으로 유리하다. 토스증권은 트레이딩 워크스테이션이라 한번 부팅된 후 사용자가 그 안에서 수십 분~수 시간을 머문다. 무게중심이 다른 트레이드오프다.

실제로 토스증권은 한번 들어간 다음에는 종목 검색, 차트 전환, 주문 패널 호출이 거의 0ms다. 같은 Slow 4G에서도 진입 후 인터랙션은 SSR로 짠 네이버보다 훨씬 부드럽다. 6MB는 “거래 앱이 들어온 후 즉시 반응하기 위해 선불로 치르는 비용”이다. 처음에 느린 것을 감수하더라도, 한번 띄운 뒤로는 모든 게 손끝에 따라붙게 만들겠다는 의도다.

다만 이 전략은 첫 관문이 너무 높아지는 순간 무너진다. Slow 4G가 정확히 그 지점이다. “진입 후의 부드러움”을 경험할 기회 자체가 사라지면, 그 비싼 부팅의 대가는 회수가 안 된다. 지하철·엘리베이터·외곽지역에서 “지금 주가 한 번만”인 사용자는 그냥 네이버를 켠다.

그러니 손볼 여지는 “SPA를 버려라”가 아니라 “첫 관문만 부분적으로 SSR/SSG로 빠르게 띄우고, 거래 화면 진입 후에는 지금 그대로” 쪽이다. Next.js라면 정적 셸을 먼저 보내고, 인증 후 거래 영역만 lazy load 하는 식. 6MB는 그대로 받되 “받는 동안 화면이 비어 있느냐”가 달라진다.


마무리

이번 탐구에서 얻은 결론은 꽤 단순하다.

속도는 결국 아키텍처 선택의 결과물이다. 토스증권의 무거운 첫 진입은 거래 앱으로서 의도된 선택이고, 그 선택이 무너지는 환경(Slow 4G)이 따로 있다는 것까지 같이 봐야 공정한 진단이 된다.

도구는 많다. SSR 하이브리드, 라우트 레벨 코드 스플리팅, 동적 import, Streamable HTTP, HTTP/3, Early Hints 등. 그런데 어떤 걸 어디까지 쓸지는 조직의 리스크 허용치와 엔지니어링 우선순위에 달려 있다. 증권사는 특히 보수적일 수밖에 없다.

다만 사용자 입장에선 간단하다. 네트워크가 나쁠 때 네이버 금융을 켠다. 그게 내가 이 글을 쓰게 된 시작이었고, 결국 한 바퀴 돌아온 결론이다.