웹 브라우저에서의 JavaScript 코드 실행되기 까지 [JavaScript Engine]

Dengo·2023년 3월 17일
0

WEB

목록 보기
2/3
post-thumbnail

HTML Parsing

웹 브라우저는 렌더링을 하기 위해서 서버로부터 받아온 HTML을 렌더링 엔진이 파싱하는 것에서 부터 시작합니다.
모두 아시다시피, 우리가 작성하는 자바스크립트 코드는 이 HTML안에 <script>태그를 통하여 불러오는데요.
웹 브라우저와 자바스크립트 코드와의 첫 만남은 바로 이 순간입니다.

<script>태그에 defer와 같은 속성을 사용하는게 아닌이상, 웹 브라우저는 제어 권한을 렌더링 엔진에서 자바스크립트 엔진으로 넘깁니다.
즉, 자바스크립트 코드 실행이 종료될 때 까지 HTML 파싱은 중단됩니다.

따라서 아래와 같은 코드는 동작할 수 없습니다.

설명한 바와 같이 자바스크립트 코드가 실행되는 시점에서는 <div id="root">Hi</div>는 DOM트리에 포함되어있지 않습니다. 따라서 웹 페이지에는 "HI"만 나와있죠.

이러한 이유 때문에 <script>태그는 HTML파싱이 모두 끝났을 시점인 body태그 마지막에 포함되어야 DOM트리를 온전히 사용할 수 있습니다.

하지만 보통의 경우 가져오게 될 js파일은 상당히 무거울텐데 HTML파싱이 끝난 이후에 가져오고 실행하려면 속도가 늦는 문제가 있어 defer라는 속성이 지원되고 있습니다.

해당 글의 주제는 아니라서 자세히는 다루지 않겠으나 참고로 위의 코드에서는 defer를 적용해도 Hello World가 출력되지 않습니다. 이유는 defer는 외부 자원(src속성을 통한)에 대해서만 유효하기 때문입니다.

어쨌든 본격적으로 자바스크립트 코드 실행에 대해서 알아보겠습니다

JavaScript Code Parsing

웹 브라우저의 제어 권한이 자바스크립트 엔진에게 주어지고 코드를 가져올 것 입니다.
이 코드는 아직 까지는 사람이 이해하기 위해 작성된 코드입니다.
이것을 당연히 컴퓨터가 이해할 수 있게 바꾸어 주어야 하는데요.

이를 위해서 자바스크립트 엔진에 있는 Lexer가 어휘 분석을 통해서 토큰화를 진행하고
Parser가 구문해석과 의미해석을 하면서 추상 구문 트리(Abstract Syntax Tree, AST)를 만들게 됩니다.

자바스크립트에서는 호이스팅(hoisting)이라고 하는 동작이 있습니다.
변수 선언이나 함수 선언을 코드 실행 이전에 끌어올리는 동작을 의미하는데, 호이스팅이 AST를 만들때 일어나게 됩니다.

만든 AST를 가지고 본격적으로 코드를 실행할 수 있습니다.

Interpreter


이제 우리가 작성한 코드는 트리가 되었습니다.
이 AST를 이용해서 엔진에 내장된 인터프리터가 코드를 실행하는데요.
자바스크립트 언어는 인터프리터 언어이기 때문에 코드를 한줄씩 실행합니다.

인터프리터는 실행할 코드를 바이트 코드로 생성합니다.
(바이트 코드란 하드웨어가 아닌 소프트웨어가 해석하기 위한 기계어보다 추상적인 코드를 의미합니다)
그리고 일단 이 바이트 코드를 실행합니다.

느리다!

인터프리터 컴파일 방식의 한계

이렇게 만들어진 바이트 코드를 실행시키는 것은 느리다는 문제가 있습니다.
인터프리터 방식의 고질적인 문제입니다.
한 줄씩 바로바로 실행하기 때문에 실행 속도가 빠르지만,
같은 코드를 두 번 이상 실행하게 되면 점점 비효율적이게 되는 것입니다.

웹 브라우저를 만드는 사람들은 현대 웹 개발이 발전함에 따라서 반복해서 수행되는 코드에 대해서는 최적화가 되길 원했습니다.
최적화를 위해서 인터프리터와 더불어 JITC(Just In Time Compilation)이라는 컴파일 방식을 도입하게 됩니다.

인터프리터는 한줄의 코드에 대해서 바이트 코드를 실행한다면
JITC는 한줄의 코드에 대해서 만들어진 바이트 코드를 기계어로 컴파일을 하고 실행하는 컴파일 방식입니다.
이렇게 한줄의 코드가 실행되는 시점에 컴파일이 되기 때문에 동적 언어인 자바스크립트에서도 문제없이 동작할 수 있습니다.

JITC가 무조건 더 좋을까?

인터프리터 방식보다 JITC가 아무래도 조금 더 최적화가 된 것은 맞지만,
그렇다고 극적인 최적화가 이루어진 것은 아닙니다.
C언어와 같은 정적 컴파일 언어의 경우 코드 전체를 읽고 전체 코드에 대한 최적화 알고리즘이 적용되어 더욱 최적화된 목적 파일을 실행할 수 있는 것을 생각하면 JITC에서 지원하는 최적화는 그다지 극적이진 않다는 것을 알 수 있습니다.

또한 자바스크립트가 동적언어라는 점에서 JITC의 방식은 오히려 걸림돌이 될 수도 있는데요.
예를 들어 자바스크립트에서 덧셈의 경우 int + int, string + string, int + string과 같은 케이스를 전부 실행할 수 있어야 하는데 이런 케이스를 전부 기계어로 뽑아낸다면 단순한 덧셈을 위해 엄청나게 많은 양의 기계어를 필요로 하게 될 것 입니다.

Optimizing Compiler

이 문제를 해결하기 위해,
즉 최적화된 기계어 코드를 얻기 위해 최근 JavaScript 엔진에서는 Adaptive JITC 방식을 사용합니다.

이 컴파일 방식을 요약하자면 모든 코드에 대해서 같은 수준의 최적화를 하는 것이 아니라 반복 수행되는 정도에 따라 유동적으로 최적화를 적용하는 방식입니다.

다시 말하면 일단은 interpreter로 바이트 코드를 하나씩 읽어서 실행합니다.
실행되는 동안에 코드 실행 시간, 메모리 사용량 등을 분석하여 프로파일링 데이터를 얻어 이것을 기준으로 자주 반복되는 코드(hotspot)를 JITC로 컴파일 하여 최적화된 기계어를 만듭니다.

프로파일링 데이터를 얻는 부분을 Profiler라고 부르고 최적화를 진행하는 부분을 최적화 컴파일러라고 부릅니다.

V8엔진에서는 어떤 모습일까?


Google Chrome 브라우저의 자바스크립트 엔진을 V8엔진이라고 부릅니다.
V8엔진은 사진과 같이 자동차에 실제로 들어가는 엔진의 이름에서 따온것을 알 수 있는데요
이러한 발상에서 시작되어 V8엔진의 인터프리터는 Ignition(점화)라고 부르고 최적화 컴파일러는 Turbo Fan이라고 불립니다.

위에서 잠깐 언급했듯이 '자주 반복되는 코드'를 hotspot이라고 부르는데
이러한 hotspot을 일단 만드는게 Ignition(인터프리터)가 마치 뜨거운 것을 만들어내는 모습이고
이것을 식히기 위해 (최적화) Turbo Fan(최적화 컴파일러)을 이용한다는 컨셉을 생각한 것 같습니다.
(참고로 V8의 최적화 컴파일러는 과거 FullCodegen, CrankShaft 두번의 최적화 컴파일러를 거쳐 지금의 Turbo Fan을 사용하고 있는데요, 과거 자료에서 V8의 최적화 컴파일러로 CrankShaft를 소개하는 경우도 있으니 참고하시길 바라겠습니다.)

V8엔진의 모습만 소개를 했으나, 사실 웹 브라우저마다 가지고 있는 자바스크립트 엔진의 종류는 다양하고 이에 따라 엔진의 내부 모습이 상이합니다. 이 역시 참고를 하시고 다른 엔진의 모습 또한 궁금하다면 추가로 조사해보는 것이 좋겠습니다

실행, 그 이후에 대해서

여기까지가 말그대로 자바스크립트 코드를 실행하는 것에 대해서 알아보았습니다.
실행하는 코드에는 여러가지가 있겠죠.
DOM 조작을 한다던지, 어떠한 비동기 작업을 한다던지 다양한 작업이 있을 것 입니다.

이번 주제에서는 '자바스크립트 엔진'에 대해서 중점적으로 다루었지만 이후 과정을 이해하기 위해서는 자바스크립트 엔진의 Heap과 CallStack, 웹 브라우저의 렌더링 엔진, event loop 등등 더 많은 것을 알아야 합니다.

차근 차근 알아보고 정리하여 포스팅 해보겠습니다
긴 글 읽어주셔서 감사하고 지적사항이나 추가사항은 댓글로 얼마든지 말씀해주시면 감사드리겠습니다!!
🙇‍♂️

참고한 글들

https://hwan-shell.tistory.com/343
https://juunone.netlify.app/javascript/jitc/
https://meetup.nhncloud.com/posts/77
https://wooncloud.tistory.com/129
https://velog.io/@godori/JavaScript-engine-1
🥹

profile
Software Engineer (전산쟁이)

0개의 댓글