JS가 "선 컴파일 후 실행" 프로그램인 증거

FAST FOX·2025년 7월 30일

About. JavaScript

목록 보기
6/10
post-thumbnail

1. 개요

JS의 컴파일를 공부하다가 JS가 선컴파일 후 실행 접근 방식을 취하는 언어이며 이를 입증할 수 있는 세 가지 특징으로 구문 오류, 초기 오류, 호이스팅이 있다는 내용을 보았다.
호이스팅은 저번에 공부를 해서 변수나 함수가 선언되기 전에 호출되면 자동으로 호출 전에 변수 선언 및 함수 선언문은 함수 자체를 메모리에 할당되는 현상이라고 알고 있었다.
하지만 구문 오류와 초기 오류는 컴파일 과정에서 잡을 수 있는 오류라고만 알고 있었고 정확한 정의를 알지 못했는데 이번 기회에 한 번 알아보자.

2. 구문 오류

구문 오류란 무엇일까?
밑에 예시를 보자

let greeting = "안녕하세요";

console.log(greeting);

greeting = ."안녕";

만약 JS가 코드가 한 줄씩 실행되는 인터프리터 언어라고 생각하면 문제가 없는 console.log(greeting)이 정상적으로 작동하여 "안녕하세요"가 출력될거라고 생각할 것이다.
하지만 해당 코드를 실행하면 기대와 달리 곧바로 Uncaught SyntaxError: Unexpected token '.'라는 오류가 발생하게 된다.
이러한 오류는 코드를 토큰으로 변경하는 렉싱과 정에서 발생한다. 연산자인 = 다음에 .이 오는건 문법상 올바른 토큰이 될 수 없기 때문에 구문 오류를 발생시킨다.

그렇기 때문에 이러한 방식으로 구문 오류가 발생하는 것은 JS는 프로그램을 실행하기 전에 프로그램을 파싱한다는 것을 알 수 있다.

3. 초기 오류

console.log("잘 지내시죠?");

saySomething("안녕하세요.", "안녕!");
// Uncaught SyntaxError: Duplicate parameter name not allowed in this context

function saySomething (greeting, greeting) {
  "use strict";
  console.log(greeting);
}

위의 코드를 실행시키면 Uncaught SyntaxError: Duplicate parameter name not allowed in this context라는 오류가 발생하게 된다.

ES5 이전 strict 모드가 없었을 때는 오류가 발생하지 않고 뒤에 있는 greeting이 덮어쓰면서 "잘 지내시죠?"와 "안녕!"이 순차적으로 출력됐었지만 지금은 스코프, 변수 이름 충돌, 오류 추적을 명확히 하기 위한 제약사항이 생겼다.

이런 오류는 위에서 봤던 구문 오류와는 다르다.
ECMA 명세서에 따르면 엄격 모드에서 프로그램을 실행할 때 가이드를 어긴 경우 초기 오류를 발생시키도록 되어있기 때문에 발생하는 초기 오류이다.

전처리 구문인 use strict가 파라미터인 greeting 보다 아래에 있지만 saySomething을 strict 모드로 실행해야 한다는걸 알 수 있었던 것도 프로그램 실행 전 코드 전체가 파싱된다는 가정을 뒷 받침한다.

4. 호이스팅

우선은 let과 const를 사용해서 변수 선언을 하면 호이스팅 단계에서 TDZ가 생기는걸 이용한 예시를 보자.

function saySomething() {
  var greeting = "안녕하세요.";
  {
    greeting = "잘 지내시죠?"; 
    let greeting = "안녕!";
    console.log(greeting);
  }
}

saySomething();

위의 코드를 실행시키면 greeting = "잘 지내시죠?"라는 구문에서 Uncaught ReferenceError: Cannot access 'greeting' before initialization라는 오류가 발생하게 된다.
만약에 실행이 동시에 일어난다고 한다면 다음과 같이 정상적으로 작동했을 것이다.

// JS가 인터프리터 언어처럼 한 줄씩 바로 실행된다면?
function saySomething() {
  var greeting = "안녕하세요.";
  {
    greeting = "잘 지내시죠?";      // greeting은 var greeting 이라고 선언된 변수
    let greeting = "안녕!";         // 새로운 greeting을 let으로 선언
    console.log(greeting);          // "안녕!" 출력
  }
}
saySomething();

하지만 실제 결과에서는 JS가 컴파일 과정에서 변수 선언을 선행하고 값의 할당은 실행 단계에서 일어난다.
그래서 블록 스코프 내부에 있는 let greeting = "안녕!" 구문은 호이스팅 되어서 greeting 변수가 선언되고 JS는 가장 인접한 스코프의 변수를 참조하기 때문에 greeting = "잘 지내시죠?"는 let greeting의 변수를 참조하게 된다.
하지만 let greeting은 실행전까지 TDZ에 존재하기 때문에 오류가 발생하게 되는 것이다.

만약에 블록 스코프에서도 let이 아닌 var를 사용했다면 결과는 또 달라진다.

function saySomething() {
  var greeting = "안녕하세요.";
  {
    greeting = "잘 지내시죠?";
    var greeting = "안녕!";
    console.log(greeting);
  }
}
saySomething();

let과 마찬가지로 var greeting = "안녕!"에서 호이스팅이 발생하여 var greeting;으로 함수가 선언되지만 동시에 undefined가 할당되기 때문에 오류가 발생하지 않고, var는 블록 스코프를 따르지 않고 함수 스코프를 따르기 때문에 var greeting = "안녕하세요"와 var greeting = "안녕!"은 같은 greeting 변수가 된다.
결국에는 greeting은 udefined -> 안녕하세요. -> 잘 지내시죠? -> 안녕! 순서로 변경되면 최후에는 "안녕!"이 콘솔에 출력되게 된다.

5. 결론

이렇게 구문 오류, 초기 오류, 호이스팅을 통해서 JS가 선 컴파일 후 실행 프로그램인 이유에 대해서 알아보았다.
하지만 이렇게 생각할 수도 있다.
파싱으로 AST만 생성하고 오류를 잡은 뒤, 컴파일 없이 AST를 다루는 인터프리트 연산을 거쳐서 실행을 할 수도 있지않나?
실제로 가능하긴 하다.
하지만 JS 엔진이 소스코드를 AST로 바꾸는 수고를 한 뒤에 AST를 가장 효율적으로 사용할 수 있는 바이너리 코드로 굳이 변환하지 않는건 굉장히 비효율적이다.

JS 엔진이 프로그램을 처리할 때 내부에서 일어나는 일들, 그리고 결정적으로 실행 단계가 아닌 실행 전에 오류를 검출한다는 관점으로 볼 때 JS는 컴파일 언어에 가깝다고 보는게 맞는거 같다.

profile
준비하는 개발자

0개의 댓글