자바스트립트는 한번에 하나만 실행할 수 있는 단일 스레드 프로그래밍 언어입니다.. 자바스크립트 엔진은 한개의 스레드로 한번에 한개의 문장(statement)만 실행할 수 있습니다.
단일 스레드 언어는 동시성 문제에 관해 신경쓰지 않아도 되기 때문에 코드를 작성하기 수월하지만, 이것은 또한 주 스레드의 중단 없이 네트워크 접근과 같은 긴 시간이 걸리는 문장을 실행할 수 없다는 의미이기도 합니다..
API로부터 몇가지 데이터를 요청한다고 상상해봅시다. 상황에 따라 서버는 주 스레드를 중단시켜 화면의 반응이 없도록 만들고 요청을 처리하는데 시간이 더 걸릴 수 있습니다.
이 부분이 바로 비동기 자바스크립가 동작하는 부분입니다. 비동기 자바스크립트의 사용으로(콜백, 프로미스, async/awit 과 같은), 주 스레드의 차단없이 시간이 오래 걸리는 네트워크 요청을 처리할 수 있습니다.
훌륭한 자바스크립트 개발자가 되기위해 이에 관련된 모든 개념을 학습하 필요는 없지만 알아두면 도움이 됩니다. :)
더이상 고민하지 않고 시작합니다.
Tip: Bit을 사용하여 JS 코드를 API로 변환할 수 있고 이를 프로젝트와 앱에서 공유하고 사용하고 동기화하여 더 빨리 만들고 더 많은 코드를 재사용할 수 있습니다. 시도해 보세요!
동기 자바스크립트는 어떻게 작동할까?
비동기 자바스크립트에 대해 알아보기 전에 먼저 동기 자바스크립트 코드는 자바스크립트 엔진에서 어떻게 작동하는지 이해해봅시다.
예시
const second = () => {
console.log('Hello there!');
}
const first = () => {
console.log('Hi there!');
second();
console.log('The End');
}
first();
위의 코드가 자바스크립트 엔진에서 어떻게 실행되는지 이해하기 위해, 우리는 실행 컨텍스트(execution context)와 콜 스택(call stack)의 개념에 대해 이해할 필요가 있습니다.
실행 컨텍스트(Execution Context)
실행 컨텍스트는 자바스크립트 코드가 평가되고 실행되는 환경에 대한 추상적인 개념입니다. 어떠한 자바스크립트 코드가 실행되더라도 실행 컨텍스트 내에서 실행됩니다.
함수 코드는 함수 실행 컨텍스트 내에서 실행되고 전역 코드는 전역 실행 컨텍스트 내에서 실행 됩니다. 각 함수는 자기 자신의 실행 컨텍스트를 가집니다.
콜 스택(Call Stack)
콜 스택은 이름이 의미하듯이 LIFO(Last In, First Out) 구조를 가지는 스택이며 코드가 실행될 때 생선되는 모든 실행 컨텍스트를 저장할 때 사용합니다.
자바스크립트는 단일 스레드 프로그래밍 언어이기 때문에 한개의 콜 스택을 가지고 있습니다. 콜 스택은 LIFO 구조이기 때문에 항목들은 스택의 가장 위에서만 추가되거나 제거될 수 있습니다.
이제 위의 코드 스니펫으로 돌아가서 자바스크립트 엔진에서 코드가 어떻게 동작하는지 이해하려고 해봅시다.
const second = () => {
console.log('Hello there!');
}
const first = () => {
console.log('Hi there!');
second();
console.log('The End');
}
first();
자, 여기서 어떤일이 일어나고 있나요?
이 코드가 실행될 때, 전역 실행 컨텍스트가(main()으로 표시된) 생성이 되고 콜 스택의 가장 위에 들어갑니다. first()가 호출되고 콜 스택의 가장 위로 들어갑니다.
그 다음, console.log('Hi there') 이 스택의 가장위로 들어가고 실행이 끝나면 스택에서 빠져나옵니다. 그리고나서 second()를 호출하고 함수가 스택의 가장위로 들어갑니다.
console.log('Hello there!') 이 스택의 가장위로 들어왔다가 완료하고 빠져나갑니다. second() 함수가 끝나고 이것 또한 스택에서 빠져나갑니다.
console.log(‘The End’) 가 스택에 들어갔다가 완료되고나서 나옵니다. 그 후에 first() 함수가 완료되고 스택에서 제거됩니다.
프로그램은 여기서 실행이 완료되고 전역 실행 컨텍스트(main())가 스택에서 나오게 됩니다.
비동기 자바스크립트는 어떻게 작동할까요?
현재 우리는 콜 스택과 동기 자바스크립트가 어떻게 동작하는지에 대한 기본 개념을 가지게 되었습니다. 이제 비동기 자바스크립트로 돌아가 봅시다!
중단(Blocking)이 뭔가요?
이미지 처리나 네트워크 요청을 동기 방식으로 수행 한다고 가정해 봅시다.
const processImage = (image) => {
/**
* doing some operations on image
**/
console.log('Image processed');
}
const networkRequest = (url) => {
/**
* requesting network resource
**/
return someData;
}
const greeting = () => {
console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();
이미치 처리와 네트워크 요청을 하는 것은 시간이 걸립니다. 그래서 processImage() 함수를 호출 할 때 이미즈 사이즈에 따라 시간이 걸리게 됩니다.
processImage() 함수가 완료되고 나면 스택에서 제거되고, 이후에 networkRequest() 함수가 호출되고 스택에 들어갑니다. 또 다시 실행이 완료되기 까지 시간이 걸리게 됩니다.
마침내 networkRequest() 함수가 완료되면 greeting() 함수가 호출되고 이 함수는 console.log 문만 포함하고 console.log 문은 보통 빨리 끝나기 때문에 greeting() 함수는 즉시 실행되고 종료됩니다.
보다시피 함수(processImage() 또는 networkRequest())가 완료될 때 까지 기다려야 합니다. 이는 이러한 함수들이 콜스택 또는 주 스레드를 중단(blocking)시킨다는 것을 의미합니다. 그래서 우리는 위의 코드가 전부 실행될 때까지 다른 어떠한 작업도 실행할 수 없게 되고 이는 이상적이지 않습니다.
그렇다면 해결 방법이 무엇 일까요?
가장 간단한 해결방법은 비동기 콜백입니다. 우리는 우리 코드를 멈추지 않도록(non-blocking) 만들기 위해서 비동기 콜백을 사용합니다.
예시
const networkRequest = () => {
setTimeout(() => {
console.log('Async Code');
}, 2000);
};
console.log('Hello World');
networkRequest();
여기서 저는 네트워크 요청을 표현하기 위해 setTimeout 메서드를 사용하였습니다. setTimeout 은 자바스크립트 엔진의 일부가 아니라 웹 API나 C/C++ API의 일부라는 점을 유의해 주세요.
이 코드가 어떻게 실행되는지 이해하기 위해서 우리는 이벤트 루프와 콜백 큐(태스크 큐 또는 메세지 큐로 알려진)의 몇가지 개념에 대해 이해 할 필요가 있습니다.
이벤트 루프, web APIs 그리고 메세지 큐/태스크 큐는 자바스크립트 엔진의 일부가 아니고, 브라우저의 자바스크립트 런타임 환경 또는 Node.js 자바스크립트 런타임 환경의 일부입니다. Node.js에서는 web APIs가 C/C+ APIs로 대체됩니다.
이제 위의 코드로 다시 돌아가서 어떻게 비동기 방식으로 실행되는지 알아봅시다.
const networkRequest = () => {
setTimeout(() => {
console.log('Async Code');
}, 2000);
};
console.log('Hello World');
networkRequest();
console.log('The End');
위의 코드가 브라우저에 로드될 때 console.log('Hello World')가 스택에 들어가고 완료된 후 나오게 됩니다. 그 다음, nerworkRequest()를 호출하고 이 함수가 스택의 가장위로 들어갑니다.
그런 다음 setTimeout() 함수가 호출되고, 스택의 맨 위로 올라갑니다. setTimeout()은 두개의 인자를 가집니다: 1) 콜백 그리고 2) 밀리초 시간(ms).
setTimeout() 함수가 웹 APIs 환경에서 2초의 타이머를 시작시킵니다. 이 시점에서 setTimeout() 은 완료되고 스택에서 나오게 됩니다. 그리고나서 console.log('The End') 가 스택에 들어가고 실행되고 완료된 후 스택에서 제거됩니다.
한편, 타이머가 만료되고 콜백이 메세지 큐에 들어갑니다. 하지만 콜백이 즉시 실행되지는 않고 여기서 이벤트 루프가 시작됩니다.
이벤트 루프
이벤트 루프는 콜 스택을 살펴보고 비어있는지 여부를 확인하는 작업을 합니다. 만약 콜 스택이 비어있다면, 메세지 큐를 보고 실행대기 중인 콜백이 있는지 확인합니다.
여기서는 메세지 큐가 한개의 콜백을 가지고 있고, 이 시점에서 콜 스택은 비어있습니다. 그래서 이벤트 루프가 콜백을 스택의 가장위로 넣습니다.
console.log('Async Code') 가 스택의 가장 위로 들어가고 실행된 후 스택에서 빠져나옵니다. 이 시점에서 콜백은 끝나게 되고 스택에서 제거되고 프로그램은 마침내 종료됩니다.
DOM 이벤트
메세지 큐는 클릭 이벤트나 키보드 이벤트와 같은 DOM 이벤트를 위한 콜백도 가지고 있습니다.
예시
document.querySelector('.btn').addEventListener('click',(event) => {
console.log('Button Clicked');
});
DOM 이벤트의 경우 이벤트 리스너가 특정 이벤트(여기서는 클릭 이벤트)가 발생하길 기다리고 있고, 이벤트가 발생하면 콜백 함수가 메세 지 큐에 들어가서 실행되길 기다리게 됩니다.
다시 한번 이벤트 루프가 콜 스택이 비었는지 확인한 후 비어있으면 이벤트 콜백을 스택에 넣습니다. 그리고 나서 콜백이 실행됩니다.
실행 대기중인 콜백들을 저장하기 위해서 메세지 큐를 사용하는 비동기 콜백과 DOM 이벤트가 어떻게 실행되는지 알아보았습니다.
ES6 Job Queue/ Micro-Task queue
ES6는 자바스크립트에서 프로미스가 사용하는 작업 큐/마이크로 태스크 큐의 개념을 도입했습니다. 메세지 큐와 작업 큐의 차이점은 작업 큐는 메세지 큐보다 우선순위가 높다는 점입니다. 이는 작업 큐/마이크로 태스크 큐에 있는 프로미스 작업들은 메세지 큐에 있는 콜백들이 실행되기 이전에 실행이 된다는 것을 의미합니다.
예시
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
resolve('Promise resolved');
}).then(res => console.log(res))
.catch(err => console.log(err));
console.log('Script End');
Output
Script start
Script End
Promise resolved
setTimeout
우리는 프로미스가 setTimeout 이전에 실행되는걸 확인할 수 있습니다. 왜냐하면 프로미스 결과는 메세지 큐 보다 우선순위가 높은 마이크로 태스크 큐 내부에 저장되기 때문입니다.
두개의 프로미스와 두개의 setTimeout이 있는 다른 예시를 봅시다.
예시
console.log('Script start');
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
new Promise((resolve, reject) => {
resolve('Promise 1 resolved');
}).then(res => console.log(res))
.catch(err => console.log(err));
new Promise((resolve, reject) => {
resolve('Promise 2 resolved');
}).then(res => console.log(res))
.catch(err => console.log(err));
console.log('Script End');
Output
Script start
Script End
Promise 1 resolved
Promise 2 resolved
setTimeout 1
setTimeout 2
우리는 두개의 프로미스가 setTimeout의 콜백들 보다 먼저 실행되는걸 확인할 수 있습니다. 왜냐하면 이벤트 루프는 메세지/작업 큐에 있는 작업을 보다 마이크로 태스크 큐에 있는 작업들을 우선시 하기 때문입니다.
이벤트 루프가 마이크로 태스크 큐에 있는 작업을 실행하는 동안, 동시에 다른 프로미스가 리졸브 되면 같은 마이크로 태스크 큐의 끝에 추가하고 얼마나 대기 하였는지에 상관없이 메세지 큐에 있는 콜백이 실행되기 전에 실행됩니다.
예시
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
resolve('Promise 1 resolved');
}).then(res => console.log(res));
new Promise((resolve, reject) => {
resolve('Promise 2 resolved');
}).then(res => {
console.log(res);
return new Promise((resolve, reject) => {
resolve('Promise 3 resolved');
})
}).then(res => console.log(res));
console.log('Script End');
Output
Script start
Script End
Promise 1 resolved
Promise 2 resolved
Promise 3 resolved
setTimeout
따라서 마이크로 태스크 큐에 있는 모든 작업은 메세지 큐가 실행되기 전에 먼저 실행됩니다. 즉, 이벤트 루프는 메세지 큐에 있는 콜백이 실행되기 전에 가장 먼저 마이크로 태스크 큐를 비웁니다.
결론
우리는 비동기 자바스크립트가 어떻게 작동하는지와 자바스크립트 런타임 환경을 구성하는 콜스택, 이벤트 루프, 메세지 큐, 작업큐/마이크로 태스크 큐에 대해 알아보았습니다. 훌륭한 자바스크립트 개발자가 되기 위해서 알아야 할 모든 개념들을 알아야 할 필요는 없지만 이러한 개념들을 알아가는게 많은 도움이 될 것 입니다.
본 게시글은 아래 링크의 글을 번역하였습니다.
원문
https://blog.bitsrc.io/understanding-asynchronous-javascript-the-event-loop-74cd408419ff