티스토리 뷰

1. Javascript의 탄생 

자바스크립트는 1995년 약 90%의 시장 점유율로 웹 브라우저 시장을 지배하고 있던 Netscape communications는 웹페이지의 보조적인 기능을 수행하기 위해 브라우저에서 동작하는 경량 프로그래밍 언어를 도입하기로 결정했다.

그래서 탄성한 것이 바로 Brendan Eich가 개발한 자바스크립트이다.

하지만 Netscape의 경쟁사인 Microsoft에서 인터넷 익스플로러에 쓰이는 자바스크립트와 유사한 JScript라는 언어를 인터넷 익스플로러 3.0에 탑재하엿는데, JScript와 Javascript가 표준화 되지 못하고 적당히 호환되었기 때문에, 브라우저에 따라 웹페이지가 정상적으로 동작하지 않는 문제가 발생하면서 결과적으로 모든 브라우저에서 정상적으로 동작하는 웹페이지 개발이 어려워지게 된다.

이에 따라 모든 브라우저에서 정상적으로 동작하는 표준화된 자바스크립트의 필요성이 대두되면서 1996년 11월, netscape는 비영리 표준화기구인 ECMA international에 자바스크립트 표준화를 요청하게 된다. 그래서 현재까지 표준화 규정을 관리해 오고 있고, 현재까지 2020년, ES11까지 버전이 출시되었다.

 

여기서 다루고 싶은것은 어떻게 Javascript 코드는 컴퓨터에게 인식되느냐는 것이다.

즉, 우리가 작성한 소스코드를 컴퓨터가 이해하는 machine code로 변환되는 작업이 어떻게 이루어 지는지 한번 살펴 보자. 그리고 자바스크립트가 왜 single threaded language인지, interpreted language에 대해서도 알아보자.

 

2. Javascript Engine

먼저 Javascript는 아래 그림과 같이 javascript engine에 의해서 machine code로 변환된다. 컴퓨터는 javascript가 뭔지 모른다. 우리가 작성한 소스코드를 컴퓨터가 이해할수 있는 machine code로 변환시켜 주는 것이 javascript engine이라보면 된다.

Javascript Engine 

https://en.wikipedia.org/wiki/List_of_ECMAScript_engines

 

List of ECMAScript engines - Wikipedia

An ECMAScript engine is a program that executes source code written in a version of the ECMAScript language standard, for example, JavaScript. These are new generation ECMAScript engines for web browsers, all implementing just-in-time compilation (JIT) or

en.wikipedia.org

위의 javascript engine의 종류에서 볼수 있다시피, 정말 많은 engine이 있다.

그중에서 현재 가장 많이 쓰이고 있는 것은 Google 2008년에 개발되어 사용되고 있는 엔진이 V8 엔진이다.

V8엔진은 크롬과 node.js에서 사용되는 엔진이다. 

최초의 자바스크립트 엔진은 자바스크립트를 만든 Brandan eich가 만든 spider monkey로 알려진 초기 버전의 엔진을 만들었고 이는 firefox 브라우저가 여전히 사용중이다. 이 엔진을 통하여 브라우저에서 자바스크립트가 돌아갈수 있게 해주었는데 이는 느렸다. 그래서 구글이 2008년, 구글 맵스를 구현하기 위해서 빠르게 브라우저에서 동작하는, c++언어를 사용하여 v8 엔진을 만든 것이다. 

그러면 지금 우리가 널리 사용하는, 구글에서 개발된 V8엔진은 어떤 원리로 소스코드를 머신코드로 변환하는 것일까

 

1) Inside Javscript Engine

Javascript engine

우리가 소스코를 작성하면 먼저 Parser에 의해서 lexical anaysis가 일어난다. 코드를 token으로 부수고 키워드에 따라 텍스트가 어떻게 나뉘어 지는지를 파악해 AST(Abstract Syntax Tree)를 형성한다. 

이에 대해서 조금더 설명하자면, 소스코드를 단어와 숫자같은 원시적인 타입으로 각 키워드를 공백 기준으로 토큰이라 불리는 것으로 분리한다. 

그리고 분리된 토큰들 사이의 관계를 찾는 parsing이란 과정이 일어나고 분석한 문법에 따라 소스 코드 구조를 계층적으로 표시하는 abstract syntax tree를 형성하게 된다.

이렇게 형성된 AST는 Interpreter로 지시를 하기 위해 CPU가 이해하도록 코드를 보낸다.

그럼 이 AST를 통해 넘겨받은 data를 interpreter는 어떻게 machine code로 전환하는가.

 

2) Interpreter과 Compiler

Interpreter and Compiler

먼저 소스코드를 machine code로 변환하는 방법에는 Interpreter와 compiler를 사용하는 두 가지 방식이 있다.

Interpreter와 Compiler의 차이점에 대해서 알아보자.

컴파일러 언어 인터프리터 언어
코드가 실행되기 전 단계인 컴파일 타임에 소스코드 전체를 한번에 머신코드로 변환 후 실행 코드가 실행되는 단계인 런타임에 한줄씩 중간코드인 bytecode로 전환 후 실행.
인터프리터와 다르게 빠르게, 단숨에 번역하지 않음. (번역속도 느림) 한줄씩 빠르게 읽기 때문에 빠르게 번역. (번역 속도 빠름)
최적화 O (실행속도 빠름) 최적화 X (실행속도 느림)
실행파일 생성 실행파일 생성 X

위와같이 소스코드를 한줄씩 바로 번역하기 때문에 interpreting 속도가 빠르다. 그래서 자바스크립트와 같은 언어에 알맞다.

서버로부터 js 파일을 브라우저로 보내고, client의 front end로 보낼때, 화면간의 interaction이 바로 빨리 실행되어야 하므로, 가능한 빨리 번역해 실행하는것이 이상적이였다. 이것이 자바스크립트가 처음에 인터프리터를 사용하는 이유이다.

하지만 Google Maps에서 google이 겪은 문제와 동일한데, 자바스크립트를 실행하지만 점점 느려진다.

왜냐하면 인터프리터의 문제는 같은 코드를 한번이상 실행할때, 예를들어 for문이 돌면 같은 결과값을 도출함에도 불구하고 같은 code를 for문이 끝날때 까지 돌린다. 이는 점점 느려지는 문제를 만들며 이를 해결하기 위해 compiler가 도움을 줄 수 있다.

Compiler는 비록 처음 시작할때 Compilation step을 거치기 때문에 시간이 조금 더 걸린다. 하지만 compiler는 반복되는 코드가 보이면 이 패턴을 simplify 시킨다. 예를들어 function을 여러번 부르는 대신 'q'라고 대체해서 부르는 것이다.

따러서 compiler를 통해서 만들어진 코드는 더 빠르다. 이러한 compiler가 하는 편집을 Optimization(최적화) 라고 한다.

여기서 Interpreter와 compiler의 장점을 합쳐놓은 JIT(Just In Time) compiler가 등장한다.

v8 엔진의 작동 원리는 다음과 같다 보면 된다.

먼저 AST를 통한 코드가 interpreter에 의해서 한줄한줄 실행되며 bytecode로 번역된다. 이를 ignition이라고 한다.

그리고 bytecode로 한줄한줄 번역되고 있는 와중에, monitor라고도 불리는 profiler가 실행되는 코드를 보고 반복되는 패턴을 어떻게 최적화 시킬지를 기록한다. 그리고 만약 같은 라인의 코드들이 여러번 실행된다면, 이 코드의 일부를 compiler또는 JIT compiler에게 넘겨준다. 그리고 이 JIT 컴파일러가 코드를 받아 컴파일 하거나 변형시켜 최적회 시켜 개선될 bytecode의 section을 최적화된 machine code로 대체한다.

따라서 최적화된 코드가 그 시점보다 더 느린 bytecode대신에 사용됨으로서 자바스크립트 코드 실행속도가 점차적으로 개선되는 것이다.

여기서 V8 엔진에서 optimization 시켜주는 컴파일러를 Turbo Fan이라고 한다.

 

그럼 이러한 엔진이 돌아가는 구조를 왜 알아야 할까?

그에 대한 해답은 

① 더 최적화된 코드를 작성할 수 있다.

② 컴파일러는 완전하지 못하고 가끔 더 시간이 걸리게 하는 deoptimization의 실수를 하기도 하는데, 컴파일러가 혼동을 하지 않기 위해서 구조를 알아야 한다.

 

 

3. 최적화된 코드 작성법

https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments

 

GitHub - petkaantonov/bluebird: Bluebird is a full featured promise library with unmatched performance.

:bird: :zap: Bluebird is a full featured promise library with unmatched performance. - GitHub - petkaantonov/bluebird: Bluebird is a full featured promise library with unmatched performance.

github.com

https://richardartoul.github.io/jekyll/update/2015/04/26/hidden-classes.html

 

Javascript Hidden Classes and Inline Caching in V8

Hidden Classes Javascript is a dynamic programming language which means that properties can easily be added or removed from an object after its instantiation. For example, in the code snippet below an object is instantiated with the properties “make” a

richardartoul.github.io

1) 주의해야할 function과 keyword

자바스크립트 엔진을 위해서 

다음과 같은 funciton과 키워드는 사용을 할때 매우 매우 신중해야 한다.

eval()

arguments

for in

with

delete

이에 대한 내용은 위 게시글을 참고 하면된다.

2) 최적화 방법 - Inline caching

function findUser(user){
    return `found ${user.firstName} ${user.lastName}`
}

const userData = {
    firstName: 'Johnson',
    lastName: 'Junior'
}

findUser(userData)

위의 코드에서, compiler의 inline caching 때문에, 같은 method를 반복적으로 실행하는 code, 예를들어 findUser(userData)를 여러번 반복 실행하게 된다면, 이를 컴파일러가 최적화 시킨다.

userData를 찾을때마다 Inline caching을 사용하여 매번 객체의 key와 value를 찾기 보다 findUser(userData)가 'found Johnson Junior'로 바로 대체된다.

 

3) 최적화 방법 - Hidden Classes

function Animal(x,y){
    this.x = x;
    this.y = y;
}

const obj1 = new Animal(1,2);
const obj2 = new Animal(3,4);

obj1.a = 30;
obj1.b = 100;

obj2.b = 30;
obj2.a = 100;

위와같이 Animal이라는 클래스를 생성할때, 컴파일러가 내부에서 obj1과 obj2는 같은 hidden class를 가진다고 생각한다. 하지만 다른 순서로 property를 추가하면 공유된 hidden class가 없고 분리된 것이라고 여긴다.

따라서 이를 Hidden class를 통해 최적화를 시키기 위해서는 a,b 프로퍼티 추가시 같은 순서로 추가 하거나, 클래스 생성시 a,b를 추가적으로 명시하면 된다.

이와 같은 이유로, delete keyword를 통해서 property를 지우면 hidden class를 지우기 때문에 더이상 매치 되지 않기 때문에 위에서 delete 키워드를 사용할때 매우 신중해야 한다고 하는 것이다.

 

4. Call Stack와 Memory Heap

자바스크립트 엔진이 많은 일을 하지만 가장 큰 일은 code를 읽고 실행하는 것이다.

따라서 ①정보를 저장하고 쓰는 공간(변수, 객체 저장 등등..) ②우리의 코드에서 한줄 한줄에서 무슨 일이 일어나고 있는지를 추적하고 실행하는 공간이 필요하다. 

첫번째 공간이 메모리 할당이 일어나는 Memory Heap이고, 두번째 공간이 엔진이 코드가 어느 실행 상태에 있는지 계속 추적하는 Call Stack이다.

 

1) Memory Heap

const number = 610;
const string = 'some text' 
const human = {
    first: 'Hwang',
    last: 'Junho'
}

위 코드에서와 같이, 메모리에 number, string. human variable을 할당하고 memory heap의  각 value를 가리킨다.

이것을 js 엔진한테 말하는 것이다.

Memory heap은 자바스크립트 엔진이 제공하는 memory에서의 큰 부분이고, 어떠한 타입의 데이터를 순서 없이 어떠한 방식으로도 저장 가능하다.

 

2) Call Stack

function subtractTwo(num){
    return num -2 ;
}

function calculate() {
    const sumTotal = 4 + 5;
    return substactTwo(sumTotal);
}
debugger;
calculate()

위 코드를 크롬 브라우저에서 debugger를 통해 확인 해보면, calculate() 함수가 실행될때 오른쪽 call stack에 anonymous, calculate, subtractTwo 순으로 쌓였다 역순으로 사라지면서 calculate()가 실행되는것을 알 수 있을 것이다.

call stack은 memory에서 'First in last out' 모드로 작동하는 공간으로, 현재의 실행 위치가 어딘지를 알려주는 역할을 한다. (anonymous는 global execution context로 나중에 다룰것이다.)

 

정리하자면, 내 코드를 실행할때 Memory heap에 내 함수와 변수를 저장하고 stack의 상태인 stack frame이 코드가 어디에 있는지를 알려 준다. 

자바스크립트 엔진 implementation이 각기 다르기 때문에 변수들이 어디에 할당되어 있는지는 완전히 동일하진 않지만, 일반적으로 simple variable은 stack에 저장되고 복잡한 자료구조나 배열, function은 memory heap에 저장되며 call stack의 맨 위가 자바스크립트가 실행되고 있는곳이다.

 

5. Stack Overflow

위의 call stack에서 계속해서 function이 쌓이면 Stack Overflow가 일어난다.

function inception(){
    inception()
}

inception()

자기 자신의 함수를 계속 부르는 것을 recursion이라고 하는데, recursion이 stack overflow를 만드는데 가장 일반적인 방법이다.

위의 코드를 실행하면 brower가 crash되거나 crashing을 막기 위해서 에러메세지를 출력한다.

 

6. Garbage Collection

자바스크립트는 garbage collected 언어이다.

자바스크립트에서 function calling이 끝나고 객체가 더 이상 필요 없어 졌을때 이를 지운다. 자바스크립트가 더 이상 사용하지 않는 메모리를 비워 memory leak을 막는다. 하지만 필요 없는 객체를 메모리에서 제거 하지 않을 수도 있다. 완벽한 시스템은 아니다.

자바스크립트에서 GC는 Mark & Sweep 알고리즙에 의해서 아래와 같이 point 되는 객체가 아니라면 제거하는 원리로 작동이 된다.

Mark & Sweep algorithms

7. Memory Leak

Stack Overflow가 call stack에서 stack이 계속 쌓여 생기는 문제였다면, Memory Heap가 GC에 의해 정리되지 않고 계속 정보가 생성되 한도를 초과하면 Memory leak이 발생한다.

let array = [];
for (let i = 0; i > 1; i ++) {
    array.push(i-1);
}

위 코드에서 볼수 있듯, array에 끝없이 정보를 추가하기때문에 위 코드를 브라우저에서 실행하면 사이트가 crash되는것을 확인할 수 있다. GC또한 array를 계속 사용하기 때문에 지우지 못하기에 그렇다.

Memory leak는 application이 과거에 사용한 메모리지만 더이상 사용하지 않는 메모리를 free memory의 pool로 되돌아 오지 못한것을 말한다.

 

아래에는 세 가지의 일반적으로 일어나는 memory leak 이다.

1) Global Variable. 전역 변수를 사용할때.

var a = 1;
var b = 1;
var c = 1;

위와 같이 전역변수로 계속하여 변수를 추가하면 메모리를 계속 추가하는 것이다.

 

2) Event Listeners 사용

var element = document.getElementById('button');
element.addEventListener('click', onclick)

 

위와 같이 이벤트 리스너를 필요하지 않을때 제거하지 않고 계속 추가하기만 한다면 background에 계속 실행되고 있으므로 memory leak이 발생한다. single page application에서 앞 뒤로 페이지를 움직이면서 특히 많이 발생한다. 사용자가 왔다 갔다 하면서 다른 event listener가 추가되면서 계속 쌓이면서 memory leak이 발생할 수 있다.

 

3) setInterval 사용.

setInterval(()=>{
    // referencing objects...
    /*
    여기의 object들은 절대 gc에 의해서 collected되지 않는다. 왜냐하면
    setInterval은 clear하지 않는한 계속 돌아가기 때문이다.
    */
})

setInterval() 함수또한 마찬가지로 GC에 의해 사라지지 않고 계속 돌아가기 때문에 memory leak이 발생할 수 있다.

 

8. Single Threaded

자바스크립트는 Single Threaded programming language이다.

무슨말이냐하면, 하나의 instruction만 한번에 실행되는 것이다. 여러개를 한번에 처리할 수 없다.

언어가 single threaded인지 확인하는 방법은 call stack이 하나인지 여러개인지 확인하면된다.

하나의 call stack은 한번에 하나의 코드를 실행하기 때문에 여러개의 function을 평행하게 동시에 실행하지 못한다.

따라서 자바스크립트는 Synchronous 하다. 

하지만 Synchronous code의 문제점이 있는데, 자바스크립트 엔진이 작동하면, single threaded synchronous code는 긴 running test가 있을 경우 실행이 어려울 것이다. 시간이 오래걸리는 작업 하나가 있다면 그 작업이 끝날때 까지 다른 작업을 할수 없을 것이다. 이에 대한 classic한 예가 alert()함수 이다. 함수가 끝날때 까지 다른 것은 아무것도 작동하지 않는다.

call stack이 빌때까지 아무것도 할수 없으면 왜 자바스크립트를 사용하는가? 페이지를 느리게 하는 이것을 왜 사용하는가에 대한 답은, 자바스크립트는 엔진만 사용하는 것이 아닌 Asynchronous 코드 또한 사용한다. 자바스크립트 엔진을 넘어의 것인 자바스크립트 런타임(Javascript Runtime)이라는 것이 필요하다.

 

9. Javascript Runtime

Javascript Runtime

자바스크립트에서는 위에서의 문제에서 같이, 시간이 오래걸리는 작업을 동시에 Asynchronous하게 처리할 수 있는 방법이 필요한데, 그방법이 자바스크립트 런타임을 통해서 처리할 수 있다.

웹 브라우저가 백그라운드에서 synchronous 자바스크립트 코드가 실행되는 동안에 작동되는데, Web API를 통해서 브라우저에서 처리할 수 있는 기능을 자바스크립트에서 불러 올 수 있다.

모든 브라우저는 자바스크립트 엔진 implementation이 있고, 모두 Web API를 제공하는 자바스크립트 런타임이 있다.

Web API는 http request 보내기, DOM event listen 과 같은 여러가지 일을 할 수 있으며, setTimeout()을 통한 실행을 delay시키거나, caching이나 브라우저가 제공하는 작은 데이터베이스 까지 사용할 수 있다.

window객체, DOM객체 모두 브라우저가 제공하는 web api이다.

브라우저가 뒤에서 c++같은 low level language를 사용하여 이러한 기능들을 백그라운드에서 수행할 수 있도록 한다.

이런 Web API는 asynchronous한데, API가 백그라운드에서 작업한 데이터를 call back을 통해 리턴하도록 지시할 수 있다. 

브라우저가 백그라운드에서 Web API를 통해 작업을 하는 동안에, 자바스크립트는 다른 작업을 처리할 수 있게 되는 것이다.

원리는 call stack에서 setTimeout()과 같은 함수가 나오면 Web API에게 처리하라고 넘기고 Web API가 인지해 백그라운드에서 작업한다. 그리고 그 결과값을 call back Queue에 넘겨주면, event loop가 Callback Queue에 있는 값을 자바스크립트 파일이 모두 실행되고 call stack이 비었을 경우 넘겨 준다.

console.log("1");
setTimeout(()=>{console.log{"2"},1000});
console.log("3");

예를들어 위의 코드를 실행하면 결과는 1, 3이 출력되고 2가 출력되는데, 이는 setTimeout을 1000가 아닌 0으로 처리해도 결과는 똑같다.

call stack이 setTimeout()을 보는 순간 Web API로 넘기게 되고, 자바스크립트 파일이 모두 실행이 되고 1, 3을 call stack에서 처리하고 난 다음, 백그라운드에서 Web API를 통해 처리된 값이 callback Queue에 넘어온 값을 event loop가 call stack이 비어 있는 것을 확인 한 후 넘겨 주는 것이다.

이를 통해서 브라우저에서 asynchronous code를 사용 가능해 진다.

 

Reference: 

Udemy의 Javascript: The Advanced Concepts(2021)를 통해 정리 / 자료 사용

https://www.udemy.com/course/advanced-javascript-concepts/

모던 자바스크립트 Deep Dive (위키북스) 

https://ui.toast.com/weekly-pick/ko_20161107

https://gyujincho.github.io/2018-06-19/AST-for-JS-devlopers

최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday