
개요
토스에서 주최한 개발자 컨퍼런스인 SLASH24에 다녀온 후기를 공유합니다.
SLASH
SLASH는 개발 시 명령어 앞에 사용되는 기호이자, 기술 혁신으로 세상에 한 획을 긋는다는 의미를 담고 있다고 합니다. 깜빡 거리는 /를 시작으로 새로운 문제를 찾고 또 끊임없이 도전하여, 올라선 문제 너머 토스 팀이 이뤄낸 기술적 성취와 배움을 공유하는 것이 SLASH24의 목적이라고 합니다. 슬래시는 올해로 4회째를 맞이하였으며, 이번 컨퍼런스는 최초로 오프라인으로 진행되었습니다. 장소는 코엑스의 그랜드볼룸이었습니다.
항상 개발 관련 컨퍼런스에 참여하고 싶다는 생각이 들었는데, 이번에 운이 좋게 SLASH24에 참여하게 되었습니다. 당첨이 되면 아래와 같은 메시지를 받게 되고, 당일에 전달받은 QR코드를 통해 입장할 수 있습니다.

5:1의 경쟁률을 뚫고 당첨되었습니다. 토강신청 성공 😆
슬래시는 연사들과 함께하는 데브챗, 각 트랙에 맞는 세션, 그리고 각종 이벤트 등으로 구성되어 있었습니다.
아무래도 저는 프론트엔드 개발자이기 때문에, 자연스레 프론트엔드 관련 세션에 관심이 갔습니다. 그중에서 꼭 듣고 싶은 세션과 데브챗을 리스트업 해 참여했습니다.
데브챗의 경우 사전에 예약을 통해 참여할 수 있었습니다.
- 토스가 오프라인 결제를 빠르고 안정적으로 혁신하는 방법
- 잃어버린 생산성을 찾아서, React Native로 알아보는 디버깅
- Yarn Plugin으로 우아하게 자동 로깅하기
- N개의 탭 단 하나의 웹소켓: SharedWorker
- SharedWorker 데브챗
각 세션 모두 흥미로웠지만, 특히 저는 "N개의 탭 단 하나의 웹소켓: SharedWorker" 세션이 가장 인상 깊었습니다. 그래서 이 세션을 중심으로 후기를 작성하겠습니다.
N개의 탭 단 하나의 웹소켓: SharedWorker
기존 모바일을 지원하던, 토스 증권이 데스크탑을 지원하게 되면서 생긴 문제점을 해결하는 과정을 소개하는 세션이었습니다.
토스 팀이 마주한 문제
토스 증권은 모바일을 지원하던 서비스였으나, 데스크탑을 지원하게 되면서 생긴 문제점을 해결해야 했습니다. 토스 팀은 증권 관련 실시간 데이터를 가져오기 위해 웹소켓(WebSocket)을 사용하고 있었습니다.
웹소켓(WebSocket)이란?
웹소켓은 클라이언트와 서버 간에 실시간 양방향 통신을 가능하게 하는 프로토콜입니다. 일반적인 HTTP와 달리, 웹소켓은 지속적인 연결을 유지하여, 클라이언트와 서버 간에 데이터를 주고받을 수 있습니다. 특히 실시간 데이터를 주고받아야 하는 서비스에 적합합니다. (e.g. 채팅, 주식 거래 등)
간단한 웹소켓 연결 코드는 다음과 같습니다:
const socket = new WebSocket("ws://localhost:8080");
socket.addEventListener("open", () => {
console.log("웹소켓 연결 성공!");
});
socket.send("데이터 전송!");
socket.addEventListener("message", (e) => {
console.log("서버로부터 메시지 수신: ", e.data);
});
그렇다면, 어떤 문제점이 발생했길래, 토스 팀이 이를 해결해야 했을까요?
모바일 앱 환경과 데스크탑 환경의 가장 큰 차이점은 바로 데스크탑의 경우 브라우저에서 여러 탭을 열었을 때, 각 탭이 독립적으로 동작한다는 점입니다.
각 탭이 독립적이라는 것의 의미는, 탭이 서로 다른 웹소켓 연결을 가지고 있다는 것입니다.

일반적인 사용자의 관점이 아닌 트레이더의 입장에선, 여러 탭을 열어 다양한 종목의 실시간 데이터를 확인해야 하는 경우가 다반사입니다. 최대 50개의 탭을 열어 실시간 데이터를 확인하는 트레이더도 있다고 합니다. 📈
각 연결 환경을 명확히 살펴보기 위해 그림으로 그려보면 다음과 같습니다:
모바일

데스크탑

이와 같이 독립적인 웹소켓들이 생성되면 어떻게 될까요? 각 탭이 독립적이기 때문에, 서버와 클라이언트 간에 여러 개의 웹소켓 연결이 생성되어, 서버에 부하가 발생하게 됩니다. 즉 서버의 리소스가 낭비되는 것입니다. 이 상황은 중복을 싫어하고 재사용을 선호하는 개발자들에게는 아주 끔찍한 호러쇼 그 자체입니다. 🫨
그렇다면, 이 상황을 토스 팀은 어떤 식으로 타개할 수 있었을까요? 우선 다음과 같이 연결을 끊어주는 방식을 고려했다고 합니다.
-
focus / blur 이벤트
- 탭이 활성화되었을 때만 웹소켓 연결을 생성하고, 비활성화되었을 때는 연결을 종료하는 방법
- 문제점: 탭이 활성화되었을 때만 데이터를 수신할 수 있어, 사용자 경험을 해침
-
visibilitychange 이벤트
- visibilitychange 이벤트는 웹 페이지의 가시성이 변경될 때 발생하는 이벤트
- 사용자가 탭을 전환할 때마다 웹소켓 연결을 생성하고 종료하는 방법
- 문제점: 여전히 보이는 탭의 개수만큼 웹소켓 연결이 생성되는 문제 발생
이와 같은 방식들은 웹소켓이 독립적으로 발생하는 근본적인 문제를 해결하지 못하였습니다.
해결책
연결을 끊는 방식은 근본적인 문제를 해결할 수 없었기 때문에, 새로운 해결책을 찾아야 했습니다. 근본적인 해결책은, 각 브라우저가 아래와 같이 각 탭이 하나의 웹소켓 연결을 공유하는 방식을 찾는 것이었습니다.

웹소켓이 탭의 개수만큼 독립적으로 생성되고 연결이 생기는 것을 방지하기 위해, 토스 팀이 도입한 해결책은 바로 Web Workers API의 SharedWorker를 사용하는 것이었습니다.
Web Workers API
웹 워커 API는 스크립트를 주 실행 스레드와 분리된 별도의 백그라운드 스레드에서 실행할 수 있게 해주는 브라우저 API입니다. 자바스크립트는 싱글스레드로 설계되어 한 번에 하나의 작업만 수행할 수 있습니다. 웹 워커를 사용하면 별도의 백그라운드 스레드에서 작업을 실행할 수 있게 됩니다.
워커로 지정한 자바스크립트 파일은 윈도우와 전혀 다른 전역 컨텍스트인 워크 스레드에서 동작됩니다. 하지만, 모든 기능이 워커 스레드에서 실행할 수 있는 것은 아닙니다. 워커 스레드에서는 DOM을 조작할 수 없고, 윈도우 객체의 일부 메서드와 속성을 사용할 수 없습니다. 그러나 웹소켓의 경우 워커 스레드에서도 사용할 수 있습니다. 워커와 메인 스레드는 메시지 시스템을 사용해 데이터를 주고받습니다.
단일 스크립트에서 사용되는 워커의 경우 Dedicated Worker라고 불리며, 여러 브라우저 컨텍스트에서 사용되는 워커의 경우 Shared Worker라고 불립니다.
-
Dedicated Worker
- 단일 연결: 하나의 스크립트에서만 접근할 수 있으며, 해당 워커가 생성된 스크립트와만 통신할 수 있습니다.
-
Shared Worker
- 다중 연결: 여러 탭에서 접근할 수 있으며, 동일한 도메인 내에서 여러 스크립트가 해당 워커 인스턴스를 공유할 수 있습니다.
- 탭 간 통신: 탭 간에 메시지를 주고받을 수 있어, 여러 탭에서 동일한 데이터를 공유하거나 상태를 유지할 수 있습니다.
위에서도 알 수 있듯 SharedWorker를 사용하면, 탭 간 하나의 웹소켓을 통해 데이터를 주고받을 수 있게 됩니다. SharedWorker는 Channel Messaging API
를 통해 탭 간 통신을 지원합니다.
이 API는 MessageChannel
의 포트인 MessagePort
를 통해 서로 다른 포트로 메시지를 전송할 수 있게 해줍니다.
탭 - SharedWorker + 웹소켓 - 서버 형태로 데이터를 실시간으로 공유할 수 있게 되었습니다. 또한, SharedWorker가 지원되지 않는 브라우저 환경에서는 Dedicated Worker를 fallback으로 사용할 수 있도록 하였다고 합니다.
실제 구현 시의 난관
- 번들링
웹 워커 파일의 경로를 지정할 때, 상대 경로로 지정할 시, 번들러가 파일을 찾지 못하는 문제가 발생하였다고 합니다. 이런 문제를 해결하는 방법은 다음과 같습니다:
const worker = new SharedWorker(new URL("./worker.ts", import.meta.url));
// 변환
const worker = new SharedWorker(new URL("./chunks/q1w2e3r4.js"));
- 메모리 누수
탭이 닫힐 때 SharedWorker 인스턴스가 메모리에 남아있는 문제가 발생하였다고 합니다.
탭이 닫힐 때 발생하는 이벤트인 BeforeUnload
는 탭이 최소화된 상태에서 닫으면 호출되지 않는 문제가 발생합니다.
이 난관을 해결하기 위해, WeakRef
와 MessagePort
를 조합하여 가비지 컬렉션을 활용한 탭 종료 감지 방법을 사용했다고 합니다.
MessagePort의 경우 탭이 닫히면, 가비지 컬렉션이 발생하여 메모리에서 해제됩니다. 하지만, SharedWorker의 경우 MessagePort 객체들을 배열로 관리하고 있기 때문에 가비지 컬렉션이 발생하지 않았다고 합니다.
이를, WeakRef
를 사용해 가비지 컬렉션이 발생하도록 구현하였다고 합니다.
WeakRef는 객체에 대한 약한 참조를 제공해, 가비지 컬렉션에 의해 정리될 수 있도록 합니다.
WeakRef의 문법은 다음과 같습니다:
const obj = { name: "Ye" };
const weakObj = new WeakRef(obj);
if (!weakObj.deref()) {
console.log("객체가 메모리에서 해제되었습니다.");
}
WeakRef를 사용해 연결된 포트들을 관리하고, 탭이 닫힐 때 해당 포트들을 해제하여 메모리 누수를 방지했다고 합니다.
사실 웹소켓, 웹 워커 API 그리고 WeakRef는 저에게 생소한 개념이었기 때문에, 이 세션을 듣고 나서 직접 공부해 보고 싶은 마음이 들었습니다. 꼭 다시 공부해서 정리하기 ❗
데브챗
데브챗은 세션을 진행한 연사들과 더 가깝게 소통할 수 있는 네트워킹 프로그램이었습니다. 해당 데브챗을 통해 연사와 질의응답을 할 수 있었고, 더 깊은 이야기를 나눌 수 있었습니다. 저는 이와 같은 문제를 해결하는 데 있어서, 어떻게 기술 검증을 했는지 궁금했고, 이에 대한 질문을 하였습니다. SharedWorker를 사용해 탭 간 웹소켓을 공유하는 예시가 거의 없었기에 W3C의 스펙과 MDN을 참고해 기술 검증을 진행했다고 답변을 받았습니다.
제가 맡고 있는 도메인이 실시간 통신을 주로 사용하는 서비스는 아니었지만, 문제를 파악한 후 해결책을 찾아내는 과정이 정말 인상 깊었습니다. 근본적인 문제를 파악한 후 새로운 기술을 도입해 문제를 해결하는 과정이 개발자로서 영감을 주었습니다.
후기

컨퍼런스에 만명이 지원하고, 이천명이 당첨되었다는 말을 듣고 토스라는 기업의 힘이 대단하다고 생각했습니다. 5개의 공간에서 서로 다른 트랙에 관한 세션을 들을 수 있었고, 데브챗과 같은 프로그램이 존재해, 각 세션의 연사와 더 밀접하게 소통할 수 있어서 좋았습니다.
세션을 들으며, 토스 팀이 어떤 식으로 일하는지 아주 단편적이지만 엿볼 수 있어 좋았습니다. 특히 토스 팀이 문제점을 파악하고 해결하는 일련의 과정을 알 수 있었습니다. 문제를 마주했을 때 정확한 해결책이 없다면, 자신들이 필요한 해결책을 스스로 만든다는 점이 정말 개발자스럽구나라는 생각이 들었습니다. 또 단순히 그 상황을 벗어나는 해결책이 아닌 근본적인 문제를 해결하는 방법을 찾는 것이 중요하다는 것을 배웠습니다.
이전에는 화면에 무언가를 그려내는 것만으로도 개발자라는 직업이 재미있다고 느꼈습니다. 하지만 최근에는 문제를 해결하는 과정에서의 즐거움을 알게 되었고, 그 문제를 해결하기 위해 새로운 기술을 도입하는 것이 재미있다는 생각이 들었습니다. 이번 컨퍼런스는 이러한 저의 생각을 다시금 확인시켜주었습니다.
컨퍼런스를 들으며 동기부여가 많이 됐습니다. 이런 유익한 컨퍼런스를 주최해주신 토스에게 감사드립니다.