출처: [우아테크-10분 테코톡] 피터의 이벤트 루프 https://www.youtube.com/watch?v=wcxWlyps4Vg&list=RDCMUC-mOekGSesms0agFntnQang&index=1
이벤트 루프에 대해 얘기하기 전에 사전에 짚고 넘어가야 할 몇 가지 개념들이 있다.
다음의 개념들에 대해 먼저 이해하고 이후에 이벤트루프에 대해 알아보자.
특정 함수의 인자로 들어가는 함수이다.
비동기 콜백이냐 동기 콜백이냐에 따라 콜백 함수의 실행 시점이 달라진다.
호출 즉시 실행
동기 콜백 실행 이후 조건을 만족하면 실행
특정 이벤트 발생 시
일정 시간 경과 후
async/await로 axios 요청 시
코드 순서 예상하기
console.log('배고프다');
setTimeout(function() {
console.log('저기 어때?');
}, 2000);
console.log('저기 가 봤어?');
위에서부터 순서대로 실행
// 배고프다
// (2초 후)
// 저기 어때?
// 저기 가 봤어?
코드를 순서대로 실행하다가 setTimeout과 같은 비동기 함수를 만나면
콜백 함수를 자바스크립트의 뒤편(backstage)으로 보내고 타이머를 작동시킨다.
나머지 동기 코드들이 실행된 후에
타이머의 시간이 지나면 대기하고 있던 콜백 함수를 실행한다.
// 배고프다
// 저기 가 봤어?
// (2초 후)
// 저기 어때?
자바스크립트 엔진은 자바스크립트 코드를 해석하고 실행하는 ‘번역기’이다.
각 브라우저마다 엔진의 종류가 다르며, (예: 사파리는 Webkit, 크롬은 V8 등)
크게 힙(Heap)과 호출스택(Call Stack)으로 구성된다.
변수나 객체 등이 저장되는 일종의 '창고'같은 개념이다.
이번 포스트에서 크게 중요한 개념은 아니다.
함수를 실행하고 제거하는 스택이다.
자바스크립트 코드에서 함수의 실행이 시작되면 호출 스택에 해당 함수를 집어넣는다.
함수의 실행이 끝나면 호출 스택의 맨 위에 있는 실행된 함수를 꺼내서 제거한다.
즉, 흔히 '호출 스택이 비어 있다'는 말은 '실행할 함수가 남아있지 않다'와 동일하다.
아래 예시를 보기 전 컵 모양의 호출 스택을 먼저 상상해보자.
코드의 실행이 시작되면 호출 스택 안에 함수를 블록처럼 쌓은 뒤
함수의 실행이 끝나면 호출 스택에서 하나씩 차례로 제거한다.
처음에 호출 스택 가장 밑바닥에는 전역 변수가 존재하는 Global context(전역 문맥)가 먼저 쌓여 있다. 전역 문맥은 호출 스택의 밑바닥에 항상 존재하며 호출 스택의 작업이 끝난 후 가장 나중에 사라진다.
function second() {
setTimeout(function() {
console.log('세상에..');
}, 2000);
}
function first() {
console.log('디저트를 안 먹는다고?');
second();
console.log('어떻게 디저트를 안 먹을 수 있지?');
}
first();
(전역 문맥은 제외)
콘솔결과
// 디저트를 안 먹는다고?
// 어떻게 디저트를 안 먹을 수 있지?
// (2초 뒤) 세상에..
자바스크립트는 ‘싱글 스레드 언어’이다. 즉, 호출 스택을 1개만 사용한다는 뜻이다.
이는 자바스크립트가 기본적으로 한 번에 한 가지 일만 처리할 수 있다는 것을 의미한다. (= 동시에 여러 작업을 진행할 수 없다)
하지만 브라우저는 자바스크립트 엔진 외에도 Web API, 이벤트 루프, 콜백 큐 등을 사용하여 여러 작업을 동시에 진행하는 멀티 스레드로 동작하는데, 이는 자바스크립트의 '비동기'적 특성에 기인한다.
Web API는 DOM 조작(addEventListener), AJAX(Fetch API), setTimeout과 같은 비동기 메소드들을 자바스크립트에 제공한다. 이 비동기 메서드들의 역할은 실행 후 인자로 받은 콜백 함수(실제 실행문)를 Web API로 보내는 것이다. (이걸로 비동기 메서드 자체의 역할은 끝나고 호출 스택에서 사라진다. 이후 실제 콜백 함수를 실행하는 것은 이벤트 루프의 역할이다)
이벤트 루프는 호출 스택과 콜백 큐를 계속 주시하고 있다가
다음의 2가지 조건이 만족되면 콜백 큐에 대기하고 있던 콜백 함수들을 순서대로 호출 스택에 넣는다.
콜백 큐에는 매크로 태스크 큐와 마이크로 태스크 큐가 있다.
매크로 태스크 큐에는 동기적으로 실행되는 코드들이,
마이크로 태스크 큐에는 비동기적으로 실행되는 코드들이 들어 있다.
이벤트 루프는 먼저 매크로 태스크 큐에 있는 동기적 코드들을 호출 스택에 넣어 모두 실행한 다음,
호출 스택이 비워지면 이후에 마이크로 태스크 큐에 있는 코드들을 실행한다.
다음의 과정을 반복한다.
다음 코드를 보고 결과를 예상해보자.
console.log('script start'); // A
setTimeout(function () { // B
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(function () { // C
console.log('promise1');
})
.then(function () { // D
console.log('promise2');
});
console.log('script end'); // E
script start
script end
promise1
promise2
setTimeout
비동기 함수를 실행하다 보면 try-catch가 에러를 제대로 잡지 못하는 경우가 생길 수 있다.
예를 들어,
btn.addEventListener('click', function() { // (A)
try {
axios.get(url, function(res) { // (B)
// 여기서 에러 발생
});
} catch (e) {
console.log(e.message);
}
});
위의 코드에서 try-catch문은 (B)에서 발생하는 에러를 잡아내지 못한다.
(B) 콜백 함수에도 똑같이 try-catch문을 넣어주면 된다.
(=> (B) 함수 자체 내에서 에러를 처리할 수 있도록)
대기 시간이 0초인 setTimeout 함수는 0초 후에 실행(= 즉시 실행)이라는 의미가 아니다.
console.log('피카츄!');
setTimeout(function() {
console.log('삐까삐까!');
}, 0);
console.log('백만볼트!');
대기 시간이 0초라 해도 비동기 함수들은 콜백 큐에서 대기하고 있다가 동기적 코드가 모두 실행되고 난 뒤에 실행된다.
따라서 위의 코드에서 '삐까삐까!'는 코드 작성 순서로는 2번째이지만 실제 출력 순서는 가장 마지막이 된다.