역할
자바스크립트 코드를 실행하는 프로그램 혹은 인터프리터
가능한 짧은 시간 내에 가장 최적화된 코드를 생성하는 것이 목표
각각의 자바스크립트 엔진은 특정 버전의 ECMAScript를 구현하기 때문에, ECMAScript가 발전하는 만큼 엔진도 발전
예시
엔진의 2개 구성요소는
만들어진 이유
특징
속도 향상을 위해 V8은 인터프리터를 사용하는 대신 자바스크립트 코드를 더 효율적인 머신 코드로 번역
저스트인타임 컴파일러를 구현함으로써 코드를 실행 시에 자바스크립트 코드를 머신 코드로 컴파일하는데, 이는 스파이더몽키나 리노와 같은 현대적인 다른 자바스크립트 엔진에서도 마찬가지
주된 차이는 V8은 바이트코드와 같은 중간 코드를 생산하지 않는다는 점입니다.
인라이닝이란
미리 가능한 많은 코드를 인라이닝(inlining)하는 것
딕셔너리를 이용해서 메모리 상에서 객체 속성의 위치를 찾아내는 것은 매우 비효율적인 일이기 때문에 V8에서는 다른 방법을 이용합니다. 바로 히든클래스(hidden classes)가 그것
예시로 보는 작동방식
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(1, 2);
p1.a = 5;
p1.b = 6;
var p2 = new Point(3, 4);
p2.b = 7;
p2.a = 8;
일단 new Point(1, 2)
이 실행되면 V8은 이라는 C0
히든클래스를 생성합니다.
Point
에 아직 아무 속성도 정의되지 않았으므로 C0
은 비어있습니다.this.x = x
가 수행되면(Point
함수 내에서)
V8은 C0
을 기반으로 C1
이라는 두 번째 히든 클래스를 생성
C1
은 x
속성을 찾을 수 있는 메모리상의 위치(오브젝트 포인터에 상대적임)에 대한 설명이 포함
예제의경우 x
는 오프셋 0에 저장되는데 이는 연속된 버퍼로서 해당 메모리의 포인트 객체를 읽을 때 첫 번째 오프셋이 x
속성에 대응한다는 것을 의미
V8은 또한 C0
을 클래스전환으로 업데이트하는데 여기에는 만약 x
속성이 포인트 객체에 추가되면 히든 클래스가 C0
에서 C1
으로 전환되어야 한다는 내용이 있습니다.
어떤 객체에 새로운 속성이 추가될 때마다 오래된 히든클래스는 새로운 히든 클래스에 대한 전환 경로로 업데이트됨.
히든클래스 전환이 중요한 이유는 이를 통해 히든 클래스가 같은 방식으로 생성된 객체들 사이에 공통으로 사용될 수 있기 때문
만약 두 개의 객체가 하나의 히든클래스를 공유하고 같은 속성이 이들에게 추가되면 전환과정은 이들 객체가 동일한 새로운 히든클래스를 받도록 하고 그에 따라 그에 딸려 오는 최적화 코드도 모두 동일합니다.
이 과정은 this.y = y
가 수행될 때도 반복됩니다(마찬가지로 Point
함수 내에서 this.x = x
구문 뒤에서)
C2
라는 새로운 히든클래스가 생성되고 C1
에 클래스전환이 추가되며 여기에는 y
속성이 포인트 객체(이미 x
속성을 갖고 있는 그 객체)에 추가되었다는 내용이 명시C2
로 변경되어야 하며 포인트 객체의 히든 클래스는 C2
로 업데이트p1
과 p2
에 대해 같은 히든클래스와 전환이 사용될 것이라고 생각할지도 모르겠습니다만 실제로는 그렇지 않음p1
에서는 속성 a
가 추가되고 그 다음 b
가 추가됩니다. 하지만 p2
의 경우 b
가 먼저 할당되고 a
가 할당됩니다. 따라서 p1
과 p2
는 서로 다른 히든클래스를 사용하게 되고 결국 전환 경로도 달라집니다. 이와 같은 경우에 같은 히든클래스를 재사용할 수 있도록 동적 속성을 같은 순서로 초기화하는 것이 훨씬 좋습니다.userData
객체를 반복해서 찾는 대신 found ${user.firstName} ${user.lastName}
를 found johnson junior
라는 문자열으로 대체하여 실행 속도를 향상시킨다.function findUser(user){
return `found ${user.firstName} ${user.lastName}`
}
const userData = {
firstName : 'johnson'
lastName : 'junior'
}
findUser(userData)
b’
속성은 서로 다른 순서로 생성되었습니다.하이드로젠 그래프가 최적화되면 크랭크샤프트는 이를 리튬이라고 부르는 더 하위레벨로 낮춥니다.
리튬은 머신 코드로 컴파일됩니다. 그런 다음 OSR(on-stack replacement, 온스택교환)이라는 것이 일어납니다.
분명하게 수행시간이 긴 메소드를 컴파일하고 최적화하기 전에 그것을 실행할 가능성이 높습니다.
V8은 더 최적화된 버전으로 다시 시작하기 위해 방금 어떤 코드가 느리게 수행됐는지 잊지 않을 것
대신 우리가 가진 모든 맥락(스택, 레지스터 등)을 전환하여 코드의 수행 중간에 최적화된 버전으로 옮겨탈 수 있도록 해줍니다.
표시하고-쓸기(Mark-and-sweep) 알고리즘
이 알고리즘은 "더 이상 필요없는 오브젝트"를 "닿을 수 없는 오브젝트"로 정의한다.
이 알고리즘은 roots 라는 오브젝트의 집합을 가지고 있다(자바스크립트에서는 전역 변수들을 의미한다). 주기적으로 가비지 콜렉터는 roots로 부터 시작하여 roots가 참조하는 오브젝트들, roots가 참조하는 오브젝트가 참조하는 오브젝트들... 을 닿을 수 있는 오브젝트라고 표시한다. 그리고 닿을 수 있는 오브젝트가 아닌 닿을 수 없는 오브젝트에 대해 가비지 콜렉션을 수행한다.2012년 기준으로 모든 최신 브라우저들은 가비지 콜렉션에서 표시하고-쓸기 알고리즘을 사용한다. 지난 몇 년간 연구된 자바스크립트 가비지 콜렉션 알고리즘의 개선들은 모두 이 알고리즘에 대한 것이다. 개선된 알고리즘도 여전히 "더 이상 필요없는 오브젝트"를 "닿을 수 없는 오브젝트"로 정의하고 있다.
자바스크립트 엔진이 구동되면서 코드를 읽고 실행하는 과정에서 아주 중요한 두가지 단계가 있는데,
여기서 정보를 저장하는 공간(Memory Allocation이 발생하는 공간)이 바로 메모리 힙(Memory Heap)이고, 실행 중인 코드를 트래킹하는 공간이 콜 스택(Call Stack)이다.
메모리 생존주기는 프로그래밍 언어와 관계없이 비슷하다.
두 번째 부분은 모든 언어에서 명시적으로 사용된다. 그러나 첫 번째 부분과 마지막 부분은 저수준 언어에서는 명시적이며, 자바스크립트와 같은 대부분의 고수준 언어에서는 암묵적으로 작동한다.
메모리 힙은 변수, 함수 저장, 호출 등의 작업이 발생하여 위 내용들이 진행되는 공간이다.
쉽게 생각하면 'Memory Heap'이라는 이름의 창고가 있고, 변수나 함수들은 겉에 이름이 라벨지로 붙어있는 박스들인거다.
콜스택은 메모리에 존재하는 공간 중의 하나로, 코드를 읽어내려가면서 수행할 작업들을 밑에서부터 하나씩 쌓고, 메모리 힙에서 작업 수행에 필요한 것들을 찾아서 작업을 수행하는 공간이다.
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
호출 스택이 최대 크기가 되면 “스택 날려 버리기” 가 일어납니다. 이는 반복문 코드를 광범위하게 테스트하지 않고 실행했을 때 자주 발생
Uncaught RangeError : Maximum call stack size exceeded
function foo() {
foo();
}
foo();
호출 스택에 처리 시간이 어마어마하게 오래 걸리는 함수가 있으면 무슨 일이 발생할까요? 예를 들어, 브라우저에서 자바스크립트로 매우 복잡한 이미지 프로세싱 작업을 한다고 합시다.
이게 대체 어때서? 라고 의문이 생길지도 모르지만, 여기서 문제는 호출 스택에서 해당 함수가 실행되는 동안 브라우저는 아무 작업도 못하고 대기 상태가 된다는 겁니다.
이 말뜻은 브라우저는 페이지를 그리지도 못하고, 어느 코드도 실행을 못한다는 거죠. 말 그대로 그냥 가만히 있는 겁니다. 만약 매끄럽고 자연스러운 화면 UI를 가진 앱을 원한다면 위 경우는 문제가 됩니다.
문제는 이뿐만이 아닙니다. 브라우저가 호출 스택의 정말 많은 작업들을 처리하다 보면 화면이 아마 오랫동안 응답하지 않게 됩니다. 이 경우에 대부분의 브라우저가 아래와 같은 에러를 띄우면서 페이지를 종료할 건지 물어봅니다.
이것을 해결책이 비동기 콜백
function getData() {
var tableData;
$.get('https://domain.com/products/1', function(response) { // ajax 통신하는 부분
tableData = response;
});
return tableData;
}
console.log(getData()); // undefined
$.get()
이 ajax 통신을 하는 부분입니다. https://domain.com
에다가 HTTP GET 요청을 날려 1번 상품(product) 정보를 요청하는 코드죠. 좀 더 쉽게 말하면 지정된 URL에 ‘데이터를 하나 보내주세요’ 라는 요청을 날리는 것과 같습니다.response
인자에 담깁니다. 그리고 tableData = response;
코드로 받아온 데이터를 tableData
라는 변수에 저장합니다. 그럼 이제 이 getData()를 호출하면 어떻게 될까요? 받아온 데이터가 뭐든 일단 뭔가 찍혀야겠죠. 근데 결과는 맨 아래에서 보시는 것처럼 undefined입니다. 왜 그럴까요?$.get()
로 데이터를 요청하고 받아올 때까지 기다려주지 않고 다음 코드인 return tableData;
를 실행했기 때문입니다. 따라서, getData()의 결과 값은 초기 값을 설정하지 않은 tableData의 값 undefined를 출력합니다.//위의 코드를 콜백함수를 이용하여 개선하기
function getData(callbackFunc) {
$.get('https://domain.com/products/1', function(response) {
callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
});
}
getData(function(tableData) {
console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});
// #1
console.log('Hello');
// #2
setTimeout(function() {
console.log('Bye');
}, 3000);
// #3
console.log('Hello Again');
비동기 처리에 대한 이해가 없는 상태에서 위 코드를 보면 아마 다음과 같은 결과값이 나올 거라고 생각할 겁니다.
그런데 실제 결과 값은 아래와 같이 나오죠.
setTimeout() 역시 비동기 방식으로 실행되기 때문에 3초를 기다렸다가 다음 코드를 수행하는 것이 아니라 일단 setTimeout()을 실행하고 나서 바로 다음 코드인 console.log('Hello Again');
으로 넘어갔습니다. 따라서, ‘Hello’, ‘Hello Again’를 먼저 출력하고 3초가 지나면 ‘Bye’가 출력됩니다.
콜백 지옥은 비동기 처리 로직을 위해 콜백 함수를 연속해서 사용할 때 발생하는 문제입니다. 아마 아래와 같은 코드를 본 적이 있을 겁니다.
$.get('url', function(response) {
parseValue(response, function(id) {
auth(id, function(result) {
display(result, function(text) {
console.log(text);
});
});
});
});
웹 서비스를 개발하다 보면 서버에서 데이터를 받아와 화면에 표시하기까지 인코딩, 사용자 인증 등을 처리해야 하는 경우가 있습니다. 만약 이 모든 과정을 비동기로 처리해야 한다고 하면 위와 같이 콜백 안에 콜백을 계속 무는 형식으로 코딩을 하게 됩니다. 이러한 코드 구조는 가독성도 떨어지고 로직을 변경하기도 어렵습니다. 이와 같은 코드 구조를 콜백 지옥이라고 합니다.
일반적으로 콜백 지옥을 해결하는 방법에는 Promise나 Async를 사용하는 방법이 있습니다. 만약 코딩 패턴으로만 콜백 지옥을 해결하려면 아래와 같이 각 콜백 함수를 분리해주면 됩니다.
function parseValueDone(id) {
auth(id, authDone);
}
function authDone(result) {
display(result, displayDone);
}
function displayDone(text) {
console.log(text);
}
$.get('url', function(response) {
parseValue(response, parseValueDone);
});
위 코드는 앞의 콜백 지옥 예시를 개선한 코드입니다. 중첩해서 선언했던 콜백 익명 함수를 각각의 함수로 구분하였습니다. 정리된 코드를 간단하게 살펴보겠습니다. 먼저 ajax 통신으로 받은 데이터를 parseValue() 메서드로 파싱 합니다. parseValueDone()에 파싱 한 결과값인 id가 전달되고 auth() 메서드가 실행됩니다. auth() 메서드로 인증을 거치고 나면 콜백 함수 authDone()이 실행됩니다. 인증 결과 값인 result로 display()를 호출하면 마지막으로 displayDone() 메서드가 수행되면서 text가 콘솔에 출력됩니다.
Callback Event Queue에서 하나 씩 꺼내 동작시키는 Loop.
자바스크립트는 단일 스레드 기반 언어이기 때문에 한번에 하나씩 작업을 진행한다.
자바스크립트가 사용되는 환경을 생각해보면 많은 작업이 동시에 처리되고 있는 걸 볼 수 있다. 예를 들면, 웹브라우저는 애니메이션 효과를 보여주면서 마우스 입력을 받아서 처리하고, Node.js기반의 웹서버에서는 동시에 여러 개의 HTTP 요청을 처리하기도 한다. 어떻게 스레드가 하나인데 이런 일이 가능할까? 질문을 바꿔보면 '자바스크립트는 어떻게 동시성(Concurrency)을 지원하는 걸까'?
자바스크립트는 이벤트 루프를 이용해서 비동기 방식으로 동시성을 지원한다.
자바스크립트 엔진에서 제공되는 것이 아닌 브라우저나 node.js에서 지원한다.
자바스크립트가 '단일 스레드' 기반의 언어라는 말은 '자바스크립트 엔진이 단일 호출 스택을 사용한다'는 관점에서만 사실이다.
실제 자바스크립트가 구동되는 환경(브라우저, Node.js등)에서는 주로 여러 개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 자바 스크립트 엔진과 상호 연동하기 위해 사용하는 장치가 바로 '이벤트 루프'인 것이다.
이벤트 루프는 '현재 실행중인 이벤트가 없는지'와 '콜백 큐에 이벤트가 있는지'를 반복적으로 확인한다. 간단하게 정리하면 다음과 같을 것이다.
모든 비동기 API들은 작업이 완료되면 콜백 함수를 콜백 큐에 추가한다.
이벤트 루프는 '현재 실행중인 이벤트(콜백)가 없을 때'(주로 호출 스택이 비워졌을 때) 콜백 큐의 첫 번째 이벤트(콜백)를 꺼내와 실행한다.
Event 실행 관리하기 위해 사용되는 Queue
function delay() {
for (var i = 0; i < 100000; i++);
}
function foo() {
delay();
console.log('foo!');
}
function bar() {
delay();
console.log('bar!');
}
function baz() {
delay();
console.log('baz!');
}
setTimeout(foo, 10);
setTimeout(bar, 10);
setTimeout(baz, 10);
이 코드를 실행하면 아무런 지연 없이 setTimeout
함수가 세 번 호출된 이후에 실행을 마치고 Call Stack이 비워질 것이다.
그리고 10ms가 지나는 순간 foo
, bar
, baz
함수가 순차적으로 태스크 큐에 추가된다. 이벤트 루프는 foo
함수가 태스크 큐에 들어오자 마자, 호출 스택이 비어있으므로 바로 foo
를 실행해서 호출 스택에 추가한다.
foo
함수의 실행이 끝나고 호출 스택이 비워지면 이벤트 루프가 다시 큐에서 다음 콜백인 bar
를 가져와 실행한다.
bar
의 실행이 끝나면 마찬가지로 큐에 남아있는 baz
를 큐에서 가져와 실행한다.
그리고 baz
까지 실행이 모두 완료되면 현재 진행중인 태스크도 없고 태스크 큐도 비어있기 때문에, 이벤트 루프는 새로운 태스크가 태스크 큐에 추가될 때까지 대기하게 된다.