2024 토스 slash 컨퍼런스 후기 (FE)
정말 오래간만의 오프라인 컨퍼런스
오프라인 컨퍼런스의 경우 19년도의 FEConf 이후 처음으로 참석하였습니다. 그간 코로나 여파로 오프라인 컨퍼런스는 전부 온라인으로 대체되었고 오프라인의 컨퍼런스도 이전과는 달리 엄청난 경쟁률로 인해.. 드디어 참석하게 되었습니다. 확실히 개발적인 동기가 오프라인 컨퍼런스에서 경험할수 있는 이점이 있던 경험이였습니다. 이번 후기는 FE관련 세션
에 대한 부분만 간단하게 정리해 보았습니다.
1. 토스가 오프라인 결제를 빠르고 안정적으로 혁신하는 방법
하루에도 수십번 배포가 가능했던 이유
빠른 배포가 중요한 이유
- 오프라인 결제라는 처음으로 도전하는 영역이였습니다.
- 이로인한 여러 가설을 검증하기 위해 많음 실험이 필요합니다.
- 많은 실험을 위해 빠른 배포 속도는 필수 조건 이였습니다.
Electron 배포의 문제점
- 간단한 변경사항이라도 web, Electron 앱 빌드, 사용자 앱 업데이트 해야 합니다..
- 사용자는 업데이트를 위해 100MB 이상을 받아야 하는데 이 과정이 End-To-End 로는 20분 이상 소요됩니다.
웹 버튼 텍스트만 바꾸는 경우 (js,css,html) 에도 전체 빌드가 필요한 상황
- 변경된 부분만 빌드할수는 없을까?
- 변경빈도가 자주 일어나는 부분을 찾아보았습니다.
- Electron 기반이다 보니 대부분 webview 영역이 변경 빈도가 잦은 문제가 있습니다..
Webview bundle
- html,js,css 를 webview bundle로 묶어서 cdn 으로 올리는 방식으로 개선을 시도하였습니다.
- 일렉트론 App 에서는 webview bundle 만 바라보도록 하면 일렉트론의 배포는 필요 없는 장점이 존재합니다.
다운로드 크기 5M이하 배포시간 1분대로 크게 상승되는 강점이 있습니다.
webview bundle로 서비스 쪼개기
- 토스의 경우 사일로 조직 구조 특성상 마이크로 프론트앤드 아키텍쳐 가 맞기때문에 이를 도입하였습니다.
- 독립적인 서비스 운영을 위해
주문
,결제
,시재관리
라는 관심사 3개를 3개의 webview bundle 단위로 msa로 나누어 관리하였습니다.
webview bundle로 크로스 플랫폼 이점 가져가기
- 웹뷰 번들의 경우 이미 다른 플랫폼도 사용하므로 네이티브 부분만 작업하면 되기때문에 네이티브 구현 비용은 크지 않습니다.
- webview bundle api, 영속적 로컬 저장소, 서버 통신 e2e 암호화 틍 일부 작업만 구현하면 되는 상황이였습니다.
- 이때 각 네이티브 앱 인터페이스는 조금씩 다르기 때문에 이를 통합하기 위한
Toss pos Api
표준화를 진행하였습니다.
- 이렇게 구성하면 각 네이티브는 표준화된 pos api에서 작업된 분기를 알아서 해주기 때문에 크게 신경쓸 필요가 없는 장점이 존재합니다.
안정적으로 수십만건의 결제 만들기
오프라인에서의 결제를 위한 토스 포스기기와 토스 프론트(카드단말기) 간의 통신은 안정적인 통신이 필요합니다. 또한 실제로는 한번의 통신이 아닌 다음과 같이 여러번의 양향뱡 통신이 필요합니다.
이때 포스와 프론트간의 양방향 통신
을 통해 다양한 기능을 제공하면서 연결이 안정적으로 되어야 합니다. 또한 물리적으로 떨어져 있는 두프로세스간 안정적인 통신 보장
, 변화가 많은 요구사항에 있어서도 안전
해야 합니다.
유선 연결 안정성
- 유선의 경우 시리얼포트로 통신, 물리적 간섭으로 인한 데이터 유실, 삭제등의 이슈가 있습니다. (정전기 등)
- 이를 해결하기 위해
Toss Serial Port protocol
이라는 자체 프로토콜 자체 구현하여 해결하였습니다. Data
,Ack
,Nak
이라는 3개의 프로토콜로 구분 각각 데이터, 데이터성공, 데이터 실패 를 의미합니다.
무선 연결 안정성
토스의 경우 무선으로 하는 양방향 통신을 위해 websocket을 선택하였습니다. 이때 각 포스와 프론트 기기간 통신을 위해서는 포스기기가 프론트 기기의 IP를 알아야 합니다. 그러나 이러한 양방향 통신의 경우 IP가 자주 바뀌게 된다면 (카페 wifi등.) 연결이 실패되는 문제가 발생합니다. 이를 해결하기 위해선 아래의 방법들이 존재 합니다.
197.128.90.xxx(0~256) 모두 연결해버리는 방법
모든 범위의 ip를 전부 연결해서 바뀌게 되더라도 문제가 없도록 처리할수는 있으나 너무 비효율 적인 방법이기에 선택하지 않았습니다.
BroadCasting 방식
호스트가 전송한 데이터가 네트워크에 연결된 모든 호스트에 전송되는 방식입니다.
이때 토스프론트는 네트워크에 연결된 호스트에 브로드 캐스팅하고 토스포스는 브로드캐스팅된 정보를 취득해 해당 정보로 토스 프론트에 웹소켓 재연결 요청하는 방식 입니다.
결제 안정성
처음에는 간단한 결제코드부분이 이후 다양한 요구사항들로 인해 분기가 많아지고 복잡도가 높아지게 되었습니다.
이러한 복잡한 코드를 안정하고 확장성 있게 작업할수 있는 방법을 고민하였고 그 방법으로 주문의 코드부분을 주문전, 주문후, 결제후 와 같이 시점에 따라 관심사를 분리하였습니다.
이후 이러한 라이프 싸이글의 관심사로 결제 플러그인을 구현하였습니다.
포인트, 알림톡 등의 관심사별로 플러그인을 만들어 사용하는 영역에서 주입하여 사용하여 난잡한 로직, 흐름을 플러구인 구조를 통해 선언적으로 정리하였습니다.
개인적인 소감
일렉트론을 아직 경험해보지는 못하였으나 이러한 번들 배포 방식
에 대해서는 꽤나 큰 영감을 받았으며 특히 결제 안정성 파트
에서의 코드 구성에 대해서는 개인적으로 이상향으로 생각하는 부분과 거의 비슷하여 꽤나 큰 공감을 한 세션이라고 생각되었습니다.
2. 잃어버린 생산성을 찾아서 React native 로 보는 디버깅
토스에서는 빠른 실험이 필요한 경우는 web
, React native
로 개발하고 최고 수준의 유저 경험이 필요한 영역
은 각 Native
로 전환하여 개발하고 있습니다.
web & React Native
- 플랫폼간 호환되는 isomorphic 라이브러리 혹은 동일한 인터페이스의 라이브러리를 제공합니다.
- 하지만 디버깅은 플랫폼이 달라 디버깅이 달라 어려움이 존재합니다.
web 디버깅
chrome devTools, React DevTools 를 통한 편한 디버깅 환경을 제공 합니다.
React Native 에서의 디버깅
React Native 에서는 Flipper
를 통한 디버깅을 제공 합니다. Flipper
의 경우 react dev tools 를 내장하고 각 플러그인이 있어 기능확장 가능합니다.
완벽한줄 알았던 Flipper
여러부분에서 이점이 있으나 아래와 같은 문제점 들이 있습니다.
- 일렉트론으로 래핑되어 다소 무겁다 또한 자잘한 버그가 많습니다. 이는 곧 생산성 저하로 이어집니다.
- 최신 버전부터는 디버깅툴 기본 탑재에서 빠지게 되었습니다. (React Native js inspector 가 기본탑재로 변경되었습니다.)
Chrome DevTools Protocol (cdp)
크로미움 blink 엔진 기반 브라우저와 호환이 가능합니다.
React Native 그리고 CDP 14
CDP는 React Native 코어에서도 사용중
입니다. 대표적 예시중 한개로 CDP 구현체가 hermes
내에 존재합니다.
새로운 디버깅 도구(React Native js inspector)의 한계
이미 검증된 디버깅 환경과 (익숙한 브라우저 디버깅 환경) React Native 코어에 주요 기능은 이미 구현되었으나 가장 큰 단점으로 아직 네트워크 인스펙터 기능을 지원하지 않습니다. 이는 네트워크 통신 디버깅을 못하게 되기때문에 디버깅 입장에서 몹시 크리티컬한 이슈입니다.
이를 토스에서는 CDP 기반으로 네이티브에서 네트워크를 가로채서 보여주도록 구현화 하여 네트워크 인스펙터를 볼수 있도록 처리하였습니다.
개인적인 소감
RN의 새로운 디버깅툴은 아직 부족한 점이 많구나 (네트워크 인스펙션 미지원은 정말 크리티컬해보입니다,) 라는 점과 보통 이러한 단점이 있는경우 불편함을 감수하고 사용하거나 이전버전을 사용하고 지원하길 기다리는 경우가 많은데 ( 이러한 작업을 하고싶어도 업무 리소스상 안되는 경우가 대다수인 현실이기에.) 이러한 생산성 측면에서 빠르게 시도하고 이를 허용하는 문화의 토스에 큰 감명을 받은 세션이였습니다.
3. Yarn Plugin 으로 우아하게 자동로깅하기
토스는 screen
, impression
, click
등 여러 요소를 로깅 처리합니다. 이러한 로그는 데이터 분석, 원인파악 등 여러 요소로 실제로 사용됩니다.
이러한 로그들은 아래 이미지와 같이 로깅 종류에 따라 useEffect
, IntersectionObserver
, onClick
이벤트와 같은 이벤트들을 활용하여 적재합니다.
이러한 로그들은 일일히 수동으로 추가하기엔 너무 많아지고 있어 이를 자동화할 방법을 생각하였습니다.
TDS 로깅
토스의 경우 공통으로 사용하는 디자인 시스템인 TDS
가 존재합니다. 처음에는 이 디자인 시스템에 로그를 자동화 하기 위한 시도를 진행해보았습니다.
그러나 도입하려고 보니 TDS의 경우 증권, 뱅크, 여러 협력사 들 여러 곳에서 사용하고 서비스간 전부 로그의 형태 등이 다른 이슈가 존재하였습니다.
hoc를 통해 로그 감싸기 (withLogging)
첫 시도는 컴포넌트 자체는 건들지 않고 이를 hoc를 사용하여 로깅을 처리해보려 하였으나 {Button} toss/tds
→ {Button} toss/loggin-tds
이렇게 치환하는게 너무 많은 수정이 필요한 단점이 존재하였습니다.
TDS 패키지를 가로챌 수 있을까?
아래와 같이 패키지를 가로채서 로깅을 처리하는 방법을 생각해 보았습니다.
이를 위한 구조를 처리하기 위해 아래와 같은 레이어를 구성해 보았습니다
서비스 ├── 로깅라이브러리 │ └── TDS └── 라이브러리
이러한 구조를 처리하기 위해 yarn-berry 의 플러그인
을 사용하기로 하였습니다.
Yarn의 설치 과정
Yarn의 설치과정은 Resolution Step
, fetchStep
, LinkStep
의 과정이 존재합니다. 각 과정에서 하는 역활은 다음과 같습니다.
- Resolution Step: 패키지를 찾는 과정 (버전의 범위 , 깃헙레포 등으로 찾기)
- fetchStep: 패키지를 다운로드 하는 과정
- LinkStep: 다운받은 의존성을 app 에서 사용가능하도록 연결하는 작업
이 과정중 Resolution Step
과정에서 TDS패키지를 바로 찾는게 아니라 로깅 패키지를 거치도록 하여 감싸도록 처리 해보았습니다.
이러한 작업을 통해 아래와 같이 추가 코드의 작업없이 자동으로 로깅의 자동화가 가능하게 되었습니다.
개인적인 소감
로깅을 yarn 플러그인을 통해 주입하는 방식이 상당히 인상적이였고 Yarn의 설치과정에 대해 좀더 자세하게 알게되어 좋았다고 생각합니다.
4. N개의 탭 단 하나의 웹소켓: SharedWorker
토스증권에서는 증권 도메인 특성상 실시간 데이터를 보여주어야 하는 니즈가 상당히 많습니다. 이러한 실시간 데이터를 가져오는 방법은 다음과 같은 방법들이 있습니다.
- websocket
- polling
- server sent event
이러한 방법들중 websocket
을 통해 실시간 데이터를 처리하였습니다.
websocket
websocket
은 브라우저와 서버간 양방향 통신에 사용됩니다.
모바일 앱에서의 토스 증권
모바일 앱에서의 웹뷰환경의 경우 유저 1명당 딱 1개의 토스 증권만 열리기 때문에 단순한 구조로 구성되어있었습니다. (웹소켓이 1개라 문제는 안되는 구조 입니다.)
그러나 이후 데스크탑 버전이 출시가 되머 아래와 같이 websocket
이 급격하게 증가하게 됩니다.
PC의 경우 핸드폰 앱과 다르게 여러 탭으로 같은 부분을 보며 사용하는 패턴이 대부분 입니다. 이러한 경우 PC에서 여러개의 탭의 문제점은 간단하게 정리하면 다음과 같습니다.
- 탭의 갯수만큼 웹소켓의 갯수가 증가하게 됩니다.
- 유저의 사용패턴에 따라서 웹소캣 증가가 N배 증가 합니다.
- 이러한 탭의 증가는 서버의 인프라가 추가로 늘려야 하는 이슈가 존재합니다. (서버 인프라 비용 상승)
물론 서버의 인프라 비용을 늘려 해결할 수는 있으나 이러한 문제점을 프론트에서 근본적으로 해결할수는 없는지 방법을 찾아보았습니다.
현재 실제로 포커싱된 브라우저탭만 연결하고 나머지는 연결을 끊어 버리는건 어떨까?
그러나 이는 유저 경험에 아주 큰 불편함을 초래하기 떄문에 바로 포기하였습니다.
visibility change 기준
으로 현재 화면에 보이는 탭만 연결하는건 어떨까?
그러나 이런 방법도 근본적으로는 여전히 보이는 탭의 갯수만큼 연결이 생기는 이슈가 존재합니다.
소켓 연결의 갯수 자체를 줄일 수 없을까?
이후 생각한 방법은 탭들간 websocket
을 공유를 할수 없는지를 생각해보았습니다.
Web Worker
기본적으로 js 생태계에서는 한개의 쓰레드만 사용가능하지만 web worker
를 사용하면 브라우저의 한계를 넘어서 새로운 쓰레드로 사용 가능합니다. 이러한 web worker
는 종류에 따라 dedicated worker
와 Shared Worker
두가지 가 존재합니다.
dedicated worker
dedicated worker
의 경우 연산을 여러 쓰레드로 분산시켜 FE 성능을 끌어올리는데 사용합니다. 이때 메인 쓰레드와 워커 쓰레드는 각 쓰레드가 다르기에 서로 공유할 수는 없습니다. 이러한 통신을 하기 위해선 postMessage
라는 이벤트를 통해 통신할 수 있습니다.
또한 dedicated worker
의 경우 워커를 만든 탭에서만 접근 가능한 특징이 존재합니다. (한개의 탭에서만 사용 가능)
Shared Worker
Shared Worker
의 경우 dedicated worker
와 다르게 워커 쓰레드가 여러 탭에서 공유가 가능한 특징이 존재합니다.
예를들어 1탭에서 new SharedWorker()
를 통해 Shared Worker
를 생성하고 2탭에서 new SharedWorker()
를 통해 동일하게 생성하면 새롭게 추가 생성이 되는게 아닌 1탭의 쓰레드를 공유 합니다. 이러한 공유를 하기 위해서는 아래의 2가지 조건을 만족해야 합니다.
- 불러오려는 JS파일이 동일해야 합니다.
- Origin이 동일해야 합니다.
Shared Worker
는 최초탭이 연결된 이후 추가적인 탭이 연결되는 시점을 알기 위해 해당 시점에 onConnect
이벤트를 실행합니다.
이를 통해 onConnect
로 연결되는 탭들을 배열에 저장하고 워커에서 전송하게 되면 모든 탭에 한번에 전송이 가능하게 됩니다.
Shared worker + Websocket
탭을 공유하는 특성을 가진 Shared Worker
에 WebSocket
을 사용하여 유저(브라우저)당 1개의 웹소켓으로 처리가능한 아래의 구조로 개선이 가능하게 됩니다.
shared worker의 지원범위
Shared Worker
의 경우 생각보다 지원되는 브라우저는 크지 않습니다. (특히 사파리) 이러한 환경의 경우 dedicated worker
를 통해 fallback 처리하도록 구현하였습니다.
다행히 토스를 사용하는 유저들의 경우 대다수가 호환가능한 브라우저를 사용중 이였습니다.
구현시의 난관
이러한 탭간 공유되는 작업을 진행하면서 여러 어려움이 있었는데 크게는 번들링
과 메모리 누수
가 존재합니다.
번들링
기본적으로 Shared Worker
인자에 사용할 js의 경로는 실제 서버에서의 경로를 넣어줘야 합니다. 그러나 최근 번들러들이 이러한 파일네이밍마저 변경하여 번들 시키기 때문에 네이밍 통일이 힘든 문제가 발생합니다. ( 네미잉이 변경되면 shared worker에서 공유되는 탭으로 인지를 못하기 떄문입니다. 그러나 이러한 부분에 대해선 번들러에서 자체적으로 기능을 지원해 주고 있습니다.
메모리 누수
여러 탭을 사용하다 닫은 탭들에 대해 리소스를 사용하지 못하도록 리소스 정리가 필요합니다. 그러나 Shared worker
와 MessagePort
탭의 닫아질때의 이벤트는 감지하지 못합니다. 이를 처리하기 위해 BeforeUnloadEvent
을 사용하면 어느정도는 해소되나 탭이 최소화 된 상태에서 닫아질경우에는 인지를 못하기 때문에 신뢰성이 부족하여 사용할수가 없었습니다.
탭이 닫히면 MessagePort가 가비지 컬렉션되는 부분을 이용하자.
때문에 탭이 닫히는 경우 WeakRef
로 가비지컬렉션 일어나도록 하고 WeakRef + MessagePort 를 사용하여 닫힘탭을 감지 가능 하도록 처리하여 해결하였습니다.
개인적인 소감
webWocker의 개념은 알고 있었으나 shared worker 의 개념까지는 알고있지 않았기에 이러한 부분에 대해서 배울점이 존재하였습니다. 또한 이러한 탭간 공유라는 개념은 BroadcastChannel API
만 알고 있었기에 여러부분에서 배울점이 많았던 세션이였습니다.