동기식 코드에서 오래 걸리는 함수를 호출했을 때 화면이 멈추는 현상은 프로그램이 단일 스레드이기 때문이다.
단일 스레드는 한 번에 한 가지 작업만 할 수 있다.
따라서 장시간 실행 중인 함수의 작업이 끝나기를 기다릴수 밖에 없다.
Worker는 일부 작업을 다른 스레드에서 실행할 수 있는 기능을 제공하므로 작업을 시작한 다음 다른 작업을 계속 할 수 있다.
하지만 멀티 스레드 코드를 사용하면 추가된 스레드가 언제 중단되는지, 그리고 언제 메인 스레드로 돌아와 실행되는지 알 수 없다.
따라서 두 스레드 모두 동일한 변수에 액세스할 수 있는 경우 언제든지 변수가 예기치 않게 변경될 수 있으며 이로 인해 찾기 어려운 버그가 발생한다.
웹에서는 이러한 문제를 방지하기 위해서 메인 코드와 Worker 코드는 서로의 변수에 직접 액세스하지 못하도록 되어있다.
Worker와 메인 코드는 완전히 분리된 세계에서 실행되며 서로 메시지를 보내는 것만으로 상호 작용한다.
이는 Worker가 DOM에 액세스하지 못한다는걸 뜻한다.
Worker는 세 가지로 분류된다.
dedicated workers
shared workers
service workers
여기서는 dedicated workers에 대해서만 알아보도록 한다.
비효율적으로 소수를 계산하는 코드를 보자.
function generatePrimes(quota) {
function isPrime(n) {
for (let c = 2; c <= Math.sqrt(n); ++c) {
if (n % c === 0) {
return false;
}
}
return true;
}
const primes = [];
const maximum = 1000000;
while (primes.length < quota) {
const candidate = Math.floor(Math.random() * (maximum + 1));
if (isPrime(candidate)) {
primes.push(candidate);
}
}
return primes;
}
document.querySelector('#generate').addEventListener('click', () => {
const quota = document.querySelector('#quota').value;
const primes = generatePrimes(quota);
document.querySelector('#output').textContent = `Finished generating ${quota} primes!`;
});
document.querySelector('#reload').addEventListener('click', () => {
document.querySelector('#user-input').value = 'Try typing in here immediately after pressing "Generate primes"';
document.location.reload();
});
generatePrime()
함수를 호출한 이후에 프로그램이 멈춘다.
디렉토리의 구성을 다음과 같이 만들자.
index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Prime numbers</title>
<script src="main.js" defer></script>
<link href="style.css" rel="stylesheet" />
</head>
<body>
<label for="quota">Number of primes:</label>
<input type="text" id="quota" name="quota" value="1000000" />
<button id="generate">Generate primes</button>
<button id="reload">Reload</button>
<textarea id="user-input" rows="5" cols="62">
Try typing in here immediately after pressing "Generate primes"
</textarea>
<div id="output"></div>
</body>
</html>
style.css
textarea {
display: block;
margin: 1rem 0;
}
main.js
generate.js
main.js 와 generate.js 는 빈 파일이다.
main.js에는 메인 코드를, generate.js에는 Worker 코드를 추가할 것이다.
다시 돌아와서 일단, 우리는 Worker 코드가 메인 코드와 별도의 스크립트로 생성되는 것을 볼 수 있다.
또한 index.html을 보면 메인 코드만 포함되어 있음을 알 수 있다.
main.js에 다음 코드를 추가하자.
// Create a new worker, giving it the code in "generate.js"
const worker = new Worker('./generate.js');
// When the user clicks "Generate primes", send a message to the worker.
// The message command is "generate", and the message also contains "quota",
// which is the number of primes to generate.
document.querySelector('#generate').addEventListener('click', () => {
const quota = document.querySelector('#quota').value;
worker.postMessage({
command: 'generate',
quota,
});
});
// When the worker sends a message back to the main thread,
// update the output box with a message for the user, including the number of
// primes that were generated, taken from the message data.
worker.addEventListener('message', (message) => {
document.querySelector('#output').textContent = `Finished generating ${message.data} primes!`;
});
document.querySelector('#reload').addEventListener('click', () => {
document.querySelector('#user-input').value = 'Try typing in here immediately after pressing "Generate primes"';
document.location.reload();
});
먼저, Worker() constructor를 사용하여 Worker를 생성한다.
Worker 스크립트를 가리키는 URL을 전달한다.
Worker가 생성되는 즉시 Worker 스크립트가 실행된다.
클릭 이벤트 핸들러를 "Generate primes" 버튼에 등록한다.
그러나 generatePrime()
함수를 호출하는 것이 아니라 worker.postMessage()
를 이용하여 worker에게 메시지를 보낸다.
2개의 프로퍼티를 가진 JSON 객체가 메시지로서 worker에게 전달된다.
command
: worker가 해야할 일을 알려주는 문자열 형식의 명령quota
: 생성할 소수의 개수Worker에게 message 이벤트 핸들러를 등록한다.
이는 Worker가 작업이 완료된 시점을 알려주고 결과 데이터를 전달할 수 있도록 하기 위한 것이다.
핸들러가 메시지의 데이터 프로퍼티로부터 데이터를 얻어와서 output 엘리먼트에 기록한다.
generate.js에 다음의 코드를 추가하자.
// Listen for messages from the main thread.
// If the message command is "generate", call `generatePrimes()`
addEventListener("message", (message) => {
if (message.data.command === 'generate') {
generatePrimes(message.data.quota);
}
});
// Generate primes (very inefficiently)
function generatePrimes(quota) {
function isPrime(n) {
for (let c = 2; c <= Math.sqrt(n); ++c) {
if (n % c === 0) {
return false;
}
}
return true;
}
const primes = [];
const maximum = 1000000;
while (primes.length < quota) {
const candidate = Math.floor(Math.random() * (maximum + 1));
if (isPrime(candidate)) {
primes.push(candidate);
}
}
// When we have finished, send a message to the main thread,
// including the number of primes we generated.
postMessage(primes.length);
}
이 코드는 main.js가 worker 인스턴스를 생성하는 즉시 실행된다.
worker가 가장 먼저 하는 일은 main.js 스크립트의 메시지를 듣기 시작하는 것이다.
worker의 전역 함수인 addEventListener()
를 사용하여 이 작업을 수행한다.
message 이벤트 핸들러에 전달된 message 이벤트객체의 data
프로퍼티는 main.js 스크립트에서 전달된 argument의 복사본이다.
메인 스크립트로부터 전달된 command
가 "generate"이라면 command
와 같이 전달된 quota
를 generatePrimes()
에 전달하여 호출한다.
generatePrimes()
함수는 완료시 값을 반환하는 대신 메인 스크립트에 메시지를 보낸다.
지금까지 살펴본 Worker는 dedicated worker이다.
즉, 단일 스크립트 인스턴스(main.js)에서 사용된다.
위에서 언급했다 시피 다른 유형의 worker가 있다.
Shared workers : 서로 다른 창에서 실행되는 여러 스크립트끼리 worker를 공유할 수 있다.
Service workers : 사용자가 오프라인일 때 웹 응용프로그램이 작동할 수 있도록 리소스를 캐싱하는 프록시 서버역할을 한다.
이는 Progressive Web Apps(PWA)의 핵심 구성요소이다.
[참고] : MDN