자바스크립트 뿐만 아니라 프로그래밍 언어를 배우면서 처음에는 그저 코드 짜기에만 급급했습니다. 그 언어의 작동원리 같은 건 봐도 도무지 이해가 안갔기에 관심이 없었습니다.
시간이 점차 흘러 우연히 자바스크립트의 작동원리 관련 글을 다시 보게되었는데 글이 어렵지 않고 이해가 되기 시작했습니다. "그때 작성했던 코드가 이래서 이런식으로 작동했구나" 를 깨달으면서 자바스크립트에 대해 더 자세히 알게 되고 나니 한층 더 코딩이 재미있어졌습니다.
그래서 이번에는 자바스크립트의 작동원리에 있어 핵심 원리인 JavasScript Engine(V8), Web API, Callback Queue, 이벤트 루프 그 중 특히 이벤트 루프 에 대해 자세히 알아보려고 합니다.
틀린 부분이나 질문 사항이 있으시면 언제든지 댓글로 남겨주세요 :)
자바스크립트란?
자바스크립트 는 Git에서 여전히 상위 랭크에 위치 되어있고 인기가 점점 많아지고 있는 언어 중의 하나입니다. 자바스크립트는 싱글 스레드 논블로킹 기반 인데, 싱글 스레드라는 점이 가장 큰 특징 중 하나입니다. 싱글 스레드는 이름처럼 단 하나의 스레드만 사용할 수 있어서 동시에 하나의 작업만 처리할수 있습니다.
('싱글 스레드' 관련 글)
* 논블로킹(비동기)과 블로킹(동기)이란?
손님이 식당에 가서 요리를 주문했다고 하면, 손님이 요리가 나올 때까지 아무것도 안하고 단지 기다리기만 한다면 블록 상태(동기) 이고, 요리를 기다리는 동안 핸드폰을 하거나, 화장실을 갔다오거나 등 다른 행위를 할 수 있으면 논블록 상태(비동기) 입니다.
('논블로킹과 블로킹' 관련 글)
위 그림은 자바스크립트가 어떻게 작동하는지 아주 잘보여주는 그림입니다. 요소로는 V8 엔진(runtime Engine), Web API, 이벤트 루프, Callback Queue가 있는 데 먼저 이벤트 루프를 제외하고 간략히 해당 요소가 무엇인지 알아보겠습니다.
Javascript Engine(V8)
V8은 크롬과 Node.js에서 사용되는 구글이 만든 엔진입니다. 크게 Memory Heap 과 Call Stack 으로 구성되어 있습니다. 자바스크립트 코드를 읽고 해석해서 실행하는 것을 담당하고 있다 생각하시면 됩니다. 기본적으로 웹 브라우저와 Node.js에 탑재되어 있습니다.
- Memory Heap : 우리가 코드를 짤때 선언하는 변수나 함수 등이 담겨져 있습니다.
- Call Stack : 코드가 실행될 때 쌓이는 곳입니다.
(사실 Javascript Engine만 봤을 때 Call Stack이 하나라 비동기를 처리해줄 수 있는 부분이 없다고 생각해 의아해 하실 수도 있는데 이 행위를 해주는 곳이 바로 밑에서 언급할 Web API입니다.)
* Stack이란?
자료구조 중 하나로 가장 처음으로 들어온 것이 가장 마지막에 나가는 선입후출(LIFO, Last In First Out)입니다. 재귀함수를 생각하면 쉽습니다.
Web API
DOM, Ajax, setTimeout(), Event Handler 등과 같이 웹 브라우저에서 제공하는 기능들을 말합니다. Call Stack에서 실행된 비동기 함수는 Web API를 호출하고, Web API는 콜백함수를 Callback Queue에 집어 넣습니다.
Javascript Engine의 싱글 스레드 외에 스레드를 지원해줍니다. 따라서 자바스크립트에서 비동기를 할 수 있게 해줍니다.
Callback Queue
비동기로 실행되야하는 콜백함수가 보관되는 영역입니다. setTimeout에서 타이머 완료 후 실행되는 함수가 Stack에 쌓여 실행되기 전 보관되어 있던 곳이 바로 Callback Queue입니다. 이해가 안가시더라도 밑에서 자세히 다루니 여기는 읽고 넘어가셔도 됩니다.
* Queue란?
Stack과 마찬가지로 자료구조 중 하나로 가장 처음으로 들어오는 것이 가장 처음으로 나가는 선입선출(FIFO, First In First Out)입니다.
(저는 Stack이랑 엄청 헷갈렸었는데 당구의 큐(Queue)대를 생각하면 안헷갈리더라고요. 큐대 모양 생각하면서 '큐대 뒷부분으로 들어와서 큐대 앞부분으로 나간다' 이런식으로 구분했었습니다 ㅎ)
Call Stack, Event loop, Callback Queue
그럼 이제 본격적으로 코드를 이용해 Call Stack을 이해해보겠습니다.
function multiply(a, b){
return a*b;
}
function square(n){
return multiply(n, n);
}
function printSquare(n){
var squared = square(n);
console.log(squared);
}
printSquare(4);
(여기를 가셔서 실행시키면 해당 코드가 어떻게 작동하는지 이해하기 훨씬 쉽습니다.)
각각의 함수들은 Stack에 위와 같이 차례대로 쌓이게 됩니다. 그리고 Stack은 LIFO의 특징을 가지므로 함수가 종료되면 가장 위에 있는 함수부터 Stack에서 제거됩니다.
multiply가 제거되고, square가 제거되고 printSquare 함수 안에 있는 console.log가 다시 Stack에 쌓입니다. 이런식으로 Stack에 쌓이고 제거되면서 마지막에는 결국 Stack 아무것도 남아있지 않습니다. 이것이 바로 자바스크립트에서 동작하는 Call Stack의 역할입니다.
만약 함수가 에러를 발생시킨다면 어떻게 될까요?
function foo(){
throw new Error('Oops!');
}
function bar(){
foo();
}
function baz(){
bar();
}
baz();
개발자도구에서는 이런식으로 Stack의 꼬리를 물면서 Oops!를 표시하게 됩니다. 에러가 발생한 Stack의 상태는 보여주는 것입니다. 에러는 foo에서 생겼는데 그것을 bar가 호출했고 bar는 또 baz에게서 호출되었기 때문에 위와 같은 상태를 보여줍니다.
자바스크립트에서 싱글 스레드라고 하는 것은 루비 같은 언어와는 달리 여러 개의 스레드를 사용하지 않는다는 의미입니다. 따라서 네트워크 요청 같은 다소 느린 작업은 작업이 완료되기까지 마냥 기다려야 하기 때문에 컴퓨터의 작업 처리 속도를 현저히 다운시킵니다. (사람한테는 여전히 빠르지만 컴퓨터 기준에서는 느립니다)
이때 필요한 것이 바로 비동기 콜백입니다. 아래 코드를 보시겠습니다.
console.log("hi");
setTimeout(function cb(){
console.log("there");
}, 5000);
console.log("JSConf");
(여기를 가셔서 실행시키면 해당 코드가 어떻게 작동하는지 이해하기 훨씬 쉽습니다.)
setTimeout 함수 같은 경우 대표적인 비동기 함수인데 위에 그림에서 보면 역시 한번 실행 된후 Stack에서 사라집니다.
그 후 Stack이 완전 빈 상태인 후에 Stack에 다시 setTimeout의 콜백이 쌓이는 걸 볼 수 있습니다.(5000이였으니 5초 후 Stack에 쌓입니다.)
왜 이렇게 되는걸까요? 정답은 바로 이벤트 루프에 있습니다.
자바스크립트는 싱글 스레드이기 때문에 당연히 다른 코드를 실행시키는 동안 setTimeout 함수를 실행시킬 수 없습니다. 하지만 우리가 동시에 여러 작업을 할 수 있는 것은 브라우저가 단순 런타임 이상의 것을 수행하기 때문입니다. 위에서 언급한 바와 같이 자바스크립트는 크게 Javascript Engine과 Web API로 구성된다 했는데 여기서 바로 Web API의 역할이 빛을 발합니다.
브라우저에서 제공되는 Web API는 자바스크립트에서 호출할 수 있는 스레드를 효과적으로 지원해줍니다. 즉, 싱글 스레드로 작동하는 자바스크립트를 멀티 스레드로 작동할 수 있게끔 도와줍니다.
console.log("hi");
setTimeout(function cb(){
console.log("there");
}, 5000);
console.log("JSConf");
그럼 다시 위 코드를 보겠습니다. 이번에는 Stack뿐만 아니라 Web API와 Callback Queue도 고려해보겠습니다. 시점은 setTimeout이 막 Stack에서 삭제되고 Web API에 쌓일 때 입니다.
자바스크립트 런타임 환경에 존재하는 별도의 API(Web API)에 setTimeout 함수를 넣고 타이머를 실행키시고 카운트 다운을 시작합니다. setTimeout 함수의 호출 자체는 완료되었기 때문에 Stack에서 삭제됩니다. 그리고 'JSconf'가 Stack에 쌓이고 호출된 후 삭제됩니다. 이제 Web API에서 실행하고 있는 타이머만이 남았습니다. 5초 뒤에 타이머가 종료되는데 Web API는 갑자기 해당 setTimeout을 Stack에 넣을 순 없습니다.
그럼 Stack에 어떻게 넣을 수 있을까요?
이제는 Callback Queue가 활약할 차례입니다. 모든 Web API는 작동이 완료되면 setTimeout 내 콜백을 Callback Queue에 집어 넣습니다.
이벤트 루프는 이 전체 시스템에서 아주 단순한 일을 하는 작은 요소입니다. 이벤트 루프의 역할은 Call Stack과 Callback Queue를 주시하는 것입니다. Stack이 비어있으면, Callback Queue에 첫번째 콜백을 Stack에 쌓아 효과적으로 실행할 수 있게 해줍니다.
Stack에 console.log가 쌓이기 전에 Stack은 아직 비어있는 상태인데 이러한 빈 상태를 보고 이벤트 루프는 '어, 뭐야 Stack이 비었잖아? 드디어 내가 할일이 있네. 자 이 이거 받아!' 하며 콜백을 Stack에 넣어줍니다. 이후 자바스크립트 엔진이 다시 console.log를 Stack에 쌓아줍니다. 따라서 위 그림처럼 console.log가 Stack에 쌓인 걸 보실 수 있습니다.
setTimeOut의 인자를 0으로 하던, 1000으로 하던, 10000으로 하던 위 코드의 결과는 항상 같습니다. 지금까지의 글을 잘 이해하셨다면 왜 setTimeout의 인자를 0으로 했을 때 바로 실행되지 않았는지, 왜 setTimeout의 인자(초)가 정확하지 않은지 알 수 있습니다.
모든 종류의 Web API는 이런식으로 동일한 방식으로 동작합니다. 좀 더 심화된 예제를 들어보겠습니다.
console.log('Started');
$.on('button', 'click', function onClick(){
console.log('Clicked');
});
setTimeout(function onTimeout(){
console.log('Timeout Finished');
}, 5000);
console.log('Done');
(여기를 가셔서 실행시키면 해당 코드가 어떻게 작동하는지 이해하기 훨씬 쉽습니다. 클릭이벤트는 코드 아래에 Click me!를 누르시면 됩니다.)
위 코드 같은 경우 이벤트 리스너가 Web API에 보관되어서 해당 버튼('button')을 누를 때마다 이벤트를 호출하는데 Web API에서 Callback Queue에 이벤트 리스너의 콜백을 보관 후 Stack에 console.log('Clicked')를 쌓습니다. 그리고 나머지는 동일합니다.
이외에도 여러 개의 setTimeout이라던가, 프론트에서의 스크롤 관련 이벤트라던가 여러가지 예제가 있지만 나머지는 독자분들에게 맡기겠습니다. 스크롤 관련한 비동기 방식은 꼭 해보시길 권장해드립니다.
맺으며..
생각보다 글이 엄청 길어졌네요.. 지금까지 작성한 글 중에서 가장 분량이 많았던 것 같습니다. 하지만 블로그(겨우 4개월..)를 관리하면서 가장 유익하고 재밌었던 내용이였습니다.
async/await와 콜백 함수, 자바스크립트에서의 비동기 관련한 퍼즐들이 드디어 맞쳐진것 같습니다. 전에 콜백함수 관련 글을 쓴적이 있는데 완전 난장판이였다는 것도 알수 있었습니다. 자바스크립트를 막 배운 분들께는 다소 어려울 수 있지만 계속 자바스크립트를 사용할 예정이라면 반드시 이해하고 넘어갈 부분이기도 한것 같습니다. 상급 개발자분께서 이런 거 몰라도 개발 잘한다고 하시는데 확실한게 좋잖아요!!
다음에는 자바의 작동원리에 대해 공부해봐야 할 것 같습니다. 긴 글 읽어주셔서 감사합니다 :)
읽어주셔서 감사합니다.
Conference
'...' 카테고리의 다른 글
[Java] 자바의 동작과정 Java Compiler와 JVM (16) | 2021.05.11 |
---|---|
[CA] 바이너리 파일과 바이트 파일 그리고 컴파일, 링크 (4) | 2021.05.10 |
[OS] 더 이상 어버버하지말자!! 스레드와 프로세스 (13) | 2021.04.26 |
[Python] list(리스트)의 얕은 복사와 깊은 복사 (0) | 2021.04.25 |
[Node.js] uuid 라이브러리 설치 및 사용하기 (0) | 2021.04.12 |