안녕하세요. 휴먼스케이프에서 개발을 하고 있는 권주희(Victoria)입니다.
어떠한 언어로 프로젝트를 진행하던지 완성도 높고 안정적인 소프트웨어를 제작하기 위해선 해당 언어의 언어적 특성과 내부 구조를 이해하는 것이 중요합니다.
오늘은 JavaScript는 어떻게 동작하는지 그 환경과 JavaScript를 해석하고 실행시키는 엔진에 대해서 알아보겠습니다.
JavaScript Engine
자바스크립트를 해석하는 JavaScript Engine과 웹 브라우저에 화면을 그리는 Rendering Engine은 서로 다릅니다.
Rendering Engine 또는 Layout Engine은 HTML과 CSS로 작성된 코드를 컨텐츠로 사용하여 웹 페이지에 “rendering” 하는 역할을 합니다. 렌더링 역할을 하는 엔진이 브라우저마다 다르기 때문에 같은 페이지가 다르게 보이는 경우도 종종 있습니다. 렌더링 엔진의 예로는 WebKit, Blink, Gecko 등이 있습니다.
반면, JavaScript 엔진이란 JavaScript로 작성한 코드를 해석하고 실행하는 인터프리터입니다. 대표적으로 구글의 자바스크립트 엔진이자 Node.js에서 사용되는 엔진인 V8이 있습니다.
JavaScript 엔진은 표준적인 인터프리터로 구현될 수도 있고 자바스크립트 코드를 바이트 코드로 컴파일하는 저스트인타임(just-in-time) 컴파일러로 구현할 수 있습니다. 위에서 언급된 V8의 경우, 웹 브라우저 내부에서 JavaScript 수행속도 개선을 위해 처음 고안되었습니다. 속도 개선을 위해 인터프리터를 사용하는 대신 JavaScript 코드를 머신코드로 번역하는 JIT 컴파일러로 구현되었습니다.
대부분의 JavaScript Engine은 Call Stack, Task Queue(Event Queue), Heap 이라는 세 가지 영역으로 구분됩니다. 추가적으로 Event Loop라는 아이로 Task Queue의 task들을 관리할 수 있습니다. 🙂
Call Stack
Call Stack은 코드가 실행되면서 스택 프레임이 쌓이는 장소입니다.
JavaScript는 단 하나의 호출스택(Call Stack)을 사용합니다. 이러한 특징때문에, JavaScript의 함수가 실행되는 방식을 “Run to Completion”이라고 합니다. 이는 단 하나의 함수가 실행되면, 해당 함수의 실행이 끝날 때까지 다른 어떤 Task들도 수행될 수 없음을 의미합니다.
👉 JavaScript는 Single Thread다!
JavaScript Engine은 어떤 요청이 들어올 때마다 해당 요청을 순차적으로 Call Stack에 담아 처리합니다. 메소드가 수행될때, Call Stack에 새로운 프레임이 Push되고 메소드의 실행이 끝나면 해당 프레임은 Pop 됩니다.
우리는 Call Stack을 통해 현재 프로그램이 어디에 위치해 있는지 파악할 수 있습니다. 간단하게 예를 들어볼까요?
function add(x, y){ return x+y; } function first(){ var i = add(1,1); console.log(i); } first();
코드를 수행하게 되면 빈 콜스택에 first()라는 스택 프레임이 추가될거에요. 단계는 아래 그림과 같습니다 :)
Image of Stack Frame Step
스택 오버플로우
이름 그대로 스택의 사이즈를 초과했을 때 발생하는 오류로, 재귀를 잘못 구현했을 때 쉽게 마주할 수 있는 오류입니다.
콜스택이 터져버린 에러
function boom() { boom(); } boom();
위의 코드를 보면 boom이라는 함수가 실행되면 또 다시 자신을 호출하는 모습이 보여지는데, 이렇게 되면 Call Stack에 계속 boom이라는 스택 프레임이 쌓이게되면서 위 사진과 같은 에러를 보이게 됩니다 :)
Heap
동적으로 생성된 객체(인스턴스)는 Heap 영역에 할당됩니다.
동적으로 할당되는 변수의 경우, 컴파일러는 얼마나 많은 메모리를 필요로 할지 알 수가 없습니다. 따라서 컴파일러는 스택에 변수를 위한 공간을 할당할 수 없기 때문에, 동적 변수를 런타임 시점에 Heap 공간에 할당받습니다.
정적할당 메모리와 동적할당 메모리의 차이
Task Queue(Event Queue)
JavaScript의 런타임 환경에서는 처리해야하는 Task들을 임시로 저장하는 대기 Queue가 존재합니다. 그런 대기큐를 Task Queue 또는 Event Queue라고 합니다.
이 Task Queue에 들어있는 Task들은 Call Stack이 empty일때, 대기열에 들어온 순서대로 수행됩니다.
setTimeout(function() { console.log('안녕하세요'); },0); console.log('안녕히가세요');
위와 같은 함수는 얼핏보면, setTimeout에 0ms을 주었기 때문에 delay되지 않고 바로 수행될 것 같습니다. 하지만 이 함수의 결과값을 보게되면,
안녕히가세요 안녕하세요
의 순서로 콘솔창에 뜨는 것을 확인할 수 있습니다.
JavaScript에서 비동기로 호출되는 함수들은 Call Stack에 쌓이는 것이 아니라 Task Queue로 enqueue됩니다. 자바스크립트 엔진 영역이 아닌 Web API 영역에 정의되어있는 함수들(ex. setTimeout)은 비동기로 실행됩니다.
아래 코드를 보고 console창에 어떤 순서로 log가 남을지 유추해보시죠!😃
function quiz1(){ console.log('quiz1'); quiz2(); } function quiz2(){ let timer = setTimeout(function(){ console.log('quiz2'); },0); quiz3(); } function quiz3(){ console.log('quiz3'); } quiz1();
우선, console창에 quiz1이 출력될거에요.
quiz2가 호출되면서 setTimeout함수가 콜스택에 들어가(push) 실행되고 바로 빠져나올거에요(pop). 이때 setTimeout함수 내부에 quiz2를 출력할 익명함수는 콜스택이 아닌 Task Queue 영역으로 들어가게 됩니다. 그 다음 quiz3이 콜스택에 push되겠죠?
콜스택에 들어간 quiz3이 실행되면서 console창에 quiz3이 출력되겠군요!
quiz3의 실행이 끝나면서 콜스택에서 pop이 되고, 뒤 이어 quiz2와 quiz1도 차례로 pop될 거에요.
이때 콜스택이 empty가 되면서 Task Queue의 head에서 event를 가져와 콜스택에 넣게되고, 이 익명함수가 실행되면서 quiz2가 출력되겠네요 :)
결과적으로
quiz1 quiz3 quiz2
의 순서로 console창에 출력되는 것을 확인하실 수 있습니다.
결과적으로, 이벤트에 걸려있는 핸들러는 절대 먼저 실행될 수 없습니다!
여기서 잠깐!
만약 호출스택에 어마어마하게 수행시간이 긴 함수가 있으면 어떻게 될까요?
호출스택에 긴 수행시간이 걸리는 함수가 있을 시, 브라우저는 해당 페이지를 렌더링할 수도 없고, 다른 코드도 수행할 수없는 ‘블록킹’ 상황에 처해버립니다. 이러한 상황에서 대부분의 브라우저는 에러를 발생시켜 페이지를 닫을지 여부를 물어보기도 합니다.
이러한 상황에서 우리는 브라우저의 렌더링 동작을 방해하지 않고, 브라우저의 응답을 끊지도 않으면서 연산량이 많은 코드를 수행하기 위해 비동기 콜백(Asynchronous Callbacks)을 사용해야 합니다.
JavaScript의 처리속도
JavaScript는 Single Thread로 한번에 한 작업만 수행가능합니다.(Run to Completion) 그렇다면 어떻게 동시성이 가능한 것일까?
Web API는 브라우저에서 제공되는 API이며, AJax나 Timeout같은 비동기 작업을 실행합니다. 자바스크립트에서 setTimeout함수를 실행하면, 자바스크립트 엔진은 Web API에 setTimeout을 요청합니다.
CallStack에서는 Web API 요청 이후, setTimeout 함수가 종료되기 때문에 제거됩니다(pop). 그래서 Web API는 방금 요청받은 setTimeout을 완료하고, 전달받은 Callback을 Task Queue에 넘겨줍니다.
Task Queue는 Callback Queue라고도 하는데, Web API에서 넘겨받은 Callback 함수를 저장하는 Queue입니다. 이 Callback 함수들은 Task Queue에 enqueue되어 있다가 자바스크립트 엔진의 CallStack이 비었을 때, Call Stack으로 push됩니다.
이때, Event Loop가 Call Stack이 비어있는지 아닌지, Task Queue에 Task가 존재하는지 아닌지 판단하는 역할을 맡습니다.
마치면서
지금까지 JavaScript 동작원리에 대해 살펴보는 시간을 가졌습니다.
위 글과 관련하여 추가적인 정보를 얻고 싶으신 분들은 아래 첫 번째 링크를 통해 살펴보셔도 좋을 것 같습니다.
또한 자바스크립트에 대해 더 알고싶으신 분은 두번째 링크를 통해 공부해보셔도 좋을 것 같습니다.
링크 : https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf
모든 자바스크립트 개발자가 알아야 하는 33가지 개념 : https://github.com/yjs03057/33-js-concepts
감사합니다 😃