JavaScript를 공부하다 보면 한 번쯤은 JavaScript가 단일 쓰레드 기반의 프로그래밍 언어라는 말을 들은 적이 있을 것이다. 그렇다면 setTimeout()
함수로 특정 시간을 기다림과 동시에 다른 코드를 동작시키는 비동기 처리는 어떻게 가능한 것일까? 단일 쓰레드라면 모든 작업이 하나씩 순서대로 이뤄져야 하기에 말이 안되는 상황이다. 이를 이해하기 위해 JavaScript의 런타임에 대한 이해가 필요하다.
런타임이란 해당 프로그래밍 언어로 작성된 코드가 구동되는 환경을 말한다. 웹 브라우저 & Node.js 가 대표적인 JavaScript 런타임이다. 웹 브라우저의 JavaScript 런타임은 크게 두 가지 구성 요소로 이루어져 있는데
JavaScript 엔진
과웹 API
이다. 이 중JavaScript 엔진
이 JavaScript 코드를 읽고 해석해서 실행하는 것을 담당하며, 이는 단일 쓰레드로만 동작한다.
--> 이것이 Js를 단일 쓰레드 기반 프로그래밍 언어라고 하는 이유
JavaScript 코드를 읽고 해석해서 실행하는 것을 담당하는 일종의 인터프리터이다. 즉, 쓰레드 하나로만 동작하기 때문에 스택 영역도 단 하나만 사용하게 된다. 'JavaScript 전용 특급 선로'로 비유할 수 있다. (태스크 큐, 이벤트 루프 개념이 존재)
JavaScript 모델은 크게 두 가지 영역을 정의한다
스택 영역에 푸시되는 함수의 스택 프레임은 곧 그 함수의 실행 맥락(Excution Context)
를 의미한다. 실행 Context란 해당 함수를 실행하기 위해 필요한 각종 정보들(지역 변수...etc) 의 집합이라고 생각하면 된다.
Ajax 요청, setTimeout(), 이벤트 핸들러의 등록과 같이 웹 브라우저에서 제공하는 기능을 말한다. 중요한 것은 이러한 요청들의 처리가 JavaScript 엔진의 쓰레드와는 다른 쓰레드들에서 이뤄진다는 것이다. Js엔진의 스택에서 실행된 비동기 함수가 요청하는 비동기 작업에 대한 정보와 콜백 함수를 웹 API를 통해 브라우저에게 넘기면, 브라우저는 이러한 요청들을 별도의 쓰레드에 위임하게 되는 것이다. 해당 쓰레드는 요청이 완료되는 순간 전달받았던 콜백 함수를 Js 엔진의 태스크 큐라는 곳에 집어 넣는다.
예를 살펴보자,
setTimeout()함수가 실행되면 Js 엔진은 웹 API를 통해 브라우저에게 setTimeout() 작업을 요청하면서 콜백 함수를 전달하고, 브라우저는 이러한 타이머 작업을 별도의 쓰레드에게 위임한다. 이후 Js 엔진의 스택에서는 setTimeout() 함수의 스택 프레임이 즉시 팝된다. 시간이 흐르고 해당 타이머 작업을 처리하고 있던 쓰레드는 전달 받았던 콜백 함수를 Js 엔진의 태스크 큐에 집어 넣게 된다.
태스크 큐는 웹 API를 처리하고 있던 쓰레드로부터 전달받은 콜백 함수들을 FIFO 구조로 저장하고 있는 일종의 큐로 Js 엔진 자체에 포함되어 있는 부분이다. 이 때 태스크란 콜백 함수를 의미하기 때문에 콜백 큐 (Callback Queue)라고 부르기도 한다. 여기에 저장된 콜백 함수들은 스택이 비는 순간 스택에 순서대로 푸시된다. 이러한 원리로 비동기 작업이 완료된 후 콜백 함수가 실행되는 것이다. 비동기 작업이 완료되어 태스크 큐에 콜백 함수가 들어가 있더라도 스택이 비어있지 않으면 해당 콜백 함수가 바로 실행되지 못한다는 특징이 있다. 따라서
setTimeout()
함수도 인자로 명시한 시간은잠드는 최소 시간
일 뿐, 더 오래 잠들 수 있다.
위와 같은 동작이 가능하려면 매 순간 스택이 비어있는지 여부와 태스크 큐에 콜백 함수가 기다리고 있는지 여부를 확인해야 한다. 이러한 역할을 수행하는 것이 바로 이벤트 루프인데, Js엔진 자체에 포함되어 있는 부분으로 매 순간 스택이 비어있는지 확인을 해서 스택이 비어있다면 태스크 큐에 콜백 함수가 들어올 때까지 기다렸다가 첫 번째로 들어오는 콜백 함수를 스택에 쌓는 역할을 한다. 만약 스택이 비어있지 않다면 일반적인 방식으로 스택에서 함수의 호출을 처리하게 된다.
틱(Tick)
이라고 함while (queue.waitForMessage()) {
queue.processNextMessage();
}
--> 동작 방식을 가상의 코드로 설명한다면 위와 같음
자바스크립트 안에서 실행되는 모든 함수의 호출을 기록하고 추적한다. 모든 process와 thread안에는 각각 저마다의 Call Stack이 들어있다. 일을 수행할 때 어디서 왔고, 어디로 다시 가야하는지에 대한 정보를 기억해야 하기 때문이다.
현재 어떤 함수가 동작하고 있는지, 그 함수 내에서 어떤 함수가 동작하는지, 다음에 어떤 함수가 호출되어야 하는지 등을 제어한다.
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
위 코드의 콜 스택 동작 단계를 살펴보자
function rec {
rec();
}
rec();
다음과 같이 계속해서 rec 함수를 호출하는 함수를 만나면 콜 스택에 스택 프레임이 계속 쌓여 스택 overflowing
이 발생한다.
출처 :