기본 스레드를 벗어난 아키텍처는 앱의 안정성과 사용자 환경을 크게 개선할 수 있습니다.
지난 20년 동안 웹은 몇 가지 스타일과 이미지를 가진 정적 문서에서 복잡하고 동적인 애플리케이션으로 크게 진화했습니다. 하지만 한 가지 사실은 크게 변하지 않았습니다. 브라우저 탭당 하나의 스레드만 있으며 (일부 예외 있음) 사이트를 렌더링하고 JavaScript를 실행하는 작업을 처리합니다.
그 결과 기본 스레드가 엄청나게 과부하되었습니다. 웹 앱의 복잡성이 커짐에 따라 기본 스레드는 성능에 심각한 병목 현상을 야기합니다. 설상가상으로, 특정 사용자의 기본 스레드에서 코드를 실행하는 데 걸리는 시간은 기기 기능이 성능에 막대한 영향을 미치기 때문에 거의 완전히 예측할 수 없습니다. 이러한 예측 불가능성은 사용자가 극도로 제한된 피처폰에서 재생 빈도가 높은 고성능 플래그십 머신에 이르기까지 점점 더 다양한 기기에서 웹에 액세스할 때만 증가할 것입니다.
정교한 웹 앱이 인간의 인지와 심리에 관한 경험적 데이터를 기반으로 하는 코어 웹 바이탈과 같은 성능 가이드라인을 안정적으로 충족하도록 하려면 기본 스레드 (OMT) 외부에서 코드를 실행할 방법이 필요합니다.
왜 웹 작업자인가?
자바스크립트는 기본적으로 기본 스레드에서 태스크를 실행하는 단일 스레드 언어입니다. 그러나 웹 작업자는 개발자가 기본 스레드에서 작업을 처리할 별도의 스레드를 만들 수 있도록 하여 기본 스레드에서 일종의 이스케이프 해치를 제공합니다. 웹 작업자의 범위는 제한적이고 DOM에 대한 직접 액세스를 제공하지 않지만, 웹 작업자는 기본 스레드를 압도할 중요한 작업이 필요한 경우 매우 유용할 수 있습니다.
코어 웹 바이탈의 경우 기본 스레드에서 벗어나 작업을 실행하는 것이 도움이 될 수 있습니다. 특히 작업을 기본 스레드에서 웹 작업자로 오프로드하면 기본 스레드의 경합을 줄일 수 있으므로 페이지의 다음 페인트에 대한 상호작용 (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);
});
물론 이 접근 방식은 다소 제한적입니다. 이전에 웹 작업자는 작업 중 하나를 기본 스레드 외부로 이동하는 데 주로 사용되었습니다. 단일 웹 작업자로 여러 작업을 처리하려고 하면 금방 다루기가 어려워집니다. 매개변수뿐 아니라 메시지의 작업도 인코딩해야 하고, 요청에 대한 응답을 매칭하기 위해 부기 작업을 해야 합니다. 이러한 복잡성으로 인해 웹 작업자가 더 광범위하게 채택되지 않았을 가능성이 높습니다.
그러나 기본 스레드와 웹 작업자 간의 통신 어려움을 어느 정도 제거할 수 있다면 이 모델은 많은 사용 사례에 매우 적합할 수 있습니다. 운이 좋게도 이러한 작업을 하는 라이브러리가 있습니다.
Comlink: 웹 작업자의 업무 감소
Comlink는 postMessage
의 세부 사항에 대해 생각할 필요 없이 웹 작업자를 사용할 수 있도록 하는 라이브러리입니다. Comlink를 사용하면 스레딩을 지원하는 다른 프로그래밍 언어와 거의 마찬가지로 웹 작업자와 기본 스레드 간에 변수를 공유할 수 있습니다.
Comlink는 웹 작업자로 가져와서 기본 스레드에 노출할 함수 집합을 정의하여 설정합니다. 그런 다음 기본 스레드에서 Comlink를 가져오고, worker를 래핑하고, 노출된 함수에 액세스합니다.
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 또는 웹 오디오와 같은 많은 API에 액세스할 수 없으므로 이러한 액세스에 의존하는 앱의 일부를 작업자에 배치할 수 없습니다. 그럼에도 불구하고 작업자로 옮겨진 모든 작은 코드 조각은 사용자 인터페이스 업데이트와 같이 존재해야 하는 작업을 위해 기본 스레드에서 더 많은 헤드룸을 구매합니다.
웹 개발자에게 있어 한 가지 문제는 대부분의 웹 앱이 Vue 또는 React와 같은 UI 프레임워크에 의존하여 앱의 모든 것을 조정한다는 점입니다. 프레임워크의 구성 요소이므로 본질적으로 DOM에 연결됩니다. 이로 인해 OMT 아키텍처로의 마이그레이션이 어려워 보입니다.
그러나 UI 문제가 상태 관리와 같은 다른 문제와 분리되는 모델로 전환하면 프레임워크 기반 앱에서도 웹 작업자가 매우 유용할 수 있습니다. 이것이 PROXX에서 취한 접근 방식입니다.
PROXX: OMT 우수사례
Chrome팀은 오프라인 작업, 흥미로운 사용자 환경 등 프로그레시브 웹 앱 요구사항을 충족하는 지뢰찾기 클론으로 PROXX를 개발했습니다. 안타깝게도 이 게임의 초기 버전은 피처폰과 같은 제한된 기기에서 저조한 성능을 보였고, 이로 인해 팀은 기본 스레드에 병목 현상이 발생한다는 사실을 깨달았습니다.
팀은 웹 작업자를 사용하여 게임의 시각적 상태를 로직과 분리하기로 결정했습니다.
- 기본 스레드는 애니메이션과 전환의 렌더링을 처리합니다.
- 웹 작업자는 순전히 계산적인 게임 로직을 처리합니다.
OMT는 PROXX의 피처폰 성능에 흥미로운 영향을 미쳤습니다. OMT가 아닌 버전에서는 사용자가 UI와 상호작용한 후 6초 동안 UI가 정지됩니다. 아무런 피드백이 없으며 사용자는 다른 작업을 수행하려면 6초 동안 기다려야 합니다.
<ph type="x-smartling-placeholder">하지만 OMT 버전에서는 게임에서 UI 업데이트를 완료하는 데 12초가 걸립니다. 성능 저하처럼 보이지만 실제로는 사용자에게 더 많은 피드백으로 이어집니다. 앱이 프레임을 전혀 제공하지 않는 비 OMT 버전보다 더 많이 제공하기 때문에 속도가 느려집니다. 따라서 사용자는 무언가가 진행 중임을 알고 UI가 업데이트됨에 따라 계속 플레이할 수 있으므로 게임이 훨씬 더 나아집니다.
<ph type="x-smartling-placeholder">이는 의식적인 절충입니다. Google은 고급형 기기 사용자에게 불이익을 주지 않으면서 제한된 기기 사용자에게 더 나은 느낌을 선사합니다.
OMT 아키텍처의 영향
PROXX 예에서 알 수 있듯이 OMT를 사용하면 앱이 더 다양한 기기에서 안정적으로 실행되지만 앱 속도가 향상되지는 않습니다.
- 작업을 줄이지 않고 기본 스레드에서 작업을 이동하는 것입니다.
- 웹 작업자 간의 추가 통신 오버헤드 기본 스레드는 때때로 작업을 약간 느려지게 할 수 있습니다.
장단점 고려하기
JavaScript가 실행되는 동안 스크롤과 같은 사용자 상호작용을 기본 스레드에서 자유롭게 처리할 수 있으므로 총 대기 시간은 약간 더 길더라도 누락된 프레임이 적습니다. 사용자가 잠시 기다리게 하는 것이 프레임을 삭제하는 것보다 낫습니다. 드롭된 프레임의 오차 범위는 더 작기 때문입니다. 프레임 삭제는 밀리초 단위로 발생하지만 사용자가 대기 시간을 인지하기까지 수백 밀리초가 걸립니다.
기기 전반에 걸쳐 성능을 예측할 수 없기 때문에, OMT 아키텍처의 목표는 병렬화의 성능상의 이점이 아닌, 가시적인 런타임 조건 속에서도 앱을 더욱 견고하게 만드는 위험을 감소하는 것입니다. 복원력이 향상되고 UX가 개선되면 속도 면에서 작은 타협을 할 만한 가치가 있습니다.
도구 관련 참고사항
웹 작업자는 아직 주류가 아니기 때문에 webpack과 Rollup과 같은 대부분의 모듈 도구는 웹 작업자를 즉시 지원하지 않습니다. 하지만 Parcel은 그렇게 합니다. 다행히 다음과 같이 웹 작업자가 webpack 및 Rollup과 작동하도록 하는 플러그인이 있습니다.
- webpack용 worker-plugin
- Rollup용 rollup-plugin-off-main-thread
요약
특히 점점 더 글로벌화되고 있는 마켓플레이스에서 앱의 안정성과 접근성을 최대한 높이려면 제한된 기기를 지원해야 합니다. 이러한 기기는 전 세계 대다수 사용자가 웹에 액세스하는 데 사용하는 수단입니다. OMT는 고급형 기기 사용자에게 부정적인 영향을 주지 않으면서 이러한 기기에서 실적을 개선할 수 있는 좋은 방법을 제공합니다.
또한 OMT에는 다음과 같은 부차적인 이점이 있습니다.
- JavaScript 실행 비용을 별도의 스레드로 이동합니다.
- 파싱 비용을 이동시킵니다. 즉, UI가 더 빠르게 부팅될 수 있습니다. 이렇게 하면 콘텐츠가 포함된 첫 페인트가 줄어들 수 있습니다. 또는 상호작용까지의 시간 이를 통해 광고 수익을 Lighthouse 점수입니다.
웹 작업자는 겁을 먹지 않아도 됩니다. Comlink와 같은 도구는 작업 부담을 덜어주고 다양한 웹 애플리케이션에 적용할 수 있는 실용적인 선택을 하고 있습니다.