왜 자바스크립트의 원리에 대해 알아야 할까요?
원리에 대한 이해가 있으면 (1)성능이 좋은 코드를 작성할 수 있고, (2)디버깅, 리팩토링 시 올바른 가설을 세우는데 도움이 됩니다.
그래서 이번 글을 준비하였습니다. 이 글은 당연하게도 바닥부터 끝까지 혼자서 작성한 것이 아니라 훌륭한 글들의 도움을 많이 받았습니다. 미리 참고 부탁드리며, 그럼 시작해보겠습니다!
자바스크립트(JS)가 어떻게 동작하는지 안다는 건 구체적으로 무엇을 안다는 것일까요? 언어에 상관없이, 코드는 기계가 이해할 수 있도록 번역되고, 번역 결과물은 실행됩니다. 따라서 JS가 어떻게 동작하는지 안다는 건 (1)번역 방식을 이해하고, (2)번역 결과물을 실행하는 규칙을 이해한다는 뜻이라고도 할 수 있겠네요.
(1)번역 방식을 이해한다는 뜻은, 다음 내용을 안다는 것입니다.
JS의 번역 원리와 결과물을 알고 설명할 수 있습니다. 구체적으로, JS의 번역 결과물이 바이트코드와 머신코드임을 알고, 각각의 차이를 압니다. JS Engine인 V8의 구성을 이해하고, 인터프리터와 컴파일러의 동작 원리와 이유를 설명할 수 있습니다.
(2)실행을 이해한다는 뜻은, 다음 내용을 안다는 것입니다.
JS가 실행되는 규칙의 이유와 원리를 설명할 수 있다. 구체적으로, Event Loop의 원리를 이해하고 설명할 수 있다. Microtask, Macrotask의 차이를 이해하고 설명할 수 있다.
이해하면, 장점과 단점을 꼽을 수 있습니다. 깊게 이해하면, 발전 방향을 제시할 수 있습니다.
아래 질문에 답하는 것입니다.
function doFunc(obj) {
return obj;
}
function f1() {
let start, end;
start = new Date();
for (let i=0; i<100000000; i++) {
let obj = {};
if (i%2) {
obj.x = 1;
obj.y = 2;
} else {
obj.y = 2;
obj.x = 1;
}
doFunc(obj);
}
end = new Date();
console.log('f1', end - start);
}
function f2() {
let start, end;
start = new Date();
for (let i=0; i<100000000; i++) {
let obj = {};
if (i%2) {
obj.x = 1;
obj.y = 2;
} else {
obj.x = 1;
obj.y = 2;
}
doFunc(obj);
}
end = new Date();
console.log('f2', end - start);
}
f1(), f2();
2. 아래 코드의 실행 결과는 어떻게 될까요?
console.log('script start')
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout(() => {
console.log('setTimeout 1')
Promise.resolve()
.then(() => console.log('promise 3'))
.then(() => console.log('promise 4'))
.then(() => {
setTimeout(() => {
console.log('setTimeout 2')
Promise.resolve().then(() => console.log('promise 5'))
.then(() => console.log('promise 6'))
.then(() => clearInterval(interval))
}, 0)
})
}, 0)
Promise.resolve()
.then(() => console.log('promise 1'))
.then(() => console.log('promise 2'))
우선 우리는, 번역의 원리부터 이해하겠습니다.
인터프리터 언어란 무엇일까요? 일단 실행하고 보는 친구입니다. 성능과 런타임 오류에 리스크가 있습니다.
컴파일 언어는 무엇일까요? 검사하고, 최적화한 뒤, 실행하는 친구입니다. 시작이 지연되나, 제대로 합니다.
자바스크립트는 인터프리터 언어입니다. 시작이 빠르나, 본질적으로 성능과 런타임 오류에 리스크가 있습니다. 그럼에도 불구하고 20년 간 번성해왔습니다. 성능이 나아졌다는 뜻일 텐데요. 어떻게 했을까요? 이번 챕터에서는 이 질문을 다룬다.
엔진의 구성과, 엔진의 주요 결과물과, 최적화 원리를 알면 이 질문에 대답할 수 있습니다.
JS 엔진은 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);
위 코드를 실행할 때, Call Stack은 다음과 같이 변합니다.
따라서 복잡한 계산, 오래 걸리는 계산을 하는 경우 다른 일은 전혀 할 수가 없습니다. 그럼에도 불구하고 왜 계산기를 한 대만 두었을까요? 메모장에 모르는 누군가가 몰래 낙서를 할 일이 없기 때문입니다. 이것을 thread-safe 하다고 하는데요. 오류의 발생 후보를 완전히 없앴으므로, 좋은 것이겠네요.
그러나 이대로 좋을까요?
계산이 오래 걸리는 이유가 기다림 때문이라면 해결할 수도 있지 않을까요? 다른 친구에게 물어본 뒤 그 대답을 기다리는 것 때문이라면, 일단 다른 계산을 하다가 대답이 왔을 때 남은 일을 마저 하면 되지 않을까요?
하지만 JS 엔진은 이렇게 일할 줄 몰라서, 도와주는 친구가 필요합니다. 이렇게 동시에(Concurrently) 일할 수 있도록 도와주는 친구를 Event Loop 라고 부릅니다. Event Loop는 추후 내용에서 보다 자세히 다루고, 지금은 전체적인 그림만 머리에 넣어두겠습니다.
다시 돌아와서, 엔진은 메모장(Memory)과 계산기(CPU)를 이용하여 JS를 실행합니다. 그러나 JS 그대로 실행하지는 않습니다. CPU가 이해할 수 있도록 번역해야 합니다. 이 번역의 과정은 두 단계로 나뉘는데요. (1)가볍게 번역하고, (2)제대로 번역합니다. 이게 무슨 말일까요?
우리가 실리콘밸리 기술 컨퍼런스에서 50분 간 발표한다고 생각해보겠습니다. 일단 대본을 한글로 작성합니다. 대본을 인공지능 번역기로 번역할 것이고요. 발표의 핵심이 아닌 부분은, 부족한 내 영어 실력으로 그냥 적당히 고치겠지요. 가볍게 인사하고 농담 나누는 부분은 말만 통하면 되겠습니다.
하지만 핵심이 되는 부분은 필요하다면 전문 번역가의 도움을 받아서라도 빡세게 번역하겠죠. 말 그대로 핵심이기 때문입니다. 단어 하나도 고치고 고쳐서 말하고 싶은 바를 풍부하고 정확하게 전달하기 위해 노력합니다.
엔진이 하는 일도 이와 같습니다. JS를 CPU가 이해할 수 있게 번역해야 하나, 전부 빡세게 할 필요는 없으므로 일단 가볍게 바이트코드(bytecode)로 번역(interpret)합니다. 이것은 CPU 입맛에 딱 맞지는 않지만, 괜찮습니다. 어차피 핵심은 아니니까요.
그러나 코드의 핵심부는 다릅니다. CPU 입맛에 딱 맞는 기계어(machine code)로 최적화된 번역(compile)합니다. 한 번 하고 마는 것이 아니라, 최적화 후 성능을 살펴보고 별로라면 다시 bytecode로 돌린 뒤 재번역합니다. 수고를 아끼지 않는 것이죠.
여담으로, V8의 인터프리터 이름은 Ignition이고 컴파일러 이름은 TurboFan 입니다. 이름을 참 잘지었다는 생각이 드는데요. Ignition은 말 그대로 JS를 점화(Ignition)하여 일단 불이 붙게, 그러니까 코드가 돌아가게 만들어 줍니다. TurboFan은 특히 자주 사용하는(hot) 부분을, 차갑게 식혀줍니다. 그것도 그냥 Fan이 아니라 TurboFan으로.
여튼 다시 돌아와서, JS는 싱글 스레드로 실행된다고 했습니다. 그런데 위에 언급된 일들은 한두 개가 아니죠. 바이트코드를 만들고, 코드 실행을 평가하고, 기계어로 번역한다. 저런 일들은 누가 하는 것일까요?
당연하게도 스레드가 여럿 있습니다. 메인 계산기가 하나라는 뜻이지, 스레드가 하나밖에 없다는 의미가 아니니까요. 그럼 주요 스레드는 무엇이 있을까요?
여기까지 V8 엔진의 동작 원리를 빠르게 살펴보았는데요. 다음 글에서는 최적화 원리를 다뤄보도록 하겠습니다.
https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf
https://www.fhinkel.rocks/posts/Understanding-V8-s-Bytecode