Before Start
어플리케이션을 실행시키면, 메모리에는 이 어플리케이션을 위한 저장 공간이 할당된다. 그리고 현재 실행 중인 어플리케이션, 즉 메모리를 할당받은 어플리케이션을 프로세스(process)라고 한다. 이 저장 공간은 4개로 나누어 지는데, 다음과 같다.
- code | 우리가 작성한 source code가 저장됨
- data | 전역 변수(global var)에 저장된 데이터와 primitive type의 data, 즉 정적 타입(static type)의 데이터가 저장됨
- stack | 지역 변수(local var)에 저장된 데이터가 저장됨
- heap | 객체 타입(object type), 즉 동적 타입(dynamict type)의 데이터가 저장됨
+) 지역 변수는 코드 블럭 내부에서 선언된 변수를 말하고, 블럭 밖에 선언된 변수를 전역 변수라고 하는데 자세한 내용은 scope 챕터에서 다루어 보도록 한다.
Call Stack
+) JS Call stack Visualization
+) chrome dev tool > sources > breakpoint > ⬇️(step into next function)
function a() {
return 1;
}
function b() {
return a() + 1;
}
function c() {
return b() + 1;
}
c();
call stack에는 코드에서 함수를 호출하는 순서 정보가 저장되고, Javascript 엔진은 딱 하나의 call stack을 가지고 있다.이것은 JS가 한번에 한 줄씩만 코드를 실행할 수 있음을 의미하고, 이것이 Javascript를 single thread 언어라고 부르는 이유이다.
위의 예제 코드에서 우리는 함수 c를 호출했으므로 call stack에 c가 저장된다. c를 실행하려고 보니 값을 반환하려면 b를 호출해야 한다. 때문에 c는 b를 호출하고, 같은 이유로 b는 다시 a를 호출한다. 이 때 call stack에는 c - b - a가 순서대로 저장되어 있다.
a를 실행하면, a는 1을 반환하며 종료된다. 할 일을 다 했기 때문에 a는 call stack에서 지워진다. 이제 call stack에는 c - b만 남아있다. b는 a가 반환한 1에 1을 더한 값을 반환하면 할 일을 끝내고 종료된다. c도 마찬가지의 과정을 거치며 최종적으로 3을 반환하고 call stack에는 아무 것도 남아있지 않게 된다.
call stack에 c - b - a 순서로 저장되었던 정보들이 모두 exit하는 것을 살펴보았는데, 이 과정에서 가장 마지막에 들어왔던 정보(a)가 가장 먼저 나가는 것을 알 수 있다. 이를 LIFO(Last In First Out)라고 하며, stack은 LIFO로 데이터를 다루는 Data structure를 일컫는 용어이다.
앞서 이야기 했듯, 자바스크립트 엔진은 이런 call stack을 딱 하나만 가지고 있는데, 이것은 곧 자바스크립트 엔진이 한 번에 하나의 작업만 수행할 수 있다(synchronous)는 것을 의미한다. 그리고 이러한 특징때문에 JS를 single thread 언어라고 부른다. 예를 들어 for문을 이용하여 무언가를 100000번 반복하는, 시간이 아주 오래 걸리는 작업이 있다고 해보자. 이 작업 뒤에는 console.log같은 아주 간단한 작업이 기다리고 있다. 그럼에도 100000번의 반복 작업이 끝나지 않으면, 그 다음 작업을 수행할 수 없다는 뜻이다.
Task Que
JS는 single thread 언어이고, 한 번에 한 줄씩의 코드만을 실행할 수 있다고 했다. 이 때, 어떤 코드가 네트워크와 통신을 하면서 데이터를 받아온다고 해보자. 이 코드는 사용자의 네트워크 환경이나, 서버의 상태 등등에 따라서 시간이 오래 걸릴 수 있는 작업이다. 그럼 JS는 이 모든 작업이 끝날 때 까지 기다린 후에야 다음 코드를 실행할 수 있는 걸까?
다행히도 그렇지 않다. 물론 JS 언어 자체는 single thread 언어이기 때문에, 한 번에 한 줄의 코드만 실행할 수 있다. 그래서 네트워크에서 데이터를 요청하고 받아오는 동시에, 사용자가 버튼을 클릭했을 때 보여줄 동작을 실행할 수 없다. 그러나 이것은 매우 비효율적이기 때문에, JS 엔진의 "Event Loop"는 자신이 처리할 수 없는 일을 브라우저 또는 노드, 즉 실행 환경에 건네어 줌으로써(hand off) 이런 비효율을 막는다. 이 때 JS가 처리 할 수 없는 일이란, 브라우저가 제공하는 Web API 또는 노드가 제공하는 Node API를 실행하는 일을 말한다.
시간이 오래 걸리는 대표적인 web API인 setTimeout의 경우를 살펴보자.
setTimeout(() => console.log('2초가 지난 뒤 출력됩니다'), 2000);
console.log("바로 출력됩니다");
Call Stack | Task Que |
console.log("바로 출력됩니다") | |
setTimeout() => Web API⭕ => 바로 나간다 | () => console.log('2초가 지난 뒤 출력됩니다') |
- JS 엔진은 함수를 실행하는 코드를 만나면, 그 함수를 call stack에 집어넣는다.
- call stack은 함수의 실행 순서를 저장해 두는 공간이다.
- 이 때 call stack에서는, setTimeout과 같은 web API를 식별한다.
- web API는 실행이 되었는지 여부에 관계없이 call stack에서 바로 빠져나와 다른 장소에 보관된다.
- web API의 실행이 실제로 끝나면 (ex. 브라우저의 타이머에서 2초가 지남), web APIs에서 전달 받은 callback 함수를 Task que라는 장소에 집어 넣는다.
- 이 때 JS 엔진에 내장되어 있는 Event loop는 call stack과 task que를 관찰하면서 task que에 저장된 함수를 call stack으로 옮기는 역할을 한다. 단, call stack이 텅 빈 상태일 때에만 call back함수를 옮길 수 있다.
즉, timer의 2초가 지나면, web API가(ex. setTimeout) console에 "2초가 지났습니다"를 출력하는 함수를 task que에 전달하고, event loop는 call stack이 텅 빈 상태가 되었을 때 task que의 함수를 call stack으로 가져온다. 만약 2초가 지났는데 call stack이 비어있지 않다면 어떻게 될까?
function long() {
for (let i = 0; i < 10000000000; i++) {}
console.log('long done');
}
setTimeout(() => console.log('test'), 2000);
long();
2초가 지난 뒤에 무사히 test가 출력될 수 있을까? 2초가 지난 시점에서도 call stack에 long 함수가 남아있기 때문에, test 문자열이 출력되지 않았다.
Call Stack | Task Que |
long | |
setTimeout => 들어오자 마자 나간다 | 2초 후 => console.log('test')가 들어온다 |
long은 종료될 때 까지 call stack에서 사라지지 않는다. 나의 경우에는 long이 종료되는 데 2초가 넘는 시간이 걸렸기 때문에, 2초가 지난 뒤에 test를 출력하는 함수가 task que에 들어와도 call stack이 텅 빈 상태가 아니기 때문에 이곳으로 들어올 수 없다. 즉 콜백 함수를 실행할 수 없는 것이다.
+) Que는 FIFO(First In First Out) 형식의 data structure이다. 즉 task que에 여러 콜백 함수가 쌓여있을 때, 먼저 들어온 함수가 먼저 call stack으로 나간다.
'Study > JavaScript' 카테고리의 다른 글
Scope, Execution context, Closure (0) | 2022.04.04 |
---|---|
Asynchronous | callBack, promise, async & await (0) | 2022.04.02 |
Modularization (0) | 2022.03.31 |
Iterable (0) | 2022.03.30 |
Map & Set (0) | 2022.03.30 |