Javascript는 어떻게 작동하는가?
💡 사전 지식
- Parse : 컴퓨터가 이해할 수 있도록 프로그래밍 언어가 데이터를 가공하고 읽기 위해 일정한 포맷으로 변환하는 것. (쉽게 말하면, 사람의 언어 구조 → 컴퓨터의 언어 구조로 변환되는 것이라 봐도 될 것 같다)
- Thread : 프로그램 내에서의 작업 또는 프로세스의 경로
자바스크립트 개요
- JS는 컴파일이 필요 없는 Interpreter 언어이다.
- JS는 변수를 설정할 때 특정 문자열타입을 지정하지 않고 let과 const에 구분없이 담을 수 있다. (동적 타입)
- 이러한 타입 시스템의 부재로 정적타입 언어인 C, C++에 비해 자바스크립트는 효율성이 떨어지고 느리다.
그렇다면 이렇게 비효율적이고 느린 언어를 계속 사용하는가?
이를 이해하기 위해선, 자바스크립트의 역사에 대해 간략히 짚고 넘어가야 한다.
JS의 역사
1990년대의 웹 브라우저들은 매우 정적이고 비상호적인 페이지들을 띄우는 역할만을 담당했었다. 하지만 이후 웹 브라우저에서의 상호작용성을 더하기 위해 1995년 Netscape의 브랜든 아이크(Brendan Eich)에 의해 10일만에 개발된 Javascript가 세상에 공개되었고, 이는 혁신을 일으켰다.
하지만 여러 경쟁사들이 이러한 동적이고 상호적인 브라우저 언어의 개발에 뛰어들었고, 브라우저마다 언어가 달랐기에 호환성 등의 문제가 발발했다. 이에 따라 ECMA Script라는 브라우저 언어에 대한 약속이자 문법을 담은 문서가 개발된 것이다.
그렇게 표준화 되어가는 것 처럼 보이던 ECMA Script에 의한 브라우저 언어의 안정성과 호환성은 마이크로소프트 사의 ECMA 참여 거부로 점점 불안정해지기 시작한다. 그러나, Jesse James라는 개발자가 AJAX를 제안했고, 2008년 생태계 교란종이라고 할 수 있는 강력한 Chrome의 등장으로 위기를 느낀 다른 브라우저들의 협력을 통해 ES5, ES6 등의 문서들이 이후 작성되었다. 드디어 JS는 성숙하고 안정된 언어로 자리잡을 수 있게 된 것이다.
이제 JS가 브라우저에서 어떻게 “실행(Implemented)”되어야 하는지는 ES에 의해 정의됐다.
그렇다면 “어떻게” 작동되어야 하는지는 무엇이 결정하는가? ⇒ JS엔진에 대한 이해가 필요하다.
자바스크립트 엔진
모든 브라우저는 JS코드를 실행하기 위한 JS 엔진을 탑재하고 있다 (Netscape는 SpiderMonkey라는 엔진을, Chrome은 V8엔진을 사용한다). 가장 기초적인 엔진구조(SpiderMonkey)는 JS소스코드를 컴파일 하여 Bytecode로 만드는 baseline compiler → 그리고 이 Bytecode를 머신코드(Binary Code)로 변환하여 최종적으로 CPU에서 실행되게 하는 Interpreter가 존재한다.
하지만 이러한 기초적 엔진구조는 컴파일 시간의 단축에만 집중할 뿐, 코드의 최적화에는 목적을 두지 않는다. 따라서 위와 같은 기초적 엔진구조는 아주 동적이고 인터렉티브한 웹 어플리케이션을 구동하는데 무리가 있다.
이러한 문제점들을 해결하기 위해 구글(Google)은 V8엔진을 사용한다. V8은 Baseline Compiler로써 Full-Codegen을 사용하고, 최적화를 위한 Crankshaft를 사용한다. 기존과 같이 Full-Codegen은 최적화를 신경쓰지 않고 최대한 빨리 Binary Code를 반환한다. 하지만 이러한 과정 중에 소스코드의 최적화를 진행해 최적화된 코드를 Full-Codegen이 반환한 코드의 일부와 대체한다. 즉, 빠른 컴파일과 최적화를 행한다는 것이다.
어떻게 작동되는지에 대한 프로세스는 이제 알겠다. 그렇다면 그 “최적화”라는 것은 어떻게 이루어지는가?
자바스크립트의 최적화
위에서 설명했듯, JS는 타입 시스템이 없고, 더 나아가 프로파일링 데이터(Profiling Data)를 수집하며 느리게 실행되는 코드를 감별하느라 CPU에 부담을 준다. 이에 따라 2017년에 새로 개발된 것이 새로운 V8엔진이다.
새로운 버전의 V8 엔진에는 Ignition이라는 파이프라인이 추가됐다. 이는 **베이스라인의 기능( JS 소스코드 → 바이트 코드로 변환) + 인터프리터의 기능(바이너리 코드로의 변환)**이 이루어지는 파이프라인이다. 그리고 Turbo Fan은 Ignition으로 부터 프로파일링 데이터를 넘겨받아 Hot코드 (CPU에 부담을 주는 코드)를 최적화 할 지, 말 지를 결정한다.
런타임에서의 JS
자바스크립트는 single-threaded 언어다.
즉, 자바스크립트는 코드가 실행될 때 모든 코드가 한 덩어리로써 한번에 실행된다는 것이다. 이러한 싱글 스레드 방식의 문제점은, 실행시간이 오래걸리는 코드가 중간에 껴있으면, 그 후의 코드 실행에 악영향을 미친다는 것이다. 따라서 이러한 블락(또는 무한루프) 문제점이 발생하면 브라우저의 모든 기능들은 중단된다는 문제점을 갖는다. 하지만 다행히도, 이제는 대부분의 브라우저들이 멀티탭 기능들을 탑재하고 있기에, 브라우저별 스레드가 아닌 탭(Tab)별 스레드가 적용되기 때문에 하나의 탭에만 싱글 스레드 문제점이 적용된다.
이렇듯 자바스크립트는 싱글 스레디드 언어이기 때문에 하나의 힙 메모리와 하나의 스택을 갖는다. 스택에서의 코드 실행 방식과 pop 방식은 아래와 같이 LIFO(후입선출)를 따른다.
이와 같은 싱글 스레드 방식만을 따르면, 브라우저는 HTTP 요청을 보내거나 받는 동안 다른 모든 핵심 기능들을 (캐싱, 데이터베이스 스토리지, DOM 이벤트 리스닝 등등) 사용하지 못할 것이다. 이를 해결하기 위해 자바스크립트만의 스레드는 독립적으로 두되, 브라우저 자체 내에서 다른 스레드를 구성하여 DOM 이벤트 리스닝, 캐싱, 데이터베이스 스토리지 등을 자바스크립트와는 독립적으로 기능할 수 있도록 하는 것이다.
이러한 독립적 스레드를 사용하는 브라우저 기능 중에는 Web API 요청을 위한
fetch()
가 있다. 이fetch()
는 자바스크립트 엔진 내의 스레드를 사용하지 않기 때문에 비동기적으로 작동하고, 이러한 비동기 방식 때문에 우리는fetch()
를 사용할 때 콜백함수를 사용하고, async await과 같은 비동기 처리 방식들을 사용하는 것이다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function printHello() {
console.log('Hello from baz');
}
function three() {
setTimeout(printHello, 3000);
}
function two() {
three();
}
function one() {
two();
}
one();Event Loop : 콜스택과 콜백 큐를 주시하며, 콜스택이 비어있고, 콜백 큐에 처리해야 할 스택이 존재한다면 해당 스택을 콜스택으로 옮겨 쌓아 올리는 역할을 한다.
이와 비슷하게
setTimeout()
또한, 콜스택과 메시지큐, 이벤트루프에 의한 독립적 스레드에 의해 효율적으로 작동된다. 먼저 위의 코드를 보면one()
→two()
→three()
순으로 코드가 실행될 것이다. 하지만three()
의setTimeout()
내의 콜백함수인printHello()
는 자바스크립트에 의해 webAPI로 보내질 것이고 자바스크립트 엔진 자체에서 3초를 기다리는 것이 아닌, 자바스크립트 자체는 이후 바로 다음 라인의 코드로 넘어간다. 하지만 다음 라인의 코드가 없기에 자바스크립트는 콜스택에서three
two
one
을 순서대로 pop할 것이다. 하지만 3초 뒤 콜백함수(printHello)는 webAPI에 의해 메시지큐에 담길 것이고, 이벤트루프가 이 콜백함수를 다시 콜스택 위에 올릴 것이다. 그렇게 setTimeout과 그 안의 콜백함수가 실행되는 것이다.
Javascript는 어떻게 작동하는가?
https://hoonjoo-park.github.io/javascript/base/javascript-howItWorks/