on
[자바스크립트] 비동기 작업, 이벤트 루프와 태스크 큐
[자바스크립트] 비동기 작업, 이벤트 루프와 태스크 큐
이전 글
이전 글의 마지막 내용의 출력을 보면,
console.log("Hello"); setTimeout(() => { console.log("James"); }, 4); console.log("World"); let i = 0; while (i < 1e9) i++; console.log("Finish");
다음의 코드 결과는
이와 같았다.
0.004초 뒤에 "James"를 출력하도록 설정했는데, 왜 모든 작업이 끝나야 출력되는지 이제 알아보자.
이 것을 위해서는, 이벤트 루프와 태스크 큐를 알아야 한다.
그 전에 자바스크립트 엔진에 대해서 아주 가볍게 알아보자.
더 상세하게 알아보고 싶다면, 자바스크립트 엔진, V8 엔진 등의 키워드로 검색하면 아주 상세하게 정의된 자료들이 많다.
자바스크립트 엔진
다음은 자바스크립트 엔진의 단순한 모습이다.
그림을 보면 자바스크립트 엔진은 크게 2개의 영역, 콜스택, 힙 으로 구성된다.
콜스택
소스코드 평가 과정에서 생성된 실행 컨텍스트가 추가/제거되는 스택 자료구조이다.
실행 컨텍스트 스택이라고 불리기도 한다. (이 실행 컨텍스트는, 스택 프레임이라고도 불린다.)
함수 호출 시 그 함수의 실행 컨텍스트가 순차적으로 콜스택에 푸시되고, 순차적으로 실행된다.
만약 해당 함수 실행 컨텍스트 내에서 다른 함수가 호출된다면, 추가적으로 다른 함수의 실행 컨텍스트가 콜스택에 푸시되어 실행된다.
스택은 LIFO(Last In First Out)의 특징을 바탕으로, 가장 나중에 호출된 실행 컨텍스트가 가장 먼저 완료된다.
그러므로 가장 나중에 푸시된 실행 컨텍스트부터 종료되면 콜스택에서 제거된다.(= 스택에서 팝된다.)
모든 실행 컨텍스트가 푸시와 팝을 거치며 더이상 실행할 작업이 없다면 종료된다.
힙
객체가 저장되는 메모리 공간이다.
콜스택에 쌓여 있는 실행 컨텍스트는 힙에 저장된 객체를 참조하고 있다.
메모리 개념에서, 힙은 동적 할당을 담당하는 공간인데 여기서도 마찬가지다.
값이 정해진 원시값(int, char, boolean...) 등과는 달리 객체는 크기가 정해져있지 않다.
그러므로 할당 크기를 런타임 시 결정해야 하므로, 그 때 힙에 저장해서 참조하는 방식으로 사용한다.
자세히 알아보지는 않았지만 이와 같이 자바스크립트 엔진은 단순한 작업을 수행한다.
작업을 실행 컨텍스트 단위로 콜스택을 이용해 순차적으로 실행할 뿐이다.
그렇기 때문에, 비동기 작업을 도울 수 있는 다른 장치가 필요한데, 이것이 이벤트 루프와 태스크 큐이다.
이 이벤트 루프와 태스크 큐는 자바스크립트 엔진이 아니라 런타임에 해당하는 브라우저/NodeJS에 내장되어 있다.
이전 글에서 언급했던 어딘가로 비동기 작업을 던져둔다는 어딘가가 이곳이다.
이벤트 루프와 태스크 큐
자바스크립트는 싱글 스레드로 동작한다.
싱글 스레드는 한 번에 하나의 작업만 처리할 수 있도록 동작한다는 의미이다.
하지만 네이버를 들어가보면 여러 작업들이 동시다발적으로 일어나는 듯 보인다.
이 동시성을 가능하도록 하는 것이 이벤트 루프 이다.
이벤트 루프
이 이벤트 루프는 콜스택에 현재 실행중인 실행컨텍스트가 있는지, 그리고 태스크큐에 대기중인 콜백함수가 있는지를 확인한다.
콜스택이 비어있고 태스크 큐에 대기중인 콜백이 있다면?
이벤트 루프는 FIFO로 태스크 큐에 대기중인 콜백을 콜스택으로 이동시킨다.
콜스택으로 이동한 함수는 위에서 처리한 방식과 동일하게, 실행 컨텍스트를 만들며 순차적으로 처리된다.
태스크 큐
비동기 함수의 콜백 함수/이벤트핸들러가 일시적으로 보관되는 곳
비동기 함수에는 setTimeout, setInterval과 같은 타이머 함수, AJAX 수행 함수 등이 있다.
태스크 큐보다 우선순위가 높은(먼저 고려되는), 마이크로 태스크 큐 라는 것도 존재하지만 지금은 넘어가도록 하자.
이벤트 루프가 태스크 큐에 순차적으로 쌓인 콜백들을 순차적으로 빼서 작업을 수행한다.
이제 이 개념들을 바탕으로 글의 처음에서 봤던 코드를 이해해보도록 하자.
코드 이해하기
console.log("Hello"); setTimeout(() => { console.log("James"); }, 4); console.log("World"); let i = 0; while (i < 1e9) i++; console.log("Finish");
다음과 같은 출력이 나오는 이유를 위에서 익힌 개념들을 가지고 알아보자.
전역 실행 컨텍스트가 생성되고 콜스택에 푸시된다. 전역 실행 컨텍스트의 첫 문단인 console.log("Hello"); 함수가 호출된다. 이 함수의 실행 컨텍스트가 콜스택에 푸시되며, Hello를 출력한 뒤 바로 pop된다. 다음 코드인 setTimeout 함수가 호출된다. setTimeout 함수의 실행 컨텍스트가 콜스택에 푸시되어 타이머 호출을 스케쥴링하고 바로 pop된다.
타이머가 만료되었을 때 태스크 큐에 콜백함수(console.log("James"))를 푸시하는 것은 브라우저가 수행한다.
타이머가 만료되어 태스크 큐에 콜백함수가 푸시되었다고 해도, 아직 콜스택이 비어있지 않기 때문에 콜백이 실행되지 않는다. 다음 코드인 console.log("World"); 가 수행된다. 함수의 실행 컨텍스트가 콜스택에 푸시되며, World를 출력한 뒤 바로 pop된다. let i~ while~ console.log(Finish)까지 수행한다. 계속 이벤트 루프는 콜스택과 태스크 큐를 살펴보지만, 전역 컨텍스트가 콜스택에 계속 남아있는 것을 알고 있다.
다음 작업까지 모두 끝나야 전역 실행 컨텍스트가 콜스택에서 pop된다. 콜스택에 비어있고, 태스크 큐에 콜백이 존재하므로 이벤트루프가 역할을 수행한다. 타이머 만료 후 브라우저가 태스크 큐에 넣어둔 콜백을 이벤트 루프가 콜스택에 넣어준다.
콜백 또한 실행 컨텍스트가 만들어져 콜스택에 푸시되고 수행해야 할 명령을 수행한 후에 콜스택에서 팝된다.
다음의 1~6 과정을 통해 "James"가 가장 마지막에 출력되는 것이다.
이 절차를 이해하면,
자바스크립트에서 setTimeout/setInterval 등과 같은 타이머가 정확하지 않은 이유도 바로 알 수 있다.
또한
let name; setTimeout(() => { name = "James"; }, 1000); console.log(name);
다음의 코드가 왜 undefined를 출력하는지도 이해할 수 있겠다.
간단하게 자바스크립트 엔진, 이벤트 루프, 콜스택을 알아보고 비동기 함수가 이상한 순서를 가지는 이유도 알아봤다.
자바스크립트 엔진은 싱글 스레드로 동작하지만, 브라우저는 멀티 스레드로 동작함을 기억하자.
참고한 사이트
https://wikibook.co.kr/mjs/
https://it-eldorado.tistory.com/86
https://kyounghwan01.github.io/blog/JS/JSbasic/eventLoop/#ecmascript에는-이벤트-루프가-없다
from http://gobae.tistory.com/101 by ccl(A) rewrite - 2021-08-22 01:00:09