비동기 자바스크립트 이해하기
https://blog.bitsrc.io/understanding-asynchronous-javascript-the-event-loop-74cd408419ff
자바스크립트는 한번에 한가지의 일만 발생할 수 있는 단일스레드 프로그래밍 언어이다. 즉, 자바스크립트 엔진은 단일스레드로 한 번에 하나의 명령문만 처리할 수 있다.
단일스레드 언어는 동시성 문제를 걱정할 필요가 없기 때문에 코드 작성이 단순하지만, 이것은 메인스레드를 차단하지 않고는 네트워크 엑세스와 같은 오래 걸리는 작업을 수행할 수 없다는걸 의미하기도 한다.
API를 이용하여 서버에 데이터를 요청한다고 상상해 보자. 상황에 따라 조금씩 차이가 있겠지만 서버가 요청을 처리하는 도중에 메인스레드를 차단하므로 웹페이지가 응답하지 않게된다.
이곳이 비동기 자바스크립트가 동작해야 하는 부분이다. callback, promise, async/await와 같은 기능을 사용하면 메인스레드를 차단하지 않고 오래 걸리는 네트워크 요청을 수행할 수 있다.
이 모든 개념을 다 알 필요는 없지만, 더욱 멋진 자바스크립트가 되기 위해서는 모든걸 배워야한다. 알아두면 유용하다. :)
팁: Bit를 사용하면 어떤 JS 코드든 공유할 수 있는 API로 바꾸어, 프로젝트와 앱 간에 더욱 빠르게 빌드하여 동기화할 수 있고 더 많은 코드를 재사용 할 수도 있다.
동기 자바스크립트는 어떻게 동작할까?
비동기 자바스크립트에 대해 알아보기 전에, 우선 동기 자바스크립트 코드가 자바스크립트 엔진에서 어떻게 실행되는지 아래 코드를 통해 알아보자.
const second = () => {
console.log('Hello there!');
}
const first = () => {
console.log('Hi there!');
second();
console.log('The End');
}
first();
위의 코드가 자바스크립트 엔진에서 어떻게 실행되는지 이해하려면, 실행 컨텍스트와 콜스택(실행스택 이라고도 함) 의 개념을 먼저 알아야 한다.
실행 컨텍스트
실행 컨텍스트는 자바스크립트 코드가 평가되고 실행되는 환경의 추상적 개념이다. 자바스크립트에서 코드를 실행할 때마다 실행컨텍스트 내에서 동작한다.
함수 코드는 함수 실행 컨텍스트 내에서 실행되며, 전역 코드는 전역 실행 컨텍스트 내에서 실행된다. 함수들은 각각 자신들만의 실행 컨텍스트를 가지고 있다.
콜 스택
이름에서 알 수 있듯이 코드 실행 중에 작성된 모든 실행 컨텍스트를 저장하는 LIFO(후 입력, 선출력) 구조의 스택을 말한다.
자바스크립트는 단일스레드 프로그래밍 언어이기 때문에 단일 콜 스택을 가지고 있다. 콜 스택은 LIFO 구조이므로 아이템을 스택의 맨 위에서만 추가하거나 제거할 수 있다.
다시 예제로 돌아가서 자바스크립트 엔진에서 코드가 어떻게 실행되는지 알아보자.
const second = () => {
console.log('Hello there!');
}
const first = () => {
console.log('Hi there!');
second();
console.log('The End');
}
first();

그래서 여기에 무슨일이 벌어지고 있는걸까?
코드가 실행되면 전역 실행 컨텍스트(main())가 생성되고 콜 스택의 맨 위에 놓여진다. 그리고 그 후 first() 라는 호출이 들어오면 main() 스택의 위로 쌓이게 된다.
다음으로 console.log('Hi there!')가 스택의 맨위로 다시 쌓이고, 실행이 끝나면 스택에서 꺼내어진다. 그 후에 second()를 호출하므로 second()함수가 스택의 맨 위에 놓인다.
console.log('Hello there!')가 스택의 맨 위로 올라가고 완료되면 스택에서 분리된다. 그리고 second()함수도 끝나므로 스택에서 꺼내어진다.
console.log('The End')가 스택의 맨 위로 올라가고 완료되면 제거된다. 그 후에는 first() 함수가 완료되므로 스택에서 제거된다.
프로그램은 이 시점에서 실행을 완료하므로 전역 실행 컨텍스트main()가 스택에서 제거된다.
비동기 자바스크립트는 어떻게 동작할까?
이제 콜 스택과 동기 자바스크립트가 기본적으로 어떻게 동작하는지 알았으니, 비동기 자바스크립트로 돌아가 보자.
메인스레드 차단이란?
아래의 예 처럼 이미지 처리나 네트워크 요청을 동기식으로 하고 있다고 가정하자.
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() 함수가 호출된다. greeting()가 갖고있는 console.log는 일반적으로 빠르게 실행되므로 greeting() 함수는 즉각 완료되어 반환된다.
결과적으로 우리가 greeting()을 실행하려면 이 전에 실행된 함수의(processImage(), networkRequest() 같은) 실행이 완료될 때까지 기다려야한다. 이것은 실행이 오래 걸리는 함수가 콜 스택이나 메인스레드를 차단하고 있음을 의미한다. 위의 예제 코드와 같이 이상적이지 않은 코드가 실행되는 동안에는 브라우저가 다른 작업을 수행할 수 없다.
그렇다면 해결책은 없을까?
가장 간단한 해결책은 비동기 콜백이다. 아래 예 처럼, 코드로 인한 차단을 막기위해 비동기 콜백을 사용한다.
const networkRequest = () => {
setTimeout(() => {
console.log('Async Code');
}, 2000);
};
console.log('Hello World');
networkRequest();
이 예제에서는 setTimeout 메서드를 사용하여 네트워크 요청을 시뮬레이션했다. setTimeout은 자바스크립트 엔진의 일부가 아니라 브라우저의 웹 API 또는 node.js의 C/C API 의 일부라는 것에 유념해야한다.
이 코드가 어떻게 실행되는지 이해하려면 이벤트 루프 및 콜백 큐(작업 큐 또는 메시지 큐 라고도 함)와 같은 몇 가지 개념을 더 이해해야 한다.

자바스크립트 런타임 환경 개요
이벤트 루프, 웹 API 및 메시지 큐/작업 큐는 자바스크립트 엔진의 일부가 아니며 브라우저의 자바스크립트 런타임 환경 또는 Nodejs 자바스크립트 런타임 환경의 일부다. (Nodejs에서 웹 API는 C/C API로 대체된다.)
이제 다시 코드로 돌아가서 비동기 자바스크립트가 어떤 식으로 실행되는지 살펴보자
const networkRequest = () => {
setTimeout(() => {
console.log('Async Code');
}, 2000);
};
console.log('Hello World');
networkRequest();
console.log('The End');

이벤트 루프
코드가 브라우저에 로딩되면, console.log(‘Hello World’)가 스택에 올라가고 실행이 완료되면 스택에서 제거된다. 그 다음 networkRequest()함수의 호출이 발생하여 스택의 맨 위에 놓여진다.
그런다음 setTimeout() 함수가 실행되어 콜스택의 가장 위에 놓인다. setTimeout()은 1) callback 그리고 2) time(ms) 의 두 인수를 가지고 있다.
setTimeout() 메서드는 웹 API 환경에서 2초의 타이머를 시작한다. setTimeout()은 바로 끝나면서 스택에서 떨어져 나간다. 그 후에 console.log('The End')를 스택에 밀어넣고, 완료 후 스택에서 제거한다.
한편에서는 setTimeout()의 2초 타이머가 끝났으므로, callback이 메세지 큐에 추가된다. 그러나 콜백은 즉시 실행되지 않으며 이벤트 루프의 신호를 기다린다.
이벤트 루프
이벤트 루프의 역할은 콜 스택이 비어 있는지 여부를 감시하는 것이다. 콜 스택이 비어 있으면 메세지 큐에 대기중인 콜백이 있는지 확인한다.
우리의 예제의 경우, 메시지 큐에는 콜백 하나가 포함되어 있으며 콜 스택이 비어 있으므로 이벤트 루프는 콜백을 콜 스택의 맨 위로 밀어 넣는다.
그 후에 console.log('Async Code')가 스택의 맨 위로 올라가 실행되고 스택에서 분리된다. 이 시점에서 콜백이 완료되어 스택에서 제거되고 프로그램이 마침내 완료된다.
DOM 이벤트
메시지 큐에는 클릭 이벤트 및 키보드 이벤트와 같은 DOM 이벤트의 콜백도 포함되어 있다. 아래의 예를 보자.
document.querySelector('.btn').addEventListener('click',(event) => {
console.log('Button Clicked');
});
DOM 이벤트의 경우, 웹 API 환경에 위치한 이벤트 수신자가 특정 이벤트(우리 예제의 경우 클릭 이벤트)가 발생하기를 기다린다. 이벤트가 발생할 경우 콜백 함수는 메세지 큐에 추가된다.
이벤트 루프는 콜 스택이 비어 있는지 계속해서 확인하고 메시지 큐에 대기하고 있는 콜백이 있으면 콜 스택으로 푸시한다.
우리는 실행 대기중인 모든 콜백을 저장하기 위해 메시지 큐를 사용하는 비동기 콜백 그리고 DOM 이벤트가 실행되는 방법을 배웠다.
ES6 잡 큐/ 마이크로 작업 큐
ES6는 자바스크립트에서 Promises에서 사용하는 잡 큐/마이크로 작업 큐의 개념을 도입했다. 메시지 큐와 작업 큐의 차이는 작업 큐가 메세지 큐 보다 우선 순위가 높다는 것이다. 이것은 메시지 큐 내의 콜백 전에 잡 큐/마이크로 작업 큐 내의 Promise 작업이 실행된다는 것을 의미한다.
예제:
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');
결과:
Script start
Script End
Promise resolved
setTimeout
Promise의 응답은 메시지 큐보다 우선순위가 높은 마이크로 작업 큐 내에 저장되므로 setTimeout 전에 약속이 실행됨을 알 수 있다.
또 다른 예로, 이벤에는 두 번의 Promise와 두 번의 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');
결과:
Script start
Script End
Promise 1 resolved
Promise 2 resolved
setTimeout 1
setTimeout 2
이벤트 루프가 메시지 큐/작업 큐 보다 마이크로 작업 큐를 우선시하기 때문에 setTimeout의 콜백 전에 두 Promise가 먼저 실행된다는 것을 알 수 있다.
이벤트 루프가 마이크로 작업 큐의 작업을 실행하는 동안 다른 Promise가 resolved되면 같은 마이크로 작업 큐의 끝에 추가되며, 콜백이 실행되기를 기다리는 시간이 얼마인지에 관계없이 메시지 큐의 콜백 작업 전에 실행될 것이다.
아래의 예를 보자
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');
결과:
Script start
Script End
Promise 1 resolved
Promise 2 resolved
Promise 3 resolved
setTimeout
따라서 마이크로 작업 큐의 모든 작업은 메시지 큐의 작업보다 먼저 실행된다. 즉, 이벤트 루프는 메시지 큐에서 콜백을 실행하기 전에 먼저 마이크로 큐를 비운다.
결론
그래서 우리는 비동기 자바스크립트가 어떻게 작동하는지, 그리고 자바스크립트 런타임 환경을 만드는 콜 스택, 이벤트 루프, 메시지 큐/태스크 큐, 잡 큐/마이크로 태스크 큐와 같은 다른 개념들을 배웠다. 이 모든 개념을 배울 필요는 없지만, 더욱 멋진 자바스크립트 개발자가 되기 위해 이러한 개념을 배워두면 분명 도움이 될 것이다 :)
이 글이 도움이 되었다면 박수 버튼을 클릭하고, 미디엄과 트위터를 통해 나를 팔로우 해주길 바란다. 궁금한 점이 있다면 언제든지 코멘트를 통해 말해달라! 기꺼이 돕겠다 :)