javascript 동작 원리

namezin·2020년 5월 20일
16

Front-End

목록 보기
14/21

Javascript가 어떻게 해석되고 동작되는지 알아보고자 한다.

개요

자바스크립트 엔진, 런타임, Call stack, Heap 등 엔진단에서 자바스크립트 동작원리를 알고 있어야 고급 개발자로 될 수 있다고 생각한다.

자바스크립트는 프론트 단에서는 유일한 프로그램 언어이기 때문에 반드시 이해하고 사용할 줄 알아야한다. 또한 백엔드 단에서는 node.js와 같은 자바스크립트 기반 언어 또한 많이 사용되기 때문에 엔진단을 이해한다면 보다 나은 설계와 개발을 할 수 있을 것이다. 무엇보다 자바스크립트로 할 수 있는 영역보다 할 수 없는 영역을 찾는게 더 빠른 현시대에는 필수적으로 알아야 할 부분이다.

  1. 자바스크립트 엔진
  2. V8
  3. 런타임
  4. Memory Heap and Call Stack

1. 자바스크립트 엔진

자바스크립트 엔진의 대표적인 예는 Google V8 엔진이다. V8은 Chrome과 node.js에서 사용한다.

엔진의 2개 구성요소는

  • Memory Heap : 메모리 할당이 일어나는 곳
  • Call Stack : 호출 스택이 쌓이는 곳

2. V8

구글이 주도하여 C++로 작성된 고성능의 자바스크립트 & 웹 어셈블리 언어이다. ECMAScript와 Web Assembly 표준에 맞게 구현하였다.

작동원리


1. 자바스크립트 소스코드를 가져와 Parser에게 넘긴다.
2. Parser는 파싱을 통해 AST(Abstract Syntax Tree)로 변환시킨다.
3. AST를 인터프리터를 통해 byte code로 변환(Ignition)한다.
4. 그리고 bytecode를 실행함으로써 실제 작동하게 된다.
5. 그 중 자주 사용되는 코드는 TruboFan으로 보내진다.
6. TruboFan은 이 코드를 Optimized Machine Code로 컴파일해놓고 사용한다.

재밌는 얘기

  • V8은 8기통 엔진을 뜻한다.
  • Ignition은 엔진 시동시에 사용되는 점화기이다.
  • TurboFan은 너무 과호출되는 부분을 식혀주는 Fan이다.
  • 다 의미를 노리고 네이밍한 것임을 알 수 있다.

Ignition

AST를 해석하고 최적화하는 인터프리터

컴파일러 대비 이점
1. 메모리 사용량 감소 : 기계어로 컴파일하는 것보다 bytecode로 컴파일 하는 것이 메모리 사용량이 적다.
2. 파싱 시 오버헤드 감소 : bytecode는 기계어보다 간결하기 때문에 파싱하기에 이점이 있다.
3. 컴파일 파이프 라인의 복잡성 감소 : optimizing, deoptimizing 이든 bytecode 하나만 생각하면 되기 때문에 복잡성이 컴파일러에 비해 낮다.

Ross Mcllroy Ignition - an interpreter for V8

TurboFan

최적화 담당 컴파일러

V8 런타임 중에 Profiler 에게 함수나 변수들의 호출 빈도와 같은 데이터를 모우도록 시킨다.
이렇게 모인 데이터는 TurboFan에 의해 몇 가지 기준을 통해서 최적화를 진행한다.
아래는 대표적 최적화 방법 2개를 설명한다.

  1. Hidden Class(히든클래스) : 비슷한 것들끼리 분류해놓고 가져다 쓰는 것
  2. Inline Caching(인라이캐싱) : 아래 철학을 통해 접근하고자하는 필드 오프셋을 캐싱하여 사용
    • 동적인 언어라고 해봤자 실제로는 안바뀌는게 더 많다
    • 성능을 빠르게 하려면 딴 거 다 필요없고 루프를 노려라

자바스크립트 최적화방법은 추후 post에서 자세하게 다루기로 하자.

3. 런타임

Web APIs

브라우저에서 제공하는 APIs.
자바스크립트(JavaScript) 코드를 사용하여 접근할 수 있으며 window나 element에 대한 간단한 작업에서부터 WebGL이나 Web Audio와 같은 API를 사용해 복잡한 그래픽 및 오디오 효과를 만들어내는 것까지 가능하다.
예를 들어, setTimeout 등이 있다.

Event Loop

Callback Event Queue에서 하나 씩 꺼내 동작시키는 Loop.
자바스크립트는 단일 스레드 기반 언어이기 때문에 한번에 하나씩 작업을 진행한다. 그러나 자바스크립트가 사용되는 환경을 생각해보면 많은 작업이 동시에 처리되고 있는 걸 볼 수 있다. 예를 들면, 웹브라우저는 애니메이션 효과를 보여주면서 마우스 입력을 받아서 처리하고, Node.js기반의 웹서버에서는 동시에 여러 개의 HTTP 요청을 처리하기도 한다. 어떻게 스레드가 하나인데 이런 일이 가능할까? 질문을 바꿔보면 '자바스크립트는 어떻게 동시성(Concurrency)을 지원하는 걸까'?

자바스크립트는 이벤트 루프를 이용해서 비동기 방식으로 동시성을 지원한다.
자바스크립트 엔진에서 제공되는 것이 아닌 브라우저나 node.js에서 지원한다.

Node.js는 비동기 IO를 지원하기 위해 libuv 라이브러리를 사용하며, 이 libuv가 이벤트 루프를 제공한다.

자바스크립트가 '단일 스레드' 기반의 언어라는 말은 '자바스크립트 엔진이 단일 호출 스택을 사용한다'는 관점에서만 사실이다.
실제 자바스크립트가 구동되는 환경(브라우저, Node.js등)에서는 주로 여러 개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 자바 스크립트 엔진과 상호 연동하기 위해 사용하는 장치가 바로 '이벤트 루프'인 것이다.

이벤트 루프는 '현재 실행중인 이벤트가 없는지'와 '콜백 큐에 이벤트가 있는지'를 반복적으로 확인한다. 간단하게 정리하면 다음과 같을 것이다.

모든 비동기 API들은 작업이 완료되면 콜백 함수를 콜백 큐에 추가한다.
이벤트 루프는 '현재 실행중인 이벤트(콜백)가 없을 때'(주로 호출 스택이 비워졌을 때) 콜백 큐의 첫 번째 이벤트(콜백)를 꺼내와 실행한다.

Callback Queue(Task Queue)

Event 실행 관리하기 위해 사용되는 Queue

  • 자바스크립트의 런타임 환경의 Callback Queue는 처리할 메세지 목록과 실행할 콜백 함수들의 리스트이다. 버튼 클릭 같은 이벤트나 DOM 이벤트, http 요청, setTimeout 같은 비동기 함수는 Web API를 호출하며 Web API는 콜백 함수를 Callback Queue에 밀어 넣는다.
  • Callback queue는 대기하다가 Call Stack이 비는 시점에 이벤트 루프를 돌려 해당 콜백 함수를 스택에 넣는다. 이벤트 루프의 기본 역할은 큐와 스택 두 부분을 지켜보고 있다가 스택이 비는 시점에 콜백을 실행시켜 주는 것이다.
  • 브라우저에서는 이벤트가 발생할 때마다 메세지가 추가되고 이벤트 리스너가 첨부된다. 콜백 함수의 호출은 호출 스택의 초기 프레임으로 사용되며 자바스크립트가 싱글 스레드 이므로 스택에 대한 모든 호출이 반환될 때까지 메세지 폴링 및 처리가 중지 된다. 동기식 함수 호출은 이와 반대로 새 호출 프레임을 스택에 추가한다.
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까지 실행이 모두 완료되면 현재 진행중인 태스크도 없고 태스크 큐도 비어있기 때문에, 이벤트 루프는 새로운 태스크가 태스크 큐에 추가될 때까지 대기하게 된다.

4. Memory Heap and Call Stack

자바스크립트 엔진이 구동되면서 코드를 읽고 실행하는 과정에서 아주 중요한 두가지 단계가 있는데,
1. 정보(ex. 변수, 함수 등)를 특정한 장소에 저장하는 것과
2. 실제 현재 실행되고 있는 코드를 트래킹하는 작업이 그 두가지이다.

여기서 정보를 저장하는 공간(Memory Allocation이 발생하는 공간)이 바로 메모리 힙(Memory Heap)이고, 실행 중인 코드를 트래킹하는 공간이 콜 스택(Call Stack)이다.

Memory Heap

메모리 생존주기는 프로그래밍 언어와 관계없이 비슷하다.

  1. 필요할때 할당한다. (allocate)
  2. 사용한다. (읽기, 쓰기) (use)
  3. 필요없어지면 해제한다. (release)

두 번째 부분은 모든 언어에서 명시적으로 사용된다. 그러나 첫 번째 부분과 마지막 부분은 저수준 언어에서는 명시적이며, 자바스크립트와 같은 대부분의 고수준 언어에서는 암묵적으로 작동한다.

메모리 힙은 변수, 함수 저장, 호출 등의 작업이 발생하여 위 내용들이 진행되는 공간이다.
쉽게 생각하면 'Memory Heap'이라는 이름의 창고가 있고, 변수나 함수들은 겉에 이름이 라벨지로 붙어있는 박스들인거다.

Call Stack

콜스택은 메모리에 존재하는 공간 중의 하나로, 코드를 읽어내려가면서 수행할 작업들을 밑에서부터 하나씩 쌓고, 메모리 힙에서 작업 수행에 필요한 것들을 찾아서 작업을 수행하는 공간이다.

stack overflow

누구나 개발을 하다보면 stack overflow 에러는 보기마련이다. 여기서의 stack이 바로 call stack이다. stack에 한정된 자원을 넘어서 계속해서 쌓이다 보면 stack이 넘치게 되는데 그걸 stack overflow라고 한다. 개발자 필수사이트 이름이 여기서 따왔다.

Garbage Collector(GC)

콜스택과 메모리 힙을 배우면서 각각의 공간은 무제한이 아니고 한정적임을 알 수 있다.
공간이 무한정 하다면야 크게 효율성에 대해서 고려하지 않을 수도 있지만, 콜스택과 메모리 힙은 언제나 한정적이기 때문에 이를 효율적으로 관리할 필요가 있다.

자바스크립트는 이 공간을 효율적으로 관리하기 위해서, 더 이상 효용가치가 없다고 판단되는 변수, 함수 등을 함수 실행 종료 후 메모리 힙에서 제거하는 동작을 수행한다.
필요한 데이터만 메모리 힙에 저장함으로써 메모리를 더욱 여유롭게 관리한다. 따라서 자바스크립트는 Garbage Collected Language라고 말할 수 있으며, 이러한 역할을 수행해주는 도구를 Garbage Collector라고 한다.

하지만 이는 자칫 자바스크립트 개발자들에게 잘못된 인상을 심어줄 수도 있다. '스스로 메모리 관리를 해주니까 내가 따로 메모리를 신경쓸 필요는 없지않을까?' 라고 생각할 수 있지만, 언제나 그렇듯 이 또한 프로그램에 지나지 않기 때문에 완벽하지는 않다.

그럼 이 Garbage Collector는 어떠한 원리로 작동하는 것일까?

표시하고-쓸기(Mark-and-sweep) 알고리즘
이 알고리즘은 "더 이상 필요없는 오브젝트"를 "닿을 수 없는 오브젝트"로 정의한다.
이 알고리즘은 roots 라는 오브젝트의 집합을 가지고 있다(자바스크립트에서는 전역 변수들을 의미한다). 주기적으로 가비지 콜렉터는 roots로 부터 시작하여 roots가 참조하는 오브젝트들, roots가 참조하는 오브젝트가 참조하는 오브젝트들... 을 닿을 수 있는 오브젝트라고 표시한다. 그리고 닿을 수 있는 오브젝트가 아닌 닿을 수 없는 오브젝트에 대해 가비지 콜렉션을 수행한다.

2012년 기준으로 모든 최신 브라우저들은 가비지 콜렉션에서 표시하고-쓸기 알고리즘을 사용한다. 지난 몇 년간 연구된 자바스크립트 가비지 콜렉션 알고리즘의 개선들은 모두 이 알고리즘에 대한 것이다. 개선된 알고리즘도 여전히 "더 이상 필요없는 오브젝트"를 "닿을 수 없는 오브젝트"로 정의하고 있다.

Reference

https://joshua1988.github.io/web-development/translation/javascript/how-js-works-inside-engine/
https://evan-moon.github.io/2019/06/28/v8-analysis/
https://meetup.toast.com/posts/78
https://developer.mozilla.org/ko/docs/Web/%EC%B0%B8%EC%A1%B0/API
https://velog.io/@imacoolgirlyo/JS-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%97%94%EC%A7%84-Event-Loop-Event-Queue-Call-Stack#3-event-queue
https://developer.mozilla.org/ko/docs/Web/JavaScript/Memory_Management

profile
개발관심자

1개의 댓글

comment-user-thumbnail
2021년 5월 13일

대단하십니다!!

답글 달기