웹 작업자를 사용하여 브라우저의 기본 스레드에서 자바스크립트 실행

오프 메인 스레드 아키텍처를 사용하면 앱의 안정성과 사용자 환경을 크게 개선할 수 있습니다.

지난 20년 동안 웹은 몇 가지 스타일과 이미지가 포함된 정적 문서에서 복잡한 동적 애플리케이션으로 크게 발전했습니다. 하지만 한 가지는 크게 달라지지 않았습니다. 사이트를 렌더링하고 JavaScript를 실행하는 작업을 하는 스레드는 브라우저 탭당 하나뿐입니다 (일부 예외 있음).

그 결과 기본 스레드가 과도하게 사용되었습니다. 웹 앱이 복잡해짐에 따라 기본 스레드가 성능의 주요 병목 현상이 됩니다. 더 큰 문제는 기기 기능이 성능에 큰 영향을 미치므로 특정 사용자의 기본 스레드에서 코드를 실행하는 데 걸리는 시간이 거의 완전히 예측할 수 없다는 것입니다. 이러한 예측 불가능성은 사용자가 극도로 제약된 피처폰부터 고성능, 고주사율 플래그십 머신에 이르기까지 점점 더 다양한 기기에서 웹에 액세스함에 따라 더욱 커질 것입니다.

인간의 지각과 심리에 관한 경험적 데이터를 기반으로 하는 Core Web Vitals와 같은 성능 가이드라인을 정교한 웹 앱에서 안정적으로 충족하려면 기본 스레드 (OMT) 외부에서 코드를 실행하는 방법이 필요합니다.

웹 작업자를 사용해야 하는 이유

JavaScript는 기본적으로 기본 스레드에서 작업을 실행하는 단일 스레드 언어입니다. 그러나 웹 워커는 개발자가 기본 스레드에서 작업을 처리하는 별도의 스레드를 만들 수 있도록 허용하여 기본 스레드에서 일종의 탈출구를 제공합니다. 웹 워커의 범위는 제한되어 있고 DOM에 직접 액세스할 수 없지만, 기본 스레드를 과도하게 사용하게 될 만큼 많은 작업을 해야 하는 경우 웹 워커가 매우 유용할 수 있습니다.

Core Web Vitals가 우려되는 경우 기본 스레드 외부에서 작업을 실행하는 것이 유용할 수 있습니다. 특히 메인 스레드에서 웹 워커로 작업을 오프로드하면 메인 스레드의 경합을 줄일 수 있으므로 페이지의 다음 페인트까지의 상호작용 (INP) 반응성 측정항목을 개선할 수 있습니다. 기본 스레드에 처리할 작업이 적으면 사용자 상호작용에 더 빠르게 응답할 수 있습니다.

특히 시작 시 기본 스레드 작업이 줄어들면 긴 작업이 줄어들어 최대 콘텐츠 렌더링 시간 (LCP)에 도움이 될 수 있습니다. LCP 요소를 렌더링하려면 텍스트 또는 이미지(자주 사용되는 일반적인 LCP 요소)를 렌더링하는 데 필요한 기본 스레드 시간이 필요합니다. 전체적으로 기본 스레드 작업을 줄이면 웹 워커가 대신 처리할 수 있는 비용이 많이 드는 작업으로 인해 페이지의 LCP 요소가 차단될 가능성이 줄어듭니다.

웹 작업자를 사용한 스레딩

다른 플랫폼은 일반적으로 스레드에 함수를 제공하여 나머지 프로그램과 동시에 실행되는 함수를 제공하여 병렬 작업을 지원합니다. 두 스레드에서 동일한 변수에 액세스할 수 있으며 이러한 공유 리소스에 대한 액세스는 경합 상태를 방지하기 위해 뮤텍스 및 세마포와 동기화할 수 있습니다.

JavaScript에서는 2007년부터 사용되어 왔으며 2012년부터 모든 주요 브라우저에서 지원되는 웹 워커에서 대략 유사한 기능을 가져올 수 있습니다. 웹 워커는 기본 스레드와 동시에 실행되지만 OS 스레딩과 달리 변수를 공유할 수 없습니다.

웹 작업자를 만들려면 작업자 생성자에 파일을 전달합니다. 그러면 별도의 스레드에서 해당 파일 실행이 시작됩니다.

const worker = new Worker("./worker.js");

postMessage API를 사용하여 메시지를 전송하여 웹 워커와 통신합니다. postMessage 호출에서 메시지 값을 매개변수로 전달한 다음 작업자에 메시지 이벤트 리스너를 추가합니다.

main.js

const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  // ...
});

메시지를 기본 스레드로 다시 전송하려면 웹 워커에서 동일한 postMessage API를 사용하고 기본 스레드에서 이벤트 리스너를 설정합니다.

main.js

const worker = new Worker('./worker.js');

worker.postMessage([40, 2]);
worker.addEventListener('message', event => {
  console.log(event.data);
});

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  postMessage(a + b);
});

물론 이 접근 방식에는 몇 가지 제한사항이 있습니다. 이전에는 웹 워커가 주로 하나의 과도한 작업을 기본 스레드 외부로 이동하는 데 사용되었습니다. 단일 웹 작업자로 여러 작업을 처리하려고 하면 금방 불편해집니다. 메시지에서 매개변수뿐만 아니라 작업도 인코딩해야 하고 요청에 대한 응답을 일치시키기 위해 관리 작업을 해야 합니다. 이러한 복잡성 때문에 웹 작업자가 더 광범위하게 채택되지 않은 것 같습니다.

하지만 기본 스레드와 웹 작업자 간의 통신과 관련된 문제를 일부 해결할 수 있다면 이 모델은 많은 사용 사례에 적합할 수 있습니다. 다행히 이를 실행하는 라이브러리가 있습니다.

ComlinkpostMessage의 세부정보를 고려하지 않고도 웹 워커를 사용할 수 있도록 하는 것을 목표로 하는 라이브러리입니다. Comlink를 사용하면 스레딩을 지원하는 다른 프로그래밍 언어와 거의 마찬가지로 웹 워커와 기본 스레드 간에 변수를 공유할 수 있습니다.

웹 워커에서 가져오고 메인 스레드에 노출할 함수 집합을 정의하여 Comlink를 설정합니다. 그런 다음 기본 스레드에서 Comlink를 가져오고 작업자를 래핑하여 노출된 함수에 액세스합니다.

worker.js

import {expose} from 'comlink';

const api = {
  someMethod() {
    // ...
  }
}

expose(api);

main.js

import {wrap} from 'comlink';

const worker = new Worker('./worker.js');
const api = wrap(worker);

기본 스레드의 api 변수는 모든 함수가 값 자체가 아닌 값에 대한 약속을 반환한다는 점을 제외하고 웹 워커의 변수와 동일하게 동작합니다.

어떤 코드를 웹 워커로 이동해야 하나요?

웹 워커는 DOM 및 WebUSB, WebRTC, Web Audio와 같은 여러 API에 액세스할 수 없으므로 이러한 액세스를 사용하는 앱 부분을 워커에 배치할 수 없습니다. 그렇더라도 작업자로 이동된 모든 작은 코드 조각은 사용자 인터페이스 업데이트와 같이 기본 스레드에 있어야 하는 작업에 더 많은 여유를 제공합니다.

웹 개발자에게는 한 가지 문제가 있습니다. 대부분의 웹 앱은 Vue 또는 React와 같은 UI 프레임워크를 사용하여 앱의 모든 것을 조정합니다. 모든 것이 프레임워크의 구성요소이므로 본질적으로 DOM에 연결되어 있습니다. 따라서 OMT 아키텍처로 이전하기가 어려울 것 같습니다.

그러나 UI 문제와 상태 관리와 같은 다른 문제가 분리된 모델로 전환하면 프레임워크 기반 앱에서도 웹 워커가 매우 유용할 수 있습니다. PROXX에서는 바로 이러한 접근 방식을 사용합니다.

PROXX: OMT 우수사례

Google Chrome팀은 오프라인 작업 및 매력적인 사용자 환경을 비롯한 프로그레시브 웹 앱 요구사항을 충족하는 Minesweeper 클론으로 PROXX를 개발했습니다. 안타깝게도 게임의 초기 버전은 피처폰과 같이 제약된 기기에서 실적이 저조하여 팀은 기본 스레드가 병목 현상이라는 것을 깨달았습니다.

팀은 웹 워커를 사용하여 게임의 시각적 상태를 로직에서 분리하기로 결정했습니다.

  • 기본 스레드는 애니메이션 및 전환의 렌더링을 처리합니다.
  • 웹 워커는 순수하게 계산적인 게임 로직을 처리합니다.

OMT는 PROXX의 피처폰 성능에 흥미로운 영향을 미쳤습니다. OMT가 아닌 버전에서는 사용자가 UI와 상호작용한 후 6초 동안 UI가 정지됩니다. 피드백이 없으며 사용자가 다른 작업을 하기 전에 6초를 기다려야 합니다.

PROXX의 비 OMT 버전에서 UI 응답 시간입니다.

그러나 OMT 버전에서는 게임이 UI 업데이트를 완료하는 데 12초가 걸립니다. 이는 성능 저하로 보이지만 실제로는 사용자에게 더 많은 의견을 제공합니다. 속도가 느려지는 이유는 앱이 프레임을 전송하지 않는 비 OMT 버전보다 더 많은 프레임을 전송하기 때문입니다. 따라서 사용자는 무언가 진행 중임을 알 수 있고 UI가 업데이트되는 동안 계속 플레이할 수 있으므로 게임의 느낌이 훨씬 좋아집니다.

PROXX의 OMT 버전에서 UI 응답 시간입니다.

이는 의식적인 절충입니다. 고급 기기 사용자에게 불이익을 주지 않으면서 제약이 있는 기기 사용자에게 더 나은 느낌의 환경을 제공합니다.

OMT 아키텍처의 의미

PROXX 예에서 볼 수 있듯이 OMT를 사용하면 더 다양한 기기에서 앱을 안정적으로 실행할 수 있지만 앱 속도는 빨라지지 않습니다.

  • 작업을 줄이는 것이 아니라 기본 스레드에서 작업을 이동하는 것입니다.
  • 웹 작업자와 기본 스레드 간의 추가 통신 오버헤드로 인해 속도가 약간 느려질 수 있습니다.

장단점 고려하기

JavaScript가 실행되는 동안 기본 스레드는 스크롤과 같은 사용자 상호작용을 자유롭게 처리할 수 있으므로 총 대기 시간이 약간 더 길더라도 프레임이 더 적게 누락됩니다. 프레임을 삭제하는 것보다 사용자를 잠시 기다리게 하는 것이 좋습니다. 프레임이 삭제되는 경우 오류 범위가 더 작기 때문입니다. 프레임 삭제는 밀리초 단위로 발생하지만 사용자가 대기 시간을 인식하기까지는 수백 밀리초가 걸립니다.

기기마다 성능이 예측할 수 없으므로 OMT 아키텍처의 목표는 병렬 처리의 성능 이점이 아니라 위험을 줄이는 것, 즉 매우 다양한 런타임 조건에서 앱을 더 강력하게 만드는 것입니다. 복원력 향상과 UX 개선은 속도 저하의 단점을 훨씬 상쇄합니다.

도구 관련 참고사항

웹 워커는 아직 주류가 아니므로 webpackRollup과 같은 대부분의 모듈 도구는 기본적으로 이를 지원하지 않습니다. (Parcel은 지원합니다.) 다행히 웹 워커가 webpack 및 Rollup과 작동하도록 하는 플러그인이 있습니다.

요약

특히 점점 더 글로벌화되는 시장에서 앱이 최대한 안정적이고 접근하기 쉬워지도록 하려면 제약된 기기를 지원해야 합니다. 제약된 기기는 전 세계 대부분의 사용자가 웹에 액세스하는 방식이기 때문입니다. OMT는 고급 기기 사용자에게 부정적인 영향을 주지 않으면서도 이러한 기기의 성능을 높이는 유망한 방법을 제공합니다.

또한 OMT에는 다음과 같은 부수적인 이점이 있습니다.

웹 워커가 무섭게 느껴질 필요는 없습니다. Comlink와 같은 도구는 작업자의 부담을 덜어주고 다양한 웹 애플리케이션에 적합한 선택이 되고 있습니다.