이번 포스팅에서는 JS 엔진이 우리가 작성한 코드를 어떻게 처리하는지 깊이 있게 알아보겠습니다.

많은 개발자들이 자바스크립트를 사용하면서도 그 내부 동작 원리에 대해서는 깊이 이해하지 못하는 경우가 많습니다. 그러나 코드가 어떻게 처리되는지 이해하면 더 효율적인 코드 작성은 물론, 디버깅 능력도 크게 향상됩니다. 특히 스코프와 클로저 같은 고급 개념을 마스터하기 위한 기본 토대가 됩니다.

본 포스팅은 4번에 걸쳐서 업로드 됩니다.

  1. JS엔진은 우리가 작성한 코드를 어떻게 처리하나?
  2. 스코프는 무엇인가?
  3. 스코프의 작동 방식과 쓰임새
  4. 스코프를 다룰 때 주의할 점

 

자바스크립트와 컴파일 과정

자바스크립트 코드는 실행되기 전에 반드시 처리 과정을 거칩니다. 이 과정은 명확히 "컴파일레이션(compilation)"이라고 할 수 있습니다. 왜냐하면:

  1. 코드는 실행되기 전에 먼저 파싱됩니다
  2. 다양한 최적화가 수행됩니다
  3. 실행 가능한 형태로 변환됩니다

전통적인 컴파일 언어(C++, Java)와의 가장 큰 차이점은 자바스크립트에서는 컴파일과 실행 사이의 시간이 매우 짧다는 것입니다. 그러나 분명한 컴파일 단계가 존재합니다.

 

코드 컴파일 과정

자바스크립트 엔진이 코드를 처리하는 과정을 더 구체적으로 살펴봅시다. 이 과정은 크게 세 단계로 나뉩니다:

1. 토크나이징/렉싱(Tokenizing/Lexing)

토큰화는 문자열을 의미 있는 토큰(token)으로 나누는 과정입니다. 예를 들어:

var greeting = "안녕하세요";

위 코드는 다음과 같은 토큰으로 나뉩니다:

  • var (키워드)
  • greeting (식별자)
  • = (할당 연산자)
  • "안녕하세요" (문자열 리터럴)
  • ; (세미콜론)

이 단계에서 구문 오류가 발견되면 바로 SyntaxError가 발생합니다:

var greeting = "안녕하세요; // 닫는 따옴표 누락
// Uncaught SyntaxError: Invalid or unexpected token

2. 파싱, AST 생성

파싱 단계에서는 토큰 배열을 프로그램 문법에 맞는 중첩 구조인 AST(Abstract Syntax Tree, 추상 구문 트리)로 변환합니다.

var greeting = "안녕하세요"; 코드의 AST는 대략 다음과 같은 구조를 가집니다:

VariableDeclaration
  ├── kind: "var"
  └── declarations: [
        VariableDeclarator
          ├── id: Identifier(name: "greeting")
          └── init: Literal(value: "안녕하세요", raw: "\"안녕하세요\"")
      ]

AST Explorer(https://astexplorer.net/)와 같은 도구를 사용하면 실제 자바스크립트 코드의 AST를 시각적으로 확인할 수 있습니다. 복잡한 코드의 동작 방식을 이해하는 데 큰 도움이 됩니다.

3. 코드 생성

마지막으로, AST는 실행 가능한 코드로 변환됩니다. 이 단계에서 다음과 같은 중요한 작업이 수행됩니다:

  • 변수와 함수 선언을 처리하고 스코프와 연결
  • 코드 최적화
  • 바이트코드 또는 기계어 생성

코드 생성 단계에서는 변수나 함수 선언들의 스코프가 결정되고, 이후 실행 단계에서 활용될 준비가 됩니다.

코드 실행 과정

컴파일 과정이 완료되면 생성된 코드가 실행됩니다. 실행 단계에서는 앞서 결정된 스코프를 기반으로 변수와 함수들이 메모리에 할당되고 접근됩니다.

실행 흐름 이해하기

간단한 예제를 통해 전체 프로세스를 이해해 봅시다:

console.log(greeting);  // undefined
var greeting = "안녕하세요";
console.log(greeting);  // "안녕하세요"

function sayHello() {
  console.log("Hello!");
}
sayHello();  // "Hello!"

이 코드의 컴파일 및 실행 흐름:

  1. 컴파일 단계:
    • greeting 변수 선언 인식 및 스코프에 등록
    • sayHello 함수 선언 인식 및 스코프에 등록
    • 실행 코드 생성
  2. 실행 단계:
    • 첫 번째 console.log(greeting) 실행: greeting은 선언만 되고 아직 할당되지 않아 undefined 출력
    • greeting = "안녕하세요" 실행: 변수에 값 할당
    • 두 번째 console.log(greeting) 실행: 할당된 값 "안녕하세요" 출력
    • sayHello() 함수 호출: "Hello!" 출력

여기서 greeting이 첫 번째 로그에서 undefined로 출력되는 현상이 바로 호이스팅(hoisting)이라고 하는데, 이는 컴파일 단계에서 선언문이 먼저 처리되기 때문에 발생합니다. 호이스팅에 대해서는 다음 포스팅에서 더 자세히 다루겠습니다.

타깃과 소스의 개념

자바스크립트 엔진이 코드를 컴파일할 때 마주하는 중요한 과제 중 하나는 모든 변수와 함수의 스코프를 올바르게 결정하는 것입니다. 이 과정에서 컴파일러는 각 식별자(변수, 함수명 등)가 "타깃(target)"인지 "소스(source)"인지 판단해야 합니다.

타깃과 소스란?

  • 타깃(Target): 값이 할당되는 변수 (할당문의 왼쪽에 위치)
  • 소스(Source): 값을 제공하는 변수 (할당문의 오른쪽이나 표현식에 위치)

다음 코드에서 타깃과 소스를 식별해봅시다:

var students = [
  { id: 14, name: "카일" },
  { id: 73, name: "수지" },
  { id: 112, name: "지영" },
  { id: 6, name: "푸른" }
];

function getStudentName(studentID) {
  for (let student of students) {
    if (student.id == studentID) {
      return student.name;
    }
  }
}

var nextStudent = getStudentName(73);
console.log(nextStudent);  // "수지"

이 코드에서:

  • students는 배열 리터럴이 할당되는 타깃
  • studentID는 함수 호출 시 인자 값(73)이 할당되는 타깃
  • student는 for 루프에서 배열의 각 요소가 할당되는 타깃
  • getStudentName 함수 호출에서 73은 소스
  • nextStudent = getStudentName(73)에서 nextStudent는 타깃, 함수 호출 결과는 소스
  • console.log에서 nextStudent는 소스

실제 개발에서의 응용

타깃과 소스 개념을 이해하면 코드의 흐름과 변수 사용을 더 명확하게 파악할 수 있습니다. 예를 들어 다음과 같은 코드가 있다고 가정합시다:

var x = 10;
var y = x + 5;
var z = y * 2;

여기서 변수들의 역할을 분석하면:

  • 첫 번째 라인: x는 타깃, 10은 소스(리터럴)
  • 두 번째 라인: y는 타깃, x와 5는 소스
  • 세 번째 라인: z는 타깃, y와 2는 소스

이렇게 코드를 분석하면 데이터의 흐름을 더 명확하게 이해할 수 있습니다. 특히 복잡한 함수나 클로저를 다룰 때 이러한 분석이 도움이 됩니다.

렉시컬 스코프와 컴파일의 관계

지금까지 살펴본 자바스크립트의 컴파일 과정은 렉시컬 스코프의 개념과 직접적으로 연결됩니다. 자바스크립트에서 스코프는 컴파일 타임에 결정되며, 이를 렉시컬 스코프(어휘적 스코프)라고 합니다.

렉시컬 스코프의 핵심 원리

렉시컬 스코프의 가장 중요한 특징은 함수나 블록, 변수 선언의 스코프가 전적으로 코드의 물리적 배치에 따라 결정된다는 점입니다. 간단히 말해, 코드를 작성할 때 변수와 함수를 어디에 배치하느냐에 따라 스코프가 정해집니다.

var globalVar = "전역 변수";

function outer() {
  var outerVar = "외부 함수 변수";
  
  function inner() {
    var innerVar = "내부 함수 변수";
    console.log(globalVar);  // 전역 스코프에서 찾음
    console.log(outerVar);   // outer 함수 스코프에서 찾음
    console.log(innerVar);   // 현재 스코프에서 찾음
  }
  
  inner();
}

outer();

이 코드에서:

  • inner 함수는 자신의 렉시컬 스코프(inner 함수 내부), outer 함수의 스코프, 그리고 전역 스코프에 접근할 수 있습니다.
  • 반면, outer 함수는 inner 함수의 스코프에 접근할 수 없습니다.

컴파일 시 스코프 맵 생성

컴파일 과정에서 JS 엔진은 프로그램 전체의 스코프 구조를 담은 일종의 "지도"를 생성합니다. 이 지도는 어떤 변수가 어떤 스코프에 속하는지를 정의하며, 런타임에 변수를 찾을 때 사용됩니다.

중요한 점은 컴파일레이션 중에는 스코프를 식별하기만 하고, 실제 스코프 객체는 코드가 실행되는 런타임에 생성된다는 것입니다. 컴파일 단계에서는 스코프와 변수의 메모리 예약 관점에서 실제로는 아무것도 실행되지 않습니다.

스코프 체인과 변수 검색

변수를 참조할 때, JS 엔진은 현재 스코프에서 시작해 외부 스코프로 단계적으로 이동하며 변수를 찾습니다:

  1. 현재 스코프에서 변수명 검색
  2. 찾지 못하면 바로 바깥 스코프로 이동하여 검색
  3. 다시 찾지 못하면 또 바깥 스코프로 이동
  4. 전역 스코프까지 검색했는데도 찾지 못하면 ReferenceError 발생

이 과정은 변수가 어디에서 선언되었는지를 기준으로 한 렉시컬 스코핑이며, 코드가 어디서 호출되는지가 아니라 어디에 작성되었는지가 중요합니다.

렉시컬 스코프의 실용적 의미

렉시컬 스코프를 이해하면 코드를 더 예측 가능하게 작성할 수 있습니다. 몇 가지 실용적인 팁:

  1. 변수 선언은 사용 범위에 가장 가까운 스코프에서 하기: 전역 변수 사용을 최소화하고, 필요한 스코프에서만 변수를 선언합니다.
  2. 같은 이름의 변수 중복 선언 피하기: 특히 중첩된 스코프에서 같은 이름의 변수를 사용하면 예상치 못한 결과가 발생할 수 있습니다.
  3. 블록 스코프 활용하기: ES6의 let과 const를 사용해 변수의 스코프를 최소화합니다.
// 피해야 할 패턴
var userId = 123;

function processUser() {
  // 의도치 않게 전역 변수를 덮어씀
  userId = 456;
  // ...
}

// 권장하는 패턴
var userId = 123;

function processUser() {
  // 지역 변수로 선언하여 스코프 분리
  let userId = 456;
  // ...
}

마치며: 렉시컬 스코프와 코드 작성의 중요성

지금까지 JS 엔진이 코드를 처리하는 방식에 대해 살펴봤습니다. 자바스크립트는 컴파일 과정을 거쳐 코드를 실행하며, 이 과정에서 렉시컬 스코프가 결정됩니다.

자바스크립트에서 스코프가 컴파일 타임에 결정된다는 사실은 코드를 작성하는 방식이 매우 중요하다는 것을 의미합니다. 변수와 함수를 어디에 배치하느냐에 따라 전체 프로그램의 동작이 결정됩니다.

컴파일 중에는 스코프를 식별하기만 하고, 실제 각 스코프를 실행해야만 하는 런타임 전까지는 스코프가 생성되지 않습니다. 컴파일 단계에서는 프로그램 실행에 필요한 모든 렉시컬 스코프가 들어간 '지도'를 만들어냅니다. 이것이 런타임에 사용할 모든 코드가 들어간 계획안이라고 생각하면 됩니다.

이러한 이해를 바탕으로 코드를 작성하면:

  • 의도하지 않은 변수 참조나 충돌을 방지할 수 있습니다
  • 코드의 예측 가능성과 유지보수성이 향상됩니다
  • 스코프 관련 디버깅을 더 쉽게 할 수 있습니다

다음 포스팅에서는 "스코프는 무엇인가?"라는 주제로, 자바스크립트의 다양한 스코프 유형과 작동 방식에 대해 더 깊이 알아보겠습니다. 특히 함수 스코프와 블록 스코프의 차이, 중첩 스코프, 그리고 스코프 체인의 동작 원리를 자세히 살펴볼 예정입니다.


참고 자료:

  • "You Don't Know JS Yet" by Kyle Simpson
  • ECMA-262 명세서
  • MDN Web Docs

+ Recent posts