paint-brush
Web Worker를 사용하여 Angular 앱 속도를 높이는 방법~에 의해@mesciusinc
178 판독값

Web Worker를 사용하여 Angular 앱 속도를 높이는 방법

~에 의해 MESCIUS inc.16m2024/06/26
Read on Terminal Reader

너무 오래; 읽다

Angular Web Worker에 장기 실행 프로세스를 추가하고 정지되지 않는 앱의 모든 이점을 활용하는 방법을 알아보세요.
featured image - Web Worker를 사용하여 Angular 앱 속도를 높이는 방법
MESCIUS inc. HackerNoon profile picture

웹 작업자 가 필요합니까? 웹 작업자는 웹 애플리케이션의 코드 구성 요소입니다. 이를 통해 개발자는 JavaScript 작업에 대한 새로운 실행 스레드를 생성할 수 있으므로 메인 앱의 실행이 중단되지 않습니다.


언뜻 보면 브라우저는 본질적으로 스레딩을 지원하므로 개발자가 특별한 작업을 수행할 필요가 없는 것처럼 보일 수 있습니다. 불행히도 그렇지 않습니다. Web Workers는 실제 동시성 문제를 해결합니다.


Web Worker는 웹 브라우저에서 예상되는 기능 표준의 일부이며 이에 대한 사양은 W3C 에서 작성되었습니다. Angular 프레임워크는 우리를 위해 Web Workers를 래핑했으며 Angular 명령줄 인터페이스(CLI)를 사용하여 이를 앱에 쉽게 추가할 수 있습니다.


이 기사에서는 먼저 브라우저에서 JavaScript를 사용한 스레드 동시성에 대한 몇 가지 오해를 살펴보겠습니다. 그런 다음 웹 사이트에서 동시 스레딩을 가능하게 하는 Angular를 사용하여 Web Workers를 구현하는 것이 얼마나 쉬운지 보여주는 기능적 예제를 만들어 보겠습니다.

JavaScript는 본질적으로 동시성이 아닌가?

일부 개발자는 브라우저가 웹 사이트에 연결하고 페이지의 HTML을 검색할 때 여러 연결(약 6개)을 열고 리소스(이미지, 연결된 CSS 파일, 연결된 JavaScript 파일, 등)을 동시에 진행합니다. 브라우저는 (컨텍스트 전환을 통해) 여러 스레드와 수많은 작업을 동시에 실행하는 것처럼 보입니다.


초보 웹 개발자에게 이는 브라우저가 동시 작업을 수행할 수 있음을 나타내는 것처럼 보입니다. 그러나 JavaScript의 경우 브라우저는 실제로 한 번에 하나의 프로세스만 실행합니다.


대부분의 최신 웹사이트, SPA( 단일 페이지 앱 ) 및 최신 PWA( 프로그레시브 웹 앱 )는 JavaScript에 의존하며 일반적으로 수많은 JavaScript 모듈을 포함합니다. 그러나 웹 앱이 JavaScript를 실행할 때마다 브라우저는 단일 활동 스레드로 제한됩니다. 정상적인 상황에서는 JavaScript가 동시에 실행되지 않습니다.


즉, JavaScript 모듈 중 하나에 정의된 장기 실행 작업이나 프로세스 집약적인 작업이 있는 경우 사용자는 앱이 끊기거나 중단되는 것처럼 보일 수 있습니다. 동시에 브라우저는 사용자 인터페이스(UI)가 업데이트되기 전에 프로세스가 완료될 때까지 기다립니다. 이러한 종류의 행동으로 인해 사용자는 웹 앱이나 SPA에 대한 신뢰를 잃게 되며 우리 중 누구도 이를 원하지 않습니다.

JavaScript 동시성을 위한 웹 워커

웹 앱에서 동시 JavaScript 스레드를 실행할 수 있도록 두 개의 창으로 구성된 예제 페이지를 만듭니다. 한 창에서 애플리케이션의 UI는 원이 있는 그리드로 표시되며, 이는 지속적으로 그림을 업데이트하고 마우스 클릭에 반응합니다. 두 번째 창에서는 일반적으로 UI 스레드를 차단하고 UI가 해당 작업을 수행하지 못하도록 하는 장기 실행 프로세스를 호스팅합니다.


동시 JavaScript 스레드


UI를 반응형으로 만들기 위해 Web Worker에서 긴 프로세스를 실행합니다. Web Worker는 이를 별도의 스레드에서 실행하고 이러한 방식으로 UI 논리 실행을 차단하지 않습니다. Angular를 앱 구축을 위한 프레임워크로 사용하면 Web Workers를 간단한 단일 명령 작업으로 구성할 수 있기 때문입니다.

Angular 앱 설정

Angular CLI를 사용하려면 Node.jsNPM (Node Package Manager)이 설치되어 있어야 합니다. Node와 NPM이 설치되었는지 확인한 후 콘솔 창을 열고 NPM을 통해 Angular CLI를 설치합니다(일회성 작업임).


 npm install -g @angular/cli


디렉터리를 새 앱 템플릿을 생성하려는 대상 디렉터리로 변경합니다. 이제 앱을 만들 준비가 되었습니다. 이를 위해 “ng new” 명령을 사용합니다. 프로젝트 이름을 NgWebWorker로 지정하겠습니다.


 ng new NgWebWorker --no-standalone


프로젝트 마법사는 프로젝트에 라우팅을 포함할지 묻습니다. 이 예에서는 라우팅이 필요하지 않으므로 n을 입력합니다.


그런 다음 어떤 유형의 스타일시트 형식을 사용할 것인지 묻습니다. Angular는 Sass 및 Less와 같은 스타일시트 프로세서 사용을 지원하지만, 이 경우에는 간단한 CSS를 사용하므로 기본값으로 Enter를 누르기만 하면 됩니다.


스타일시트 형식


그런 다음 NPM이 필수 패키지를 가져오고 CLI가 템플릿 프로젝트를 생성할 때 일부 CREATE 메시지가 표시됩니다. 마지막으로 완료되면 명령줄에 커서가 다시 깜박입니다.


이 시점에서 Angular CLI는 프로젝트를 생성하여 NgWebWorker 폴더에 배치했습니다. 디렉터리를 NgWebWorker로 변경합니다. Angular CLI는 Node HTTP 서버 설치를 포함하여 Angular 프로젝트 작업에 필요한 모든 것을 생성했습니다. 즉, 템플릿 앱을 시작하기 위해 해야 할 일은 다음과 같습니다.


 ng serve 


각도 CLI


Angular CLI는 프로젝트를 컴파일하고 Node HTTP 서버를 시작합니다. 이제 <a href="http://localhost:4200"target="_blank"> URL을 지정하여 브라우저에 앱을 로드할 수 있습니다.

CLI의 첫 번째 Angular 앱

페이지를 로드하면 상단에 프로젝트 이름이 있는 기본 템플릿이 표시됩니다.


"ng Serve"를 실행하면 코드가 변경되면 자동으로 브라우저에서 사이트가 새로 고쳐지므로 변경 사항이 적용되는 것을 훨씬 쉽게 확인할 수 있다는 이점이 있습니다.


우리가 집중할 코드의 대부분은 /src/app 디렉토리 아래에 있습니다.


/src/app 디렉토리


app.comComponent.html에는 현재 기본 페이지를 표시하는 데 사용되는 하나의 구성 요소에 대한 HTML이 포함되어 있습니다. 구성 요소 코드는 app.comComponent.ts (TypeScript) 파일에 표시됩니다.


app.comComponent.html의 내용을 삭제하고 자체 레이아웃을 추가하겠습니다. 왼쪽에는 장기 실행 프로세스 값을 표시하고 오른쪽에는 임의의 원을 그리는 분할 페이지를 만듭니다. 이를 통해 브라우저는 독립적으로 작동하는 두 개의 추가 Web Worker 스레드를 실행할 수 있으므로 Angular Web Worker가 실제로 작동하는 모습을 볼 수 있습니다.


기사의 나머지 부분에 대한 모든 코드는 GitHub 저장소에서 얻을 수 있습니다.


다음은 임의의 원을 그리는 동안(장기 실행 프로세스가 시작되기 전) 최종 페이지의 모습입니다.


Web Workers로 Angular 앱 속도 향상


app.comComponent.html 의 코드를 다음으로 바꿉니다.


 <div id="first"> <div class="innerContainer"> <button (click)="longLoop()">Start Long Process</button> </div> <div class="innerContainer"> <textarea rows="20" [value]="(longProcessOutput)"></textarea> </div> </div>


코드 다운로드에는 UI의 매우 간단한 형식 지정에 사용되는 styles.css 의 일부 ID와 CSS 클래스 및 관련 스타일도 포함되어 있으므로 두 개의 섹션(왼쪽 및 오른쪽)과 기타 기본 스타일이 있습니다.


 .container { width: 100%; margin: auto; padding: 1% 2% 0 1%; } .innerContainer{ padding: 1%; } #first { width: 50%; height: 405px; float:left; background-color: lightblue; color: white; } #second { width: 50%; float: right; background-color: green; color: white; }


여기서 주목해야 할 중요한 점은 버튼에 Angular 이벤트 바인딩(클릭)을 추가했다는 것입니다. 사용자가 버튼을 클릭하면 구성 요소 TypeScript 파일 app.comComponent.ts 에 있는 longLoop 메서드를 호출하여 긴 프로세스가 시작됩니다.


 title = 'NgWebWorker'; longProcessOutput: string = 'Long\nprocess\noutput\nwill\nappear\nhere\n'; fibCalcStartVal: number; longLoop() { this.longProcessOutput = ''; for (var x = 1; x <= 1000000000; x++) { var y = x / 3.2; if (x % 20000000 == 0) { this.longProcessOutput += x + '\n'; console.log(x); } } }


이는 구성 요소 longProcessOutput의 멤버 변수에 쓰기를 100억 번 반복 실행합니다.


app.comComponent.html (textarea 요소)에서 해당 멤버 변수를 바인딩했기 때문에 UI는 변수가 업데이트될 때마다 업데이트를 반영합니다. HTML에서 설정한 값은 멤버 변수를 바인딩하는 위치입니다.


 <textarea rows="20" [value]="longProcessOutput"></textarea>


실행하세요. 버튼을 클릭해도 아무 일도 일어나지 않고 갑자기 텍스트 영역이 여러 값으로 업데이트되는 것을 볼 수 있습니다. 콘솔을 열면 코드가 실행될 때 거기에 기록된 값을 볼 수 있습니다.

Angular CLI를 사용하여 임의의 원 구성 요소 추가

다음으로 "원" 구성 요소를 추가하여 임의의 원을 그립니다. 다음 명령으로 Angular CLI를 사용하여 이를 수행할 수 있습니다.


 ng generate component circle


이 명령은 Circle이라는 새 폴더를 만들고 4개의 새 파일을 만들었습니다.

  • 원.컴포넌트.html

  • Circle.comComponent.spec.ts (단위 테스트)

  • Circle.comComponent.ts (TypeScript 코드)

  • Circle.comComponent.css (이 구성 요소의 관련 HTML에만 적용되는 스타일)


구성요소 원 생성


HTML은 간단합니다. 구성 요소를 나타내는 HTML만 있으면 됩니다. 우리의 경우 이는 페이지 오른쪽에 있는 구성 요소로 연한 녹색 격자를 표시하고 원을 그립니다. 이 그리기는 HTML Canvas 요소를 통해 수행됩니다.


 <div id="second"> <canvas #mainCanvas (mousedown)="toggleTimer()"></canvas> </div>


mousedown 이벤트를 잡기 위해 Angular 이벤트 바인딩을 추가하여 원 그리기를 시작하고 중지합니다. 사용자가 캔버스 영역 내부의 아무 곳이나 클릭하면 프로세스가 아직 시작되지 않은 경우 원이 그리기 시작됩니다. 프로세스가 이미 시작된 경우에는ggleTimer 메서드( circle.comComponent.ts 에 있음)가 그리드를 지우고 원 그리기를 중지합니다.


ToggleTimer는 단순히 setInterval을 사용하여 100밀리초(초당 10개의 원)마다 무작위로 선택된 색상으로 무작위 위치에 원을 그립니다.


 toggleTimer(){ if (CircleComponent.IntervalHandle === null){ CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles,50); } else{ clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } }


Circle.comComponent.ts 에는 Canvas 요소를 설정하고, 멤버 변수를 초기화하고, 그리기를 수행하는 더 많은 코드가 있습니다. 추가되면 코드는 다음과 같아야 합니다.


 import { ViewChild, Component, ElementRef, AfterViewInit } from '@angular/core'; @Component({ selector: 'app-circle', templateUrl: './circle.component.html', styleUrl: './circle.component.css', }) export class CircleComponent implements AfterViewInit { title = 'NgWebWorker'; static IntervalHandle = null; static ctx: CanvasRenderingContext2D; GRID_LINES: number = 20; lineInterval: number = 0; gridColor: string = 'lightgreen'; static CANVAS_SIZE: number = 400; @ViewChild('mainCanvas', { static: false }) mainCanvas: ElementRef; constructor() { console.log('ctor complete'); } ngAfterViewInit(): void { CircleComponent.ctx = (<HTMLCanvasElement>( this.mainCanvas.nativeElement )).getContext('2d'); this.initApp(); this.initBoard(); this.drawGrid(); this.toggleTimer(); } initApp() { CircleComponent.ctx.canvas.height = CircleComponent.CANVAS_SIZE; CircleComponent.ctx.canvas.width = CircleComponent.ctx.canvas.height; } initBoard() { console.log('initBoard...'); this.lineInterval = Math.floor( CircleComponent.ctx.canvas.width / this.GRID_LINES ); console.log(this.lineInterval); } drawGrid() { console.log('drawGrid...'); CircleComponent.ctx.globalAlpha = 1; // fill the canvas background with white CircleComponent.ctx.fillStyle = 'white'; CircleComponent.ctx.fillRect( 0, 0, CircleComponent.ctx.canvas.height, CircleComponent.ctx.canvas.width ); for (var lineCount = 0; lineCount < this.GRID_LINES; lineCount++) { CircleComponent.ctx.fillStyle = this.gridColor; CircleComponent.ctx.fillRect( 0, this.lineInterval * (lineCount + 1), CircleComponent.ctx.canvas.width, 2 ); CircleComponent.ctx.fillRect( this.lineInterval * (lineCount + 1), 0, 2, CircleComponent.ctx.canvas.width ); } } toggleTimer() { if (CircleComponent.IntervalHandle === null) { CircleComponent.IntervalHandle = setInterval(this.drawRandomCircles, 100); } else { clearInterval(CircleComponent.IntervalHandle); CircleComponent.IntervalHandle = null; this.drawGrid(); } } static generateRandomPoints() { var X = Math.floor(Math.random() * CircleComponent.CANVAS_SIZE); // gen number 0 to 649 var Y = Math.floor(Math.random() * CircleComponent.CANVAS_SIZE); // gen number 0 to 649 return { x: X, y: Y }; } drawRandomCircles() { var p = CircleComponent.generateRandomPoints(); CircleComponent.drawPoint(p); } static drawPoint(currentPoint) { var RADIUS: number = 10; var r: number = Math.floor(Math.random() * 256); var g: number = Math.floor(Math.random() * 256); var b: number = Math.floor(Math.random() * 256); var rgbComposite: string = 'rgb(' + r + ',' + g + ',' + b + ')'; CircleComponent.ctx.strokeStyle = rgbComposite; CircleComponent.ctx.fillStyle = rgbComposite; CircleComponent.ctx.beginPath(); CircleComponent.ctx.arc( currentPoint.x, currentPoint.y, RADIUS, 0, 2 * Math.PI ); // allPoints.push(currentPoint); CircleComponent.ctx.stroke(); CircleComponent.ctx.fill(); } }


index.html 파일에 원 구성요소를 추가하는 것을 잊지 마세요.


 <body> <app-root></app-root> <app-circle></app-circle> </body>


페이지가 로드되면 원이 그리기 시작합니다. [긴 프로세스 시작] 버튼을 클릭하면 그리기가 일시 중지되는 것을 볼 수 있습니다. 모든 작업이 동일한 스레드에서 수행되기 때문입니다.


Web Worker를 추가하여 이 문제를 해결해 보겠습니다.

Angular 웹 작업자 추가

CLI를 사용하여 새 웹 작업자를 추가하려면 프로젝트 폴더로 이동하여 다음 명령을 실행하면 됩니다.


 ng generate web-worker app


마지막 매개변수(앱)는 웹 작업자에 배치할 장기 실행 프로세스가 포함된 구성 요소의 이름입니다.


웹 작업자 앱 생성


Angular는 다음과 같은 몇 가지 코드를 app.comComponent.ts 에 추가합니다.


 if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. }


새로운 코드는 무엇을 합니까? 이 코드는 명령이 추가한 새로운 app.worker 구성 요소를 참조하는 것을 볼 수 있습니다. 이 시점에서 코드는 다음과 같습니다.


  1. 브라우저가 웹 작업자를 지원하는지 확인합니다.
  2. 새 작업자를 만듭니다.
  3. 작업자에게 메시지를 게시합니다( app.worker.ts 에 있음).
  4. 작업자가 "hello" 메시지를 받으면 EventListener가 실행됩니다(다음 코드 조각 참조).
  5. EventListener가 실행되면( app.worker.ts 에서) 응답 객체를 생성하여 호출자에게 다시 게시합니다.


app.worker.ts 의 전체 내용은 다음과 같습니다.


 /// <reference lib="webworker" /> addEventListener('message', ({ data }) => { const response = `worker response to ${data}`; postMessage(response); });


이러한 단계의 결과로 콘솔에 메시지가 표시되며 이는 다음 콘솔 출력의 마지막 줄과 같습니다.


콘솔 출력


이는 원래 Worker 객체에 생성된 EventHandler에서 발생하는 console.log입니다.


 worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); };


이는 app.comComponent가 app.worker에 메시지를 게시했고 app.worker가 자체 메시지로 응답했음을 알려줍니다.


원 그리기 코드가 중단되지 않도록 작업자를 사용하여 다른 스레드에서 장기 실행 프로세스를 실행하려고 합니다.


먼저 UI 요소와 관련된 코드를 app.comComponent 클래스의 생성자로 이동해 보겠습니다.


 constructor() { if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); this.longProcessOutput += `page got message: ${data}` + '\n'; }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. } }


이를 통해 이제 longProcessOutput 변수를 참조할 수 있습니다. 이를 통해 해당 변수에 액세스할 수 있습니다. 초기 테스트처럼 콘솔에 쓰는 대신 기본 데이터를 텍스트 영역에 추가하는 Worker.onmessage가 있습니다.


왼쪽에 강조 표시된 텍스트가 수신된 메시지임을 확인할 수 있습니다.


받은 메시지

웹 워커의 LongLoop

루프가 실행될 때 자체 스레드에서 실행되도록 하려면 장기 실행 루프를 Web Worker로 이동해야 합니다.


최종 app.comComponent.ts 에 포함될 대부분의 코드는 다음과 같습니다.


 constructor() { if (typeof Worker !== 'undefined') { // Create a new const worker = new Worker(new URL('./app.worker', import.meta.url)); worker.onmessage = ({ data }) => { console.log(`page got message: ${data}`); this.longProcessOutput += `page got message: ${data}` + '\n'; }; worker.postMessage('hello'); } else { // Web Workers are not supported in this environment. // You should add a fallback so that your program still executes correctly. } } longLoop() { this.longProcessOutput = ''; for (var x = 1; x <= 1000000000; x++) { var y = x / 3.2; if (x % 20000000 == 0) { this.longProcessOutput += x + '\n'; console.log(x); } } }


작업자 변수를 이제 멤버 변수인 클래스로 옮겼습니다. 이렇게 하면 AppComponent 클래스 어디에서나 쉽게 참조할 수 있습니다.


다음으로 생성자의 코드를 사용하여 작업자 객체에 메시지 이벤트 핸들러를 정의한 방법을 더 자세히 살펴보겠습니다.


 this.worker.onmessage = ({ data }) => { this.longProcessOutput += `${data}` + "\n"; };


해당 코드는 Web Worker 클래스( app.worker.ts 에 있음)가 postMessage(data)를 호출할 때 실행됩니다. postMessage 메소드가 호출될 때마다 longProcessOutput(텍스트 영역에 바인딩된 모델)은 데이터와 캐리지 리턴("\n")으로 업데이트됩니다. 텍스트 영역 요소.


실제 웹 작업자( app.worker.ts )에 있는 모든 코드는 다음과 같습니다.


 addEventListener('message', ({ data }) => { console.log(`in worker EventListener : ${data}`); for (var x = 1; x <=1000000000;x++){ var y = x/3.2; if ((x % 20000000) == 0){ // posts the value back to our worker.onmessage handler postMessage(x); // don't need console any more --> console.log(x); } } });


이 이벤트 핸들러는 사용자가 [긴 프로세스 실행] 버튼을 클릭하면 시작됩니다. Web Worker( app.worker.ts )에 있는 모든 코드는 새 스레드에서 실행됩니다. 이것이 바로 Web Worker의 가치입니다. 해당 코드는 별도의 스레드에서 실행됩니다. 이것이 더 이상 웹 앱의 메인 스레드에 영향을 미치지 않는 이유입니다.


이제 longLoop 메서드에 다음 코드가 있으므로 사용자가 버튼을 클릭하면 Web Worker 코드가 실행됩니다.


 longLoop(){ this.longProcessOutput = ""; // the following line starts the long process on the Web Worker // by sending a message to the Web Worker this.worker.postMessage("start looping..."); }


메시지가 워커에 게시되면 EventListener는 거기에 배치한 원래 longLoop의 코드를 실행하고 실행합니다.

Web Workers로 Angular 앱 마무리하기

앱을 실행하면 [긴 프로세스 시작] 버튼을 클릭해도 더 이상 원 그리기가 일시 중지되지 않는 것을 확인할 수 있습니다. 또한 원 그리기 Canvas 구성 요소와 직접 상호 작용할 수 있으므로 longLoop이 계속 실행되는 동안 클릭하면 Canvas가 즉시 다시 그려집니다. 이전에는 이렇게 하면 앱이 정지된 것처럼 작동했습니다.


이제 장기 실행 프로세스를 Angular Web Worker에 추가하고 정지되지 않는 앱의 모든 이점을 누릴 수 있습니다.


JavaScript Web Workers로 해결된 예제를 보려면 Plunker 에서 볼 수 있습니다.


프레임워크에 구애받지 않는 UI 구성 요소를 찾고 계십니까? MESCIUS에는 데이터 그리드, 차트, 게이지 및 입력 컨트롤을 포함한 완전한 JavaScript UI 구성 요소 세트가 있습니다. 또한 강력한 스프레드시트 구성 요소 , 보고 제어 기능향상된 프리젠테이션 보기 도 제공합니다.


우리는 Angular (React 및 Vue는 물론)에 대한 깊은 지원을 받고 있으며 최신 JavaScript 프레임워크에서 사용할 수 있도록 구성 요소를 확장하는 데 전념하고 있습니다.