JavaScript의 동작 원리
왜 동작 원리를 공부를 하는가?
기술 면접을 준비를 하면서 그냥 답만 찾는 것 보다는 내가 이해를 바탕으로 답변을 준비를 하는것이 나중에 꼬리 질문이 들어왔을 때도 대처가 가능 할 수 있다고 생각이 들어서 시작하였다.
클로저, 스코프, 호이스팅 등 개념을 알려면 먼저 JavaScript를 알아야 한다고 생각이 들었다. 그래서 동작 원리부터 차근차근 준비를 해보려 한다.
자바스크립트 동작 구조
먼저 자바스크립트를 실행하기 위해서는 자바스크립트 엔진이 필요하다. 자바스크립트 엔진은 여러가지가 있지만 대표적인 예로는 Google에서 만든 V8 엔진 이다.
엔진은 사진에서와 같이 Memory Heap과 Call Stack 두 가지로 이루어져 있다. Memory Heap에서는 메모리 할당이 이루어지고, Call Stack에서는 코드 실행에 따라 스택이 하나씩 쌓이는 곳이다.
자바스크립트는 여러 API를 사용을 한다. 이런 API를 지원 해주는 곳은 Web API라는 곳에서 지원을 해준다.
자바스크립트는 setTimeOut()과 같은 비동기 코드 작성이 가능함에도 불구하고, 자바스크립트 자체에는 비동기 코드를 처리하기 위한 개념을 갖고 있지 않다. 그렇다면 어떻게 비동기 코드로 처리할 수 있을까? 바로 Event loop & Callback Queue의 콜라보레이션으로 가능 하게 된다.
결국 자바스크립트 엔진은 Memory Heap과 Call Stack로 구성이 되어 지고, Web API로 인해 많은 API를 사용 할 수 있다. 또한 런타임 환경에서 Event loop & Callback Queue을 지원을 하게 된다.
자바스크립트는 싱글 or 멀티 스레드?
위의 자바스크립트 엔진의 구성을 살펴 봤을 때, 하나의 Memory Heap과 하나의 Call Stack으로 구성이 되어진걸 봤었다. Call Stack은 기본적으로 자바스크립트를 한 줄씩 읽어가며 우리의 코드가 순서대로 돌 수 있도록 보장해주는 구조 이다. 스택을 사용하기 때문에 후입 선출의 구조를 가지고 있다.
함수 호출이 되면 Call Stack에 스택 프레임이 하니씩 쌓이게 된다. 이때 함수 실행이 되면 스택 프레임이 사라지게 된다.
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
맨 처음 엔진이 자바스크립트를 실행 할 때에는 아무 함수를 만나지 않은 상태라서 비어 있겠지만, 코드를 실행하며 가장 먼저 printSquare를 호출하기 때문에 printSquare를 Call Stack에 Push를 하여 스택을 쌓고 읽어간다. 그러다 multiply를 Push를 한다. 더이상 쌓을 함수가 존재하지 않으면 위에 쌓인 스택 프레임부터 하나씩 처리를 하며 출력 하게 된다.
이처럼 자바스크립트는 하나의 Call Stack을 가지고 코드를 순차적으로 처리하기 때문에 한 번에 하나의 명령어만 실행될 수 밖에 없다. 그렇기 때문에 단일 스레드이며 동기식 언어라고 할 수 있다. 자바스크립트는 이러한 특성 때문에 자바스크립트는 무한 루프가 발생할 수 있어도 동기화 문제인 교착상태(DeadLock)는 발생할 수 없다.
자바스크립트는 어떻게 비동기 작업을 수행하는가?
자바스크립트는 어느 웹이든 많이 사용하고 있다. 하지만 이러한 웹 서비스들이 동기식 + 단일 스레드로만 동작을 한다면 하나의 작업을 처리할 때 많은 시간이 소요가 되면서 사용자가 이용하는데 어려움을 겪을 것이다.
이때 필요한 것이 Event loop & Callback Queue이다. 다음 코드의 출력 순서를 알아보자.
function foo() {
console.log("1");
}
function foo2() {
console.log("2");
}
foo();
setTimeout(function () {
console.log("3");
}, 2000);
foo2();
이 코드는 1,2,3 순으로 출력이 될 것이다. 싱글 스레드로 작업이 되는데 어떻게 1,3,2가 아닌 1,2,3이 출력이 될까? 그 이유는 Event loop와 Callback Queue 덕분이다. 동작 원리를 알아보자.
- 제일 먼저 foo()라는 함수가 Call Stack에 쌓이게 된다.
- 그 다음 foo() 함수 안에 있는 console.log()가 Call Stack에 쌓인다.
- 콘솔 창에 1을 출력한다.
- foo() 함수는 종료 되었으니 Call Stack에서 빠지게 되고, setTimeOut()이 Call Stack으로 들어온다.
- setTimeOut()을 Web API에서 처리하도록 보낸다. 만약 node.js나 deno.js 경우 백그라운드에서 처리하도록 보낸다. 그리고 그 다음 함수인 foo2() 를 Call Stack에 들어온다.
- foo2() 함수 안에 있는 console.log()가 Call Stack에 쌓인다.
- 콘솔 창에 2를 출력한다.
- foo2() 함수는 종료 되었으니 Call Stack에서 빠지게 된다. 이제 실행 될 함수는 없지만 Web API에서 setTimeOut() 함수를 처리하고 있다.2초간 Web API에서 처리를 하고 2초 후 setTimeOut()의 콜백함수를 Callback Queue로 보내게 된다.
- 이제 Event Loop가 나오게 되는데, Event Loop는 Callback Queue에 있는 콜백 함수를 Call Stack으로 보내서 처리하기 위해 Call Stack이 비어있는지를 검사한다. 만약 Call Stack이 비어 있다면 Callback Queue에 있던 함수를 Call Stack으로 보내서 처리하게 된다.
- Call Stack에 있던 console.log()를 콘솔에 출력하는 것으로 프로그램이 종료 된다.
왜 Call Stack이 비어있어야 하는가?
왜 Event Loop는 Call Stack이 비어져 있는 것을 확인 하고 Callback Queue의 함수를 처리할까?
예를 들어 Call Stack에 정상적으로 처리되고 있는 함수들이 있다고 가정을 해보자. 그런데 어떤 함수를 잘 처리하고 있던 와중에 갑자기 Event Loop에서 Callback Queue의 내용을 Call Stack으로 Push 해서 처리해야 한다고 한다. 그래서 어쩔 수 없이 잘 처리하고 있던 함수를 중단하고 Event Loop가 보낸 함수를 처리해야 한다. 그런데 또 갑자기 Event Loop에서 Callback Queue의 내용을 Call Stack으로 Push 해서 처리해야 한다고 한다. 과연 이 경우 실행의 결괏값을 어떻게 될까? 과연 내가 처리하려고 했던 함수는 내가 예상했던 시간에 끝나고 값을 제대로 도출할 수 있을까?
간단히 말하자면 이벤트 루프가 반드시 Call Stack이 비어져있는 상태에서만 Call Stack으로 Push 하는 이유는 자바스크립트라는 언어가 동기화 문제를 안는 것을 피하고 단일 스레드 언어라는 것을 보장해주기 위함이다.만약 단일 스레드 환경에서 위에서 예를 들었던 것 같은 상황이 발생한다면, 그것은 멀티 스레드에서 발생하는 문제 상황을 그대로 단일 스레드 언어가 안게 되어버린다. 따라서 단일 스레드 언어에서 “해당 함수를 중단하지 말고 실행이 끝난 뒤에 Event Loop가 보내준 함수를 처리해야 해!”같은 동기화 문제를 해결할 수 있는 요소가 추가로 필요해지게 되는 것이다.
결국에는 Event Loop가 Call Stack이 비어있는지를 확인하게 함으로써 프로그래머는 동기화 문제에 대해 골머리를 앓을 필요가 없어지는 것이다.
댓글남기기