목차
메인 스레드를 비워 준 지도 엔진의 웹 워커 (2022-09)
원문 정보
- 원문: 자바스크립트 지도 엔진에 웹 워커를 적용해봤습니다
- 부제: 14편 - 웹 워커, 어디까지 써봤니?
- 매체 · 게시일: 티맵모빌리티 테크노트(브런치) · 2022-09-05
- 저자: 티맵모빌리티 (본문에 저자 미표기)
- 원문 내용을 바탕으로 재구성한 요약입니다.
풀 벡터 렌더링과 단일 스레드의 부담
TMAP JS SDK는 티맵 데이터를 브라우저에서 해석해 그리는 JavaScript 지도 엔진입니다. WebGL로 풀 벡터 방식을 써서 기존 네이티브 SDK와 같은 화면을 보여 주고, 사용자가 데이터를 직접 얹을 수도 있습니다.
JavaScript는 스레드가 하나라서 오래 걸리는 작업이 있으면 연산과 렌더링이 밀리고 화면이 버벅입니다. 보통은 작업을 잘게 나눠 비동기로 돌려 해결하지만 지도는 사정이 다릅니다. 땅, 길, 건물, 아이콘, 글자를 한 장면에 그려야 하고 패닝과 줌이 오면 연산이 한꺼번에 몰립니다. 미리 이미지로 구워 두는 방식도 있지만 TMAP JS는 풀 벡터 렌더링을 목표로 해서 쓸 수 없었습니다. 형상과 스타일을 실시간으로 받아 해석하고 그리는 일을 모두 브라우저가 맡습니다. 타일 단위 최적화와 캐시 같은 지도 자체의 튜닝을 마쳤는데도 원하는 성능이 나오지 않아 웹 워커를 쓰게 됐습니다.
워커로 넘긴 범위
지도는 사용자가 끊임없이 조작하고 애니메이션도 매끄러워야 해서 메인 스레드에 여유를 최대한 남겨야 했습니다. 그래서 워커가 할 수 있는 일은 모두 넘겼습니다. 최종 렌더링은 메인 스레드에서 하고, 타일 데이터 요청부터 렌더링 가능한 형태로 가공하는 단계까지를 워커가 맡습니다. 가공 결과가 WebGL 버퍼에 넘길 ArrayBuffer라서 워커에서 받기에도 알맞았습니다.
메인과 워커 사이의 통신
JavaScript에서는 두 스레드가 변수를 공유하지 못해 매개변수와 데이터를 인코딩하고 요청과 응답을 짝지어야 합니다. 팀은 양쪽 끝에 입구점을 두어 postMessage와 onmessage를 한곳에 모으고, 사용할 때는 통신을 의식하지 않고 비동기 구문만 쓰게 했습니다. 메시지는 직렬화와 역직렬화를 자동으로 거칩니다.
데이터는 공유되지 않고 복사되며, 함수나 DOM 노드는 보낼 수 없고, 클래스 인스턴스는 속성만 복사돼 제대로 복원되지 않습니다. 그래서 중첩 데이터까지 전송 가능한 타입별로 변환하면서 이동시킬 Transferable Object를 함께 골라냈고, 공유가 필요한 클래스에는 변환 어댑터를 등록했습니다. 기본 타입은 복사해도 부담이 작았지만 큰 지도 데이터 배열은 복사만으로 성능이 크게 떨어졌습니다. 이럴 때는 bytebuffer나 pbf 라이브러리로 ArrayBuffer를 직접 만들었습니다. 이렇게 하면 역직렬화 시점을 유휴 시간까지 미룰 수 있어 프레임 드롭을 줄일 수 있습니다.
우선순위 스케줄링과 워커 풀
패닝과 줌 때문에 먼저 요청한 작업의 우선순위가 바뀌거나 취소되는 일이 잦습니다. 통신 시스템 위에 우선순위 기반 큐를 얹어 그룹 이름으로 작업을 보내면 정해진 순서대로 정렬해 실행하고, 흐름은 Promise로 제어합니다. 타일 한 장의 처리도 단계별로 나눠 스케줄링해서 중간에 취소하거나 노출 순서를 바꿀 수 있게 했습니다.
워커 하나만으로도 사용자 조작의 버벅임은 대부분 사라졌지만 타일이 한 장씩 채워지는 문제는 남았습니다. 그렇다고 워커를 무작정 늘릴 수는 없었습니다. 컨텍스트 스위칭과 데이터 복사 비용이 있고 메인 스레드가 병목이 될 수 있으며, 오래된 저사양 단말에서는 하나일 때가 더 빠르기도 했습니다. 모든 환경을 시험하기는 어려워 현재는 워커 2개로 고정했습니다. 한 페이지에 지도 컴포넌트가 여러 개 뜰 수 있어서 워커는 컴포넌트가 나눠 쓰는 풀로 두었습니다. 중앙 대기 큐 방식은 아니고, 요청 메시지를 워커에 분산하거나 특정 워커에 지정하고, 워커 안에서 컴포넌트별로 묶어 관리합니다.
중복 없는 단일 파일 빌드와 결과
라이브러리로 배포하려면 메인과 워커의 공통 코드를 중복 없이 한 파일에 담아야 했습니다. 코드의 40% 이상이 양쪽에서 쓰였기 때문입니다. 대부분의 빌더는 중복을 처리해 주지 않아, 팀은 rollup의 코드 분할로 공통, 메인, 워커 코드를 AMD 모듈로 먼저 나누고 병합 스크립트로 한 파일로 합쳤습니다. 원문은 깔끔하지 않지만 더 나은 방법을 찾지 못했다고 적었습니다.
결과는 flyTo 애니메이션을 크롬 개발자 도구로 측정해 비교했습니다. 뷰포트는 340x600입니다. 개선 전에는 메인 스레드가 모든 연산을 맡아 필요한 최소 fps에도 못 미치고 흰 화면이 보였습니다. 개선 후에는 메인 스레드에 유휴 시간이 생겨 렌더 파이프라인이 훨씬 많이 돌았습니다. 원문은 수치 없이 측정 화면만 비교했습니다. 이 지도의 데이터가 만들어지는 과정은 지도 데이터 구축 글에 있습니다.
워커 구성 요약
| 항목 | 내용 |
|---|---|
| 워커로 넘긴 범위 | 타일 데이터 다운로드부터 렌더링 가능한 형태로의 가공까지 |
| 메인에 남긴 일 | 최종 WebGL 렌더링 |
| 워커 수 | 2개로 고정, 컴포넌트가 나눠 쓰는 풀 |
| 큰 데이터 전송 | bytebuffer, pbf로 ArrayBuffer 변환 후 이동 |
| 공통 코드 비율 | 전체 코드의 40% 이상 |
| 빌드 | rollup 코드 분할(AMD) 후 병합 스크립트 |
| 측정 조건 | 크롬 개발자 도구, 뷰포트 340x600, flyTo 애니메이션 |