사전 자바스크립트 엔진
개념

자바스크립트 엔진

gabury1고친 사람 github-actions[bot]

자바스크립트 엔진은 자바스크립트로 쓴 코드를 읽어 실행합니다. 브라우저와 서버용 실행 환경에 부품으로 들어갑니다. 처음에는 코드를 해석해서 돌립니다. 자주 도는 부분은 기계어로 바꿔 둡니다.

쉽고 빠른 이해

자바스크립트 엔진은 자바스크립트 코드를 받아 실행하는 프로그램입니다. 쇼핑몰에서 장바구니 버튼을 누르면 담긴 개수가 바뀝니다. 그 숫자를 바꾸는 코드를 돌리는 것이 엔진입니다.

웹 페이지는 코드를 글자 그대로 보냅니다. 받은 컴퓨터 안에서 누군가 그 글자를 읽고 실행해야 합니다.

  1. 코드가 문법에 맞는지 읽고 실행하기 알맞은 모양으로 바꿉니다
  2. 바꿔 둔 명령을 하나씩 실행합니다. 그러면서 함수마다 몇 번 불렸는지 셉니다
  3. 자주 불린 함수는 기계어로 바꿔 둡니다. 다음부터는 그것을 씁니다

대가는 바꾸는 일에도 시간과 메모리가 든다는 것입니다. 켠 직후에는 바꿔 둔 것이 없어 제 속도가 안 납니다.

상세

레시피 한 장을 건네받아 요리하는 요리사를 떠올려 봅시다. 요리사는 레시피를 읽고 적힌 순서대로 요리할 줄 압니다. 불과 냄비와 재료는 요리사가 챙겨 오지 않습니다. 일하는 식당이 내어 줍니다.

자바스크립트 엔진이 이 요리사입니다. 레시피는 자바스크립트 코드입니다. 식당은 엔진을 품은 브라우저 같은 프로그램입니다.

코드를 받아 실행하는 프로그램

자바스크립트는 웹 페이지에 움직임을 붙이는 프로그래밍 언어입니다. 장바구니 버튼을 누르면 담긴 개수가 바로 바뀝니다. 그 숫자를 바꾸는 코드가 자바스크립트로 쓰여 있습니다.

웹 페이지는 이 코드를 소스 글자 그대로 보냅니다. 컴파일러로 미리 번역해 둔 실행 파일이 오는 것이 아닙니다. 코드를 받은 컴퓨터 안에서 누군가 그 글자를 읽고 실행해야 합니다.

그 일을 하는 프로그램이 자바스크립트 엔진입니다. 엔진이 없으면 브라우저는 코드를 받아도 글자 뭉치로 들고 있을 뿐입니다. 버튼을 눌러도 아무 일이 일어나지 않습니다.

엔진이 따르는 언어 규칙은 ECMAScript라는 표준이 정합니다. 엔진은 여러 개가 있습니다. 모두 이 표준을 따릅니다. 같은 코드가 어느 브라우저에서나 같은 결과를 내도록 맞춰 둔 것입니다.

엔진이 맡는 일과 호스트 환경이 맡는 일

엔진은 혼자 쓰이지 않습니다. 더 큰 프로그램 안에 부품으로 들어갑니다. 엔진을 품고 엔진에게 실행할 코드를 넘기는 이 프로그램을 호스트 환경이라고 부릅니다.

호스트 환경의 첫째는 브라우저입니다. 둘째는 브라우저 밖 서버에서 자바스크립트를 돌리는 서버용 실행 환경입니다. Node.js가 대표입니다.

엔진과 호스트 환경이 맡는 일은 나뉘어 있습니다. 엔진은 언어 자체만 압니다. 변수를 만들고, 함수를 부르고, 값을 계산하는 일입니다. 화면에 글자를 그리거나 네트워크로 요청을 보내는 기능은 언어에 들어 있지 않습니다. 그런 기능은 호스트 환경이 함수로 만들어 엔진에게 넘겨줍니다.

하는 일 맡는 프로그램 코드에서 보이는 모습
값 계산 · 함수 호출 엔진 a + b · add(1, 2)
안 쓰는 메모리 정리 엔진 코드에 안 보인다
화면의 요소 바꾸기 브라우저 document.querySelector(...)
네트워크 요청 보내기 호스트 환경 브라우저의 fetch(...)
정해 둔 시간 뒤에 함수 부르기(타이머) 호스트 환경 setTimeout(...)
파일 읽고 쓰기 서버용 실행 환경 서버용 실행 환경이 주는 파일 함수

표의 위 두 줄만 엔진의 일입니다. 나머지는 호스트 환경이 붙여 준 것입니다. 호스트 환경이 바뀌면 있던 함수가 없어지기도 합니다. 브라우저에서 늘 쓰던 document 가 서버용 실행 환경에는 없는 것이 이 때문입니다.

자바스크립트 엔진은 화면을 직접 그리지 않습니다. 브라우저 안에는 화면을 그리는 부품이 따로 있습니다. 이 부품이 렌더링 엔진입니다. 코드가 document 로 화면의 요소를 바꾸면 그 일은 렌더링 엔진에게 넘어갑니다.

같은 자바스크립트 엔진이 두 호스트 환경에 들어간 모양은 아래와 같습니다. 엔진은 두 곳에서 같은 부품입니다. 곁에 붙은 함수 묶음만 다릅니다.

flowchart TD
    subgraph B["브라우저"]
        B3["화면 · 네트워크 · 타이머 함수"]
        B2["자바스크립트 엔진"]
        B1["렌더링 엔진 · 화면을 그린다"]
        B3 -->|"엔진에게 넘겨준다"| B2
        B3 -->|"화면 바꾸는 일을 넘긴다"| B1
    end
    subgraph S["서버용 실행 환경"]
        S3["파일 · 네트워크 · 타이머 함수"]
        S2["자바스크립트 엔진"]
        S3 -->|"엔진에게 넘겨준다"| S2
    end

서버용 실행 환경은 대개 브라우저의 엔진 하나를 떼어 와 파일과 네트워크 함수를 붙인 것입니다. 엔진이 처음부터 다른 프로그램에 끼워 넣을 수 있는 부품으로 만들어지기 때문입니다. 그래서 백엔드 개발자가 서버에서 쓰는 자바스크립트도 브라우저와 같은 엔진이 실행합니다.

소스에서 실행까지 거치는 단계

엔진이 받는 것은 글자뿐입니다. 이 글자는 실행되기 전에 두 번 모양을 바꿉니다. 글자가 나무 구조가 되고, 나무가 짧은 명령의 목록이 됩니다. 그 목록을 하나씩 돌리는 것이 실행입니다.

먼저 코드가 문법에 맞는지 읽어 봅니다. 이 일을 하는 부분을 파서라고 합니다. 괄호가 안 닫혔거나 낱말 순서가 틀리면 여기서 문법 오류가 납니다. 그 스크립트는 한 줄도 실행되지 않습니다.

파서는 읽은 코드를 나무 모양의 구조로 바꿔 둡니다. 이 구조가 추상 구문 트리입니다. 글자 줄로는 무엇이 무엇에 걸리는지 안 보입니다. 나무에서는 계산 순서가 모양으로 드러납니다.

a + b * 2 를 나무로 바꾸면 더하기가 뿌리에 섭니다. 그 아래에 a 와 곱하기가 매달립니다. 곱하기 아래에는 b 와 2 가 매달립니다.

flowchart TD
    P["+ 더하기"] --> A["a"]
    P --> M["* 곱하기"]
    M --> B["b"]
    M --> T["2"]

계산은 아래쪽 곱하기부터 합니다. 그 결과가 뿌리의 더하기로 올라가 a 와 더해집니다.

다음으로 대부분의 엔진은 이 나무를 바이트코드로 옮깁니다. 바이트코드는 엔진 안에서만 쓰는 짧은 명령의 목록입니다. 앞의 식은 「a 를 꺼낸다」·「b 를 꺼낸다」·「2 를 꺼낸다」·「곱한다」·「더한다」가 됩니다. 명령 하나가 한 가지 일만 합니다.

이 명령을 하나씩 읽어 가며 실행하는 부분이 인터프리터입니다. 번역을 더 하지 않고 바로 돌리므로 페이지를 연 직후부터 코드가 움직입니다.

처음부터 전부 기계어로 번역하지 않는 데는 이유가 있습니다. 웹 페이지는 받자마자 움직여야 합니다. 받아 온 코드 가운데 상당 부분은 한 번도 안 불리거나 한 번 불리고 끝납니다. 그런 코드까지 번역하느라 사용자를 기다리게 할 까닭이 없습니다.

자주 도는 함수를 기계어로 바꾸기

해석은 시작이 이릅니다. 대신 같은 명령을 지날 때마다 다시 읽어야 합니다. 백만 번 도는 반복문이면 안의 명령을 백만 번 해석합니다.

그래서 엔진은 실행하면서 함수마다 몇 번 불렸는지 세어 둡니다. 자주 불린 함수는 그 컴퓨터가 바로 알아듣는 기계어로 바꿔 둡니다. 다음 호출부터는 바꿔 둔 것을 실행합니다. 실행 도중에 이렇게 번역하는 것을 JIT 컴파일(Just-In-Time compilation, 실행 중 컴파일)이라고 부릅니다. 번역을 맡는 부분은 JIT 컴파일러라고 부릅니다.

자바스크립트에서는 이 번역이 한 가지 이유로 더 까다롭습니다. 변수에 어떤 종류의 값이 들어올지 코드에 적지 않기 때문입니다. 값의 종류가 실행할 때 정해지는 이런 방식을 동적 타이핑이라고 부릅니다. 아래 함수 하나가 두 가지로 쓰입니다.

JavaScript
function add(a, b) { return a + b; }

add(1, 2);      // 3
add("1", "2");  // "12"

같은 + 가 숫자를 받으면 더합니다. 문자열을 받으면 이어 붙입니다. 코드만 봐서는 add 가 어느 용도로 쓰일지 모릅니다. 두 경우를 다 처리하는 기계어는 값의 종류를 따지는 검사가 많아 길어집니다.

엔진은 돌려 본 결과를 써서 이 문제를 넘깁니다. 지금까지 add 에 숫자만 들어왔다면 「숫자만 온다」고 가정하고 숫자 더하기만 하는 기계어를 만듭니다. 실행하면서 이런 정보를 모으는 일이 프로파일링입니다.

이렇게 가정을 깔고 번역본을 짧고 빠르게 만드는 일을 최적화라고 합니다. 그 결과물이 최적화된 번역본입니다. 검사가 적어 같은 일을 더 빨리 끝냅니다.

가정은 깨질 수 있습니다. 숫자만 받던 add 에 어느 날 문자열이 들어옵니다. 엔진은 이런 때를 대비해 최적화된 번역본 앞에 가정을 확인하는 짧은 검사를 붙여 둡니다. 검사에 걸리면 번역본을 버리고 인터프리터로 돌아갑니다. 최적화를 이렇게 되돌리는 일이 탈최적화입니다.

한 함수에 늘 같은 종류의 값을 넘기는 코드는 번역본을 계속 씁니다. 넘기는 값의 종류가 자주 바뀌는 코드는 번역과 되돌림을 되풀이합니다.

지금까지의 단계를 한 장에 놓으면 아래와 같습니다. 아래로 내려가는 줄기가 코드가 거치는 변환입니다. 위로 되돌아가는 화살표가 탈최적화입니다.

flowchart TD
    A["자바스크립트 소스"] --> P["파서"]
    P --> T["추상 구문 트리"]
    T --> BC["바이트코드"]
    BC --> I["인터프리터가 하나씩 실행한다"]
    I -->|"자주 불린 함수"| J["JIT 컴파일러"]
    J --> M["기계어 번역본으로 실행한다"]
    M -->|"가정이 깨졌다"| I

메모리를 알아서 치우는 일

자바스크립트 코드는 객체를 만들기만 하고 지우는 명령을 쓰지 않습니다. { name: "kim" } 을 만들어 쓰고 나면 그냥 잊습니다. 그 뒤처리는 엔진이 맡습니다.

엔진은 만든 객체를 힙이라는 메모리 구역에 둡니다. 힙은 프로그램이 실행 도중에 필요한 만큼 떼어 쓰는 메모리입니다. 객체가 몇 개 생길지 미리 알 수 없으니 이렇게 그때그때 떼어 줍니다.

엔진은 때때로 힙을 훑어 어디에서도 가리키지 않는 객체를 찾습니다. 아무도 가리키지 않는 객체는 다시 쓰일 길이 없습니다. 엔진은 그 메모리를 돌려받습니다. 이 일이 가비지 컬렉션입니다.

이 일에도 시간이 듭니다. 치우는 동안 코드 실행이 잠깐 멈추기도 합니다.

엔진은 가리키는 곳이 남은 객체를 치우지 않습니다. 버린 줄 알았던 객체가 캐시로 쓰려고 만든 배열에 계속 걸려 있으면 메모리가 줄지 않고 쌓입니다. 이것이 메모리 누수입니다.

아래 그림에서 코드가 쥔 변수로부터 화살표를 따라 닿는 객체는 남습니다. 닿지 않는 객체 C 만 치워집니다. 객체 D 는 더 쓰지 않는데도 배열이 가리키고 있어 남습니다.

flowchart TD
    V["코드가 쥔 변수"]
    subgraph H["힙"]
        A["객체 A"]
        B["객체 B"]
        L["캐시로 쓰려고 만든 배열"]
        D["객체 D · 더 안 쓰지만 남는다"]
        C["객체 C · 아무도 안 가리킨다 · 치운다"]
    end
    V --> A
    A --> B
    V --> L
    L --> D

한 번에 한 함수씩 실행하는 방식

엔진은 한 페이지의 코드를 하나의 실행 흐름으로만 돌립니다. 운영체제가 따로따로 돌려 주는 실행 흐름 하나를 스레드라고 부릅니다. 함수 하나가 도는 동안 다른 함수는 끼어들지 못합니다.

함수가 다른 함수를 부르면 엔진은 어디까지 했는지를 쌓아 둡니다. 불린 함수가 끝나면 맨 위의 것을 꺼내 원래 함수로 돌아옵니다. 이렇게 쌓아 두는 구조를 콜 스택이라고 부릅니다. 오류가 났을 때 함수 이름이 줄줄이 찍히는 목록이 이 콜 스택을 적은 것입니다.

버튼 클릭이나 네트워크 응답은 아무 때나 도착합니다. 코드는 이런 일이 생기면 부를 함수를 미리 걸어 둡니다. 일이 도착하면 호스트 환경은 거기 걸어 둔 함수를 줄 세워 둡니다.

엔진의 콜 스택이 비면 호스트 환경은 줄의 맨 앞 함수를 엔진에게 넘깁니다. 줄 세운 함수를 이렇게 하나씩 넘기는 고리가 이벤트 루프입니다. 이벤트 루프는 엔진이 아니라 호스트 환경이 돌립니다.

flowchart TD
    subgraph H["호스트 환경"]
        Q["줄 · 클릭 · 네트워크 응답 · 타이머에 걸어 둔 함수"]
        L["이벤트 루프"]
        Q --> L
    end
    subgraph E["자바스크립트 엔진"]
        S["콜 스택 · 지금 도는 함수가 맨 위에 쌓인다"]
    end
    L -->|"콜 스택이 비면 하나 넘긴다"| S

이 방식의 대가는 오래 걸리는 계산 하나가 줄 선 일을 전부 세운다는 것입니다. 브라우저에서는 그동안 화면이 굳고 버튼이 안 눌립니다. 서버용 실행 환경에서는 그동안 들어온 요청이 전부 기다립니다.

오래 걸리는 계산을 따로 돌리려면 호스트 환경이 엔진의 실행 흐름을 하나 더 띄웁니다. 브라우저에서는 이것을 웹 워커라고 부릅니다. 두 흐름은 변수를 나눠 쓰지 않고 메시지를 주고받습니다.

남이 보낸 코드를 돌리는 프로그램

브라우저의 엔진은 처음 가 본 웹사이트가 보낸 코드도 돌립니다. 누가 썼는지 모르는 코드입니다. 이 코드가 사용자의 파일이나 다른 사이트의 데이터에 손대지 못하게 막아야 합니다.

첫 울타리는 언어 자체입니다. 자바스크립트에는 메모리 주소를 직접 다루는 기능이 없습니다. 파일을 여는 함수도 브라우저가 넘겨주지 않습니다. 코드가 쓸 수 있는 것은 엔진과 호스트 환경이 허락한 함수뿐입니다.

엔진 자체에 버그가 있으면 이 울타리가 뚫릴 수 있습니다. 엔진은 크고 복잡한 프로그램이라 이런 버그가 나옵니다. 그래서 브라우저는 엔진을 권한을 줄인 별도 프로세스 안에서 돌립니다. 이렇게 가둬 두고 돌리는 방식을 샌드박스라고 부릅니다.

한 걸음 더 나가면 사이트마다 프로세스를 따로 씁니다. 한 사이트의 코드가 엔진의 버그를 파고들어도, 그 프로세스의 메모리에는 그 사이트의 데이터밖에 없습니다. 이 방식을 사이트 격리라고 부릅니다.

같은 울타리는 자바스크립트가 아닌 코드에도 걸립니다. C 나 Rust 같은 언어로 쓴 프로그램을 브라우저에서 돌리려고 만든 이진 형식이 WebAssembly입니다. 이 형식의 코드도 자바스크립트 엔진이 받아 실행하는 경우가 많습니다. 그때는 자바스크립트와 같은 울타리 안에서 돕니다.

엔진마다 다른 속사정

브라우저를 만드는 회사마다 자기 엔진을 따로 만들어 싣습니다. 표준이 정하는 것은 코드가 내야 할 결과까지입니다. 번역을 언제 얼마나 공들여 하는지, 메모리를 언제 치우는지는 엔진마다 다릅니다. 그래서 같은 코드라도 엔진에 따라 빠르기와 메모리 쓰임이 달라집니다.

관련 항목

자바스크립트 엔진이 코드를 실행하며 거치는 단계

파서 · 추상 구문 트리 · 바이트코드 · 인터프리터 · JIT 컴파일 · 기계어 · 프로파일링 · 탈최적화 · 인라인 캐시 · 히든 클래스

자바스크립트 엔진이 관리하는 메모리와 실행 구조

힙 · 스택 · 콜 스택 · 가비지 컬렉션 · 메모리 누수 · 클로저 · 실행 컨텍스트 · 스코프

자바스크립트 엔진을 품는 호스트 환경

호스트 환경 · 브라우저 · Node.js · Deno · Bun · 런타임 · 웹 워커 · 서비스 워커

호스트 환경이 자바스크립트 엔진에 붙여 주는 기능

이벤트 루프 · DOM · 웹 API · 타이머 · 비동기 프로그래밍 · Promise · 콜백

자바스크립트 엔진을 구현한 제품

V8 · SpiderMonkey · JavaScriptCore · Chakra · Hermes · QuickJS

자바스크립트 엔진이 받아 실행하는 언어와 형식

JavaScript · ECMAScript · WebAssembly · 동적 타이핑 · 스크립트 언어

자바스크립트 엔진에 넘기기 전에 코드를 바꾸는 도구

트랜스파일러 · TypeScript · 번들러 · Babel · 압축기

자바스크립트 엔진을 가둬 두는 보안 장치

샌드박스 · 사이트 격리 · 렌더러 프로세스 · 프로세스 · 동일 출처 정책 · Spectre

브라우저 안에서 자바스크립트 엔진 곁에 서는 부품

렌더링 엔진 · 렌더링 · CSSOM · HTML

자바스크립트 엔진과 같은 일을 다른 언어에서 맡는 실행기

JVM · CLR · CPython · 가상 머신 · 컴파일러 · AOT 컴파일

다른 이름: JavaScript engine · JavaScript 엔진 · JS 엔진 · 자바스크립트 실행 엔진