HyunJun 기술 블로그

JavaScript Engine & JavaScript Runtime 본문

JavaScript

JavaScript Engine & JavaScript Runtime

공부 좋아 2023. 6. 23. 09:13
728x90
반응형

1. JS Engine & JS Runtime 

  • JS Engine: JavaScript를 해석 및 동작 시키기 위한 엔진이다. 대표적으로 Chrome의 V8 엔진이 있다.
    • 구문을 분석하고, 변수를 저장 및 할당하는 등 자바스크립트 전반적인 해석 및 구동을 담당한다.
    • 예를 들면 Python의 공식 interpreter, Java의 공식 JDK가 있다.
  • JS Runtime: JavaScript를 특정 목적에 맞게 실행시키기 위한 실행 환경이다.
    • JavaScript는 태초에 웹페이지(HTML, CSS)를 동적으로 컨트롤하고자 생긴 언어이므로, JS Runtime이라고 부르면 기본적으로 웹브라우저를 기반으로 한 런타임이 맞다.
    • 하지만 이를 서버용으로 활용하고자 하는 시도들이 나왔고, JS의 핵심인 V8 엔진을 떼어와 Runtime의 구성요소를 조금씩 수정해서 서버용으로 사용한 것이 Node.js Runtime이다.
    • 즉 JS 내에서도 JS Runtime(크롬, 파이어폭스, ..), NodeJS 등 여러 가지 목적에 따른 Runtime 환경이 있다
    • 자바스크립트는 동기 방식의 싱글 스레드 언어이면서 논 블로킹 언어이다. 즉, 이 뜻은 콜 스택을 하나만 가지고 있다는 뜻이다.
      • 콜 스택을 하나만 가지고 있기 때문에, 기본적으로 한 번에 두 개 이상의 코드를 처리하지 못한다.

 

1) 브라우저 환경을 위한 JS Runtime VS 서버 환경을 위한 Node.js Runtime

Chrome V8 엔진은 자바스크립트의 실행을 처리하는 엔진으로, 브라우저 환경과 Node.js 환경에서 모두 사용된다. 하지만 브라우저 환경과 Node.js 환경 간에는 몇 가지 중요한 차이점이 있다.

 

  1. 실행 환경과 API 차이: 브라우저 환경은 웹 페이지를 렌더링하고 DOM을 조작하는 등의 웹 관련 작업을 수행하는 API를 제공한다. 반면에 Node.js 환경은 파일 시스템 액세스, 네트워킹, 서버 개발 등 서버 측 작업을 위한 API를 제공한다. 따라서 브라우저 환경과 Node.js 환경에서는 각각 다른 API를 사용하여 작업을 처리해야 한다. 
  2. 전역 객체: 브라우저 환경에서는 window라는 전역 객체가 제공되며, DOM 요소에 접근하고 브라우저 관련 기능을 사용할 수 있다. 반면에 Node.js 환경에서는 global이라는 전역 객체가 제공되며, 파일 시스템 접근 및 모듈 로딩과 같은 기능을 사용할 수 있다.
  3. 모듈 시스템: 브라우저 환경에서는 주로 ES6 모듈 시스템을 사용하여 모듈을 로드하고 내보낼 수 있다. Node.js 환경에서는 CommonJS 모듈 시스템을 주로 사용한다. 브라우저 환경에서는 번들러를 통해 모듈을 번들링하고 사용하는 것이 일반적이다.
  4. 네트워킹: 브라우저 환경에서는 웹 소켓 및 AJAX와 같은 네트워크 관련 API를 사용하여 서버와 통신할 수 있다. Node.js 환경에서는 내장된 http, https, net 등의 모듈을 사용하여 서버 및 클라이언트 사이의 통신을 처리할 수 있다.
  5. 프로세스 환경: Node.js 환경은 서버 측에서 동작하며, 프로세스 간 통신 및 다중 프로세싱과 관련된 기능을 제공한다. 브라우저 환경에서는 이러한 기능이 제한적이거나 없을 수 있다.
  6. 보안 및 접근 권한: 브라우저 환경에서는 보안 제약 사항이 더욱 엄격할 수 있으며, 사용자의 동의 없이는 일부 작업을 수행할 수 없다. Node.js 환경에서는 보안 제약이 상대적으로 덜 엄격하며, 파일 시스템 액세스와 같은 작업을 수행할 수 있다. 

요약하면, Chrome V8 엔진은 브라우저 환경과 Node.js 환경에서 모두 사용되지만, 각 환경마다 제공하는 API, 전역 객체, 모듈 시스템, 네트워킹 및 보안 관련 기능 등에 차이가 있다. 개발 시에는 해당 환경에 맞는 API와 패턴을 사용하여 작업을 처리해야 한다.

 

 

2. JavaScript Runtime의 4가지 요소

현재 점유율이 제일 높고, Node.js의 기반인 Chrome의 V8 엔진을 기준으로 브라우저 환경을 위한 JS Runtime을 설명하려고 한다.

  • Javascript Engine
  • WEB API
  • Callback Queue
  • Event Loop

 

1) Javascript Engine

JSConf EU 2017에서 발표한 Franziska Hinkelmann님의 자료

자바스크립트 엔진(이 글에서는 V8 엔진을 기준으로 함)은 Call Stack, Memory Heap뿐만 아니라, 실제로는 Lexer(Tokenizer), Parser, Interpreter, JIT 컴파일러, Call Stack, Memory Heap, ... 등 많은 기술들이 엮여 있다. 이는 브라우저의 엔진마다 다를 수 있다.

  • Lexer(Tokenizer): 소스 코드를 토큰(Token) 단위로 분해하는 역할을 한다.
  • Parser: 토큰들을 받아서 문법 구조를 분석하고, AST(Abstract Syntax Tree)로 변환한다.
  • Interpreter(실행기): AST를 기반으로 코드를 실행한다.
  • Call Stack(호출 스택): 함수 호출의 추적과 관리를 위한 스택 자료구조이다. 코드를 실행하는 곳이고, 런타임 동안 호출 스택에서 호출하고 코드를 한 줄씩 실행하는 Global Execution Context가 있다. 이것은 C, Java의 main과 비슷한 개념이다. 원시 타입(Primitive Type)이 저장되는 곳
  • Memory Heap(메모리 힙): 동적으로 할당된 메모리를 관리하는 영역으로서, 객체, 변수, 함수 등의 데이터가 저장된다. 가능할 때마다 메모리 공간을 확보하는 데 사용하는 가비지 컬렉터가 있다.
    • 참조 타입(Referenct Type) 즉, 객체(Array, Object, Function) 등이 메모리힙에 할당된다. 사용이 끝나면 Garbage Collector에 의해 자동으로 해제(Garbage collection) 된다.

 

 

자바스크립트 엔진에는 크롬(V8), 파이어폭스(스파이더몽키), 사파리(웹킷) 등의 엔진이 있다. Javascript Engine에서 코드를 읽는 Interpreter가 해당 코드를 읽고 콜 스택에 쌓는다. 읽힌 코드는 Call Stack에 쌓이게 되고, Call Stack 안에 있는 변수들은 스코프에 따라 Memory Heap에 할당되게 된다. 이후에 Call Stack에서 해당 컨텍스트가 제거되면(스코프가 끝나면) 적정 알고리즘에 따라 Garbage Collector에 의해 자동으로 해제(Garbage collection) 된다. 즉, 자바스크립트 엔진은 .js 파일을 읽으면서 Call Stack을 채우고, Call Stack에 있는 작업들을 수행하는 것을 반복한다.

 

2) WEB API & Background

WebAPI와 Background의 차이점은 아래와 같다.

 

  • WebAPI: 자바스크립트 WebAPI는 웹 브라우저에서 제공하는 여러 가지 기능적인 인터페이스들을 나타낸다. 이들 API는 웹 페이지에서 동작하는 기능을 확장하거나 조작할 수 있는 다양한 방법을 제공한다. 예를 들어, DOM 조작, Console API, Promise, eventListener, HTTP 요청(Ajax), 타이머 관리(setTimeout, setInterval), 클라이언트 측 스토리지(localStorage, sessionStorage), 미디어 재생 및 제어, 그래픽 처리(Canvas), 웹 워커(Web Worker) 등이 이에 해당한다. 이러한 WebAPI들을 사용하여 웹 페이지를 보다 동적이고 상호작용적으로 만들 수 있다.
  • Background: Background는 웹 브라우저의 주요 스레드인 메인 스레드(main thread)와는 별개로 실행되는 공간을 의미한다. 웹 브라우저에서 실행되는 자바스크립트 코드는 기본적으로 메인 스레드에서 실행되지만, 일부 작업은 백그라운드에서 병렬로 처리될 수 있다. 이것은 주로 시간이 오래 걸리는 작업이나 동시성이 필요한 작업에 적용된다. 대표적인 예로 웹 워커(Web Worker)가 있다. 웹 워커는 메인 스레드와 별개의 스레드에서 실행되며, 긴 작업을 처리하거나 병렬적으로 작업을 수행하는 데 사용된다. 이렇게 하면 웹 페이지의 성능이 향상되고 더 나은 사용자 경험을 제공할 수 있다.

요약하면, 자바스크립트 WebAPI는 웹 브라우저에서 제공하는 여러 기능 인터페이스를 나타내며, 백그라운드는 메인 스레드와 별개로 실행되는 자바스크립트 코드의 공간을 의미한다. 백그라운드에서 실행되는 코드를 활용하여 웹 페이지의 성능을 최적화하고 병렬 작업을 처리할 수 있습니다.

 

3) 그렇다면, 싱글 스레드인 JS가 어떻게 비동기 방식을 구현할까?

JavaScript를 구동하는 엔진인 자바스크립트 엔진(V8)은 Call Stack, Memory Heap, ...으로 구성되어 있다. 이 엔진을 브라우저에서 활용하면 브라우저 런타임으로 사용하는 것이고, 따로 떼와서 외부에서 활용한 것이 Node.js가 되는데 여기까지만 봤을 때 분명 자바스크립트는 싱글 스레드가 맞다.

 

하지만 그것은 엔진만 봤을 때이고, 실제로 자바스크립트가 실행되는 런타임 환경(브라우저 Runtime, Node.js Runtime)에서 보자면 멀티 쓰레딩이 가능한 환경에서 실행된다. 그래서 결국 Background라는 공간도 별도의 스레드가 돌아가는 것이고, WebAPI 등을 사용해서 비동기 처리가 가능한 것이다.

 

즉, JavaScript Runtime을 하나의 프로세스로 보면, 비동기를 위한 스레드도 1개가 있기에 스레드가 2개라고 볼 수 있다, 하지만 우리가 보통 자바스크립트를 설명할 때에는 자바스크립트 언어 자체, 즉 엔진을 기준으로 설명을 하는 것이기 때문에, 자바스크립트 자체 엔진의 측면에서 봤을 때는 자바스크립트 코드는 기본적으로 1개의 콜 스택, 1개의 스레드로 돌아가기 때문에 싱글 스레드 언어라고 불리는 것이다.

 

 

여기서 브라우저 환경을 위한 JavaScript는 WEB API를 사용할 때, 싱글 스레드(동기 방식)에서 비동기 방식의 작업들을 구현하게 된다. 하지만 모든 Web API들이 비동기로 동작하는 것은 아니다. DOM API, Console API 등은 동기적으로 처리된다.

 

  • 예를 들어, JS Engine에서 코드를 수행하다가 WebAPI 중 하나인 setTimeout을 사용하게 되면
  • setTimeout은 Call Stack에 올라가서 실행되고 곧바로 WebAPI를 사용하여 브라우저 자바스크립트 엔진과 타이머 시스템에서 처리되며, 설정한 시간만큼 대기하게 된다.
  • 이때 호출한 해당 API는 Call Stack에서 사라지게 되고, 다음 코드의 내용이 진행되게 된다. (비동기 방식)
  • setTimeout의 설정한 시간이 지나면 setTimeout의 콜백 함수가 Callback Queue로 이동하게 되고
  • Event Loop가 Call Stack이 비게 되면(Global Execution Context 조차 사라지게 되면),
  • Callback Queue에 있는 콜백 함수를 Call Stack으로 옮겨 해당 내용이 실행되게 된다.

 

결론적으로 자바스크립트는 단일 스레드로 동작하기 때문에, 이벤트 기반의 비동기 처리를 위해 이벤트 루프와 콜백 큐를 사용한다. 비동기 함수들은 콜백 큐에 들어가서 이벤트 루프에 의해 실행된다. console.log와 같은 동기 함수는 일반적으로 콜백 큐에 들어가지 않는다. 이러한 함수는 현재 실행 중인 스레드에서 직접 실행되며, 콜백 큐에 대기하는 것이 아니라 결과를 즉시 출력한다.

 

4) Callback Queue

  • Callback Queue에는, Task Queue, Microtask Queue이 존재한다.
    • Task Queue: setTimeout, setInterval, fetch, addEventListener와 같이 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐이다.
    • Microtask Queue: promise.than, process.nextTick, MutationObserver와 같이 우선적으로 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐이다. (처리의 우선순위가 높다.)
  • 비동기 작업들이 실행할 요소들을 담는 공간이다. (setTimeout 예제 확인)

 

5) Event Loop

  • CallBack Queue와 Call Stack을 감시하며 CallBack Queue에 실행할 함수가 담겨있고, Call Stack이 비어있다면 CallBack Queue에 있는 함수를 순차적으로 Call Stack으로 넘겨주는 역할을 한다.

 

3. 동기 / 비동기

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

기본적으로 완전한 동기 방식이라면 중간에 3초를 기다리더라도, 1, 2, 3이 출력되어야 한다.

  • Call Stack에 log(1)이 쌓이게 되고 곧바로 콘솔 창에 출력한 후 Call Stack에서 제거된다.
  • Call Stack에 setTimeout이 쌓이게 되고, 곧바로 Web API에 의해 3초 카운트를 시작한다. (Call Stack에서는 제거된다.)
  • Call Stack에 log(3)이 쌓이게 되고 곧바로 콘솔 창에 출력한 후 Call Stack에서 제거된다.
  • setTimeout의 3초 카운트가 끝나면 실행할 내용, 즉 콜백 함수를 Callback Queue로 이동한다.
  • Event Loop는 Call Stack이 비어있는 것을 확인하면 Callback Queue에 있는 setTimeout의 콜백 함수를 Call Stack에 옮기게 되고 Call Stack에 의해 실행하게 된다.

setTimeout의 시간을 0으로 바꿔도, 결국 setTimeout의 콜백 함수는 Call Stack이 비어야 실행되기 때문에 위의 결과와 같다.

 

 

즉 Call Stack에 시간이 오래 걸리는 작업이 실행되고 있다면 해당 작업이 끝나기 전에는 (Call Stack에서 사라지기 전에는) setTimeout의 Callback Function이 실행되지 않으므로 setTime의 정확도에 문제가 생길 수 있다. 그러므로 시간이 오래 걸리는 코드는 스택에 쌓지 않는 것이 좋다.

 

예를 들어, 아래와 같은 코드는 setTimeout을 1초로 설정해 놨지만, for 자체가 1초 이상 걸리기 때문에 1초가 넘어도 CallBack Queue에 대기하고 있다가 for문이 끝나고 CallStack이 비게 되면 Event Loop에 의해 Call Stack으로 옮겨저 실행되게 된다. 

   for (let i = 0; i < 200000; i++) {
        console.log(i);
      }

      setTimeout(() => {
        console.log("!!!!!");
      }, 1000);

 

이 글에 이어서 아래의 실행 컨텍스트 글을 참조하면 자바스크립트의 원리에 대해서 파악하는데 많은 도움이 될 것이다.

728x90
반응형
Comments