JavaScript

[JavaScript] JavaScript 스코프 이해하기 01_컴파일, 렉시컬 스코프

joseph0926 2024. 12. 21. 11:40

[참고: You-Dont-Know-JS]

 

GitHub - getify/You-Dont-Know-JS: A book series on JavaScript. @YDKJS on twitter.

A book series on JavaScript. @YDKJS on twitter. Contribute to getify/You-Dont-Know-JS development by creating an account on GitHub.

github.com

 

자바스크립트 코드를 작성하다보면 무조건적으로 변수와 함수등을 선언하고 호출하는 경우가 생깁니다.

근데 만약에 동일한 코드 베이스에 같은 이름의 변수가 2개 이상 존재한다면 자바스크립트는 이를 어떻게 처리할까요?

또는 한 파일에 정의된 변수면 어디서든 참조 가능할까요?

위 책은 이러한 물음을 시작으로 이와 관련된 개념이 스코프에 대해서 설명합니다.

컴파일

스코프를 알아보기 전에 자바스크립트가 어떻게 코드를 읽는지를 알아볼 필요성이 있습니다.

왜냐하면 만약에 자바스크립트가 위에서부터 순차적으로 한줄한줄씩 코드를 읽을때와 한번에 모든 코드를 읽을때와 위 질문들에 대한 답변이 달라질 것이기 때문입니다.

 

앞서 말한 두가지 방식중에 위에서부터 순차적으로 한줄한줄씩 코드를 읽는 방식을 인터프리터 방식이라고 하고, 실행전에 코드를 한번 읽고 실행하는 방식을 컴파일 방식이라고 합니다.

 

조금 더 자세히 풀어보면

  • 공통점
    • 사용자가 작성한 코드를 읽고 기계가 이해 가능한 기계어로 변환하는 과정을 가집니다.
  • 인터프리터
    • 인터프리터 방식은 위에서부터 순차적으로 한줄한줄씩 코드를 기계어로 변환 -> 실행 합니다.
  • 컴파일
    • 소스코드 전체를 기계어로 변환 -> 저장 -> 이후 실행 합니다.

그렇다면 자바스크립트는 인터프리터 언어일까요? 컴파일언어일까요?

사실 자바스크립트 공식 명세서에는 자바스크립트는 xxx한 방식의 언어다 라고 명시된 부분이 없습니다.

따라서 사람들의 해석에 따라 다르게 보는 경우가 존재합니다.

 

하지만 이 책에서는 자바스크립트를 컴파일 언어라고 말하면서 2가지 근거를 제시합니다.

 

우선 컴파일은 크게 3단계로 이루어집니다.

  1. 토큰화/렉싱
    • 단순 문자열이 코드를 의미 있는 조각으로 나누는 과정입니다.
    • 예를들어 const a = 2; 라는 코드 문자열이 존재한다면, 이 과정에서 const, a, =, 2, ; 이렇게 의미 있는 토큰으로 나눠질 수 있습니다.
  2. 파싱
    • 위에서 토큰화한 토큰 스트림을 프로그램의 문법 구조를 나타내는 중첩된 요소들의 트리(AST)로 변환합니다.
    • 예를들어 아래와 같은 구조가 될 수 있습니다.
VariableDeclaration (kind: 'const')
├── declarations: [
│   └── VariableDeclarator
│       ├── id: Identifier (name: 'a')
│       └── init: Literal (value: 2)
│   ]

 

3. 코드 생성

  • AST를 실행 가능한 코드로 변환합니다. const a = 2;의 경우, a라는 변수를 생성하고, 거기에 값을 저장하는 기계어 명령어 세트로 변환됩니다.

이러한 컴파일 과정을 이해한 후에 아래 자바스크립트 2가지 동작 예시를 보면 이 책에서 왜 자바스크립트가 컴파일 언어라고 주장하는지 이해가 잘됩니다.

 

1. 문법 오류

var greeting = "Hello";
console.log(greeting);
greeting = ."Hi";  // SyntaxError

위의 코드는 흐름상에는 오류가 없지만 ."Hi" 부분은 문법적으로 틀린 부분입니다.

만약에 자바스크립트가 인터프리터 언어라면 2번째 라인까지 실행된 후 3번째 라인을 읽을 때 에러가 발생할 것입니다.

하지만 이 코드를 그대로 브라우저 콘솔에 찍어보면 콘솔이 찍히지 않고 에러가 바로 발생합니다.

이는 자바스크립트 코드가 실행 전에 한번 컴파일 된다는 것을 의미합니다.

 

2. 호이스팅

function saySomething() {
    var greeting = "Hello";
    {
        greeting = "Howdy";
        let greeting = "Hi";
        console.log(greeting);
    }
}
// ReferenceError: 초기화 전에 접근할 수 없습니다

saySomething 함수를 호출하면 레퍼런스 에러가 발생합니다.

왜냐하면 블록 스코프를 가진 let으로 선언된 greeting 변수가 블록 스코프 상단으로 호이스팅 되고, 초기화 하는 라인까지 TDZ가 생성됩니다. 이러한 TDZ 영역에서 greeting = "Howdy"로 greeting 변수를 참조하려다 보니 참조 에러가 발생하게 됩니다.

이는 자바스크립트가 greeting 변수가 let 변수에 속함을 이미 알고 있기 때문에 발생하는 에러이고, 이는 마찬가지로 코드가 컴파일 된다는 것을 의미합니다.

 

렉시컬 스코프

이제 자바스크립트가 컴파일 언어라는 것(책 기준)을 이해했으니 스코프에 대해서 말해보겠습니다.

자바스크립트에서의 스코프는 변수나 함수가 접근할 수 있는 유효한 범위를 결정하는 규칙 입니다.

이러한 스코프 중에 컴파일 과정에서 스코프가 결정되는 스코프를 렉시컬 스코프라고 합니다.

컴파일 과정 중에 특히 1번째 단계인 토큰화/렉싱 부분을 가르키는 말입니다. (렉시컬 => 렉싱)

 

책에서는 이러한 스코프를 설명하기 위해 구슬/양동이, 역할극, 빌딩등의 비유를 도입하여 서술합니다.

 

아래는 책이 구슬/양동이에 비유한 내용을 한국어로 번역하여 그대로 가져온 부분입니다.

상상해보세요: 여러분이 구슬 더미를 발견했고, 모든 구슬이 빨강, 파랑, 초록 중 하나의 색으로 되어 있습니다. 이 구슬들을 색깔에 맞는 바구니에 분류합니다 - 빨간 구슬은 빨간 바구니에, 초록 구슬은 초록 바구니에, 파란 구슬은 파란 바구니에 넣습니다. 나중에 초록 구슬이 필요할 때, 여러분은 이미 초록 바구니에서 찾아야 한다는 것을 알고 있죠.

이 비유에서

구슬: 변수

양동이: 스코프

색상: 스코프 범위

 

// 외부/전역 스코프: 빨강(RED)

var students = [
    { id: 14, name: "Kyle" },
    { id: 73, name: "Suzy" },
    { id: 112, name: "Frank" },
    { id: 6, name: "Sarah" }
];

function getStudentName(studentID) {
    // 함수 스코프: 파랑(BLUE)

    for (let student of students) {
        // 루프 스코프: 초록(GREEN)

        if (student.id == studentID) {
            return student.name;
        }
    }
}

var nextStudent = getStudentName(73);
console.log(nextStudent);   // Suzy

이 코드를 구슬/양동이에 비유해서 해석해보면,

초록색 양동이인 (루프)스코프에서 students를 참조하고 있지만, 해당 스코프에서 선언된 변수는 아닙니다.

이 students 구슬은 전역 스코프에서 선언된 빨간색 구슬입니다.

 

이러한 비유에서 알 수 있는 스코프에 대한 특징이 존재합니다.

초록색 양동이에 존재하지만 구슬의 색상이 빨간색인 것을 보아 해당 구슬은 빨간색 양동이 영역에서 선언된 것을 파악했습니다.

 

이처럼 특정 스코프에서 다른 스코프의 변수를 참조할 수 있고, 이는 아래처럼 동작합니다.

  1. 현재 초록색(3) 스코프 바구니에 해당 이름의 구슬이 있는지 확인
  2. 없다면, 다음 외부/상위 스코프인 파란색(2)으로 이동
  3. 파란색(2) 바구니에서도 없으므로, 다시 외부/상위인 빨간색(1)으로 이동
  4. 빨간색(1) 바구니에서 students라는 이름의 구슬을 찾음
  5. 따라서 루프 문의 students 변수 참조는 빨간색(1) 구슬로 결정

변수 참조에는 아래와 같은 규칙이 존재합니다.

  • 현재 스코프에서 일치하는 선언이 있는 경우
  • 현재 스코프보다 상위/외부 스코프에서 일치하는 선언이 있는 경우
  • 하위/중첩된 스코프의 선언은 참조할 없습니다

구슬/양동이 예시로 규칙을 치환해보면

  • 빨간색(1) 양동이의 스코프는 빨간색(1) 구슬만 접근 가능
  • 파란색(2) 양동이의 스코프는 파란색(2)과 빨간색(1) 구슬에 접근 가능
  • 초록색(3) 양동이의 스코프는 빨간색(1), 파란색(2), 초록색(3) 구슬 모두 접근 가능

책에서 또 다른 예시로 친구들과의 대화로도 스코프를 설명합니다. 아래는 책의 예시를 그대로 가져온 부분입니다.

  • Engine: JavaScript 프로그램의 컴파일과 실행을 처음부터 끝까지 담당
  • Compiler: Engine의 친구로, 파싱과 코드 생성의 모든 작업을 처리
  • Scope Manager: Engine 다른 친구로, 모든 선언된 변수/식별자의 목록을 관리하고 현재 실행 중인 코드에서 이들을 어떻게 접근할 있는지에 대한 규칙을 관리

- 컴파일 단계에서의 대화

Compiler: 안녕하세요, (전역 스코프의) Scope Manager님! students라는 식별자의 공식 선언을 발견했는데, 혹시 들어보셨나요?

(전역) Scope Manager: 아니요, 처음 듣네요. 바로 생성해드렸습니다.

Compiler: Scope Manager님, getStudentName이라는 식별자의 공식 선언도 발견했는데요?

(전역) Scope Manager: 이것도 처음이네요. 생성 완료했습니다.

Compiler: getStudentName이 함수를 가리키고 있어서, 새로운 스코프 바구니가 필요합니다.

(함수) Scope Manager: 알겠습니다. 여기 스코프 바구니 있습니다.

Compiler: (함수의) Scope Manager님, studentID라는 매개변수 선언을 발견했는데요?

(함수) Scope Manager: 처음 듣는 거네요. 이 스코프에 생성해뒀습니다.

 

- 실행 단계에서의 대화

실행 단계에서의 대화
Engine: (전역 스코프의) Scope Manager님, 시작하기 전에 getStudentName 식별자를 찾아서 이 함수를 할당하고 싶은데요?

(전역) Scope Manager: 네, 여기 변수 있습니다.

Engine: students 대상 참조를 발견했는데, 알고 계신가요?

(전역) Scope Manager: 네, 이 스코프에 공식적으로 선언되어 있습니다. 여기 있습니다.

Engine: 감사합니다. students를 undefined로 초기화하여 사용할 준비를 하겠습니다.

 


이번 파트를 정리하면

  • 자바스크립트는 컴파일 언어
  • 렉시컬 스코프는 컴파일 단계에서 결정되는 스코프
  • 하위 스코프는 본인 스코프와 상위 스코프 범위만 참조 가능 / 하위 스코프 범위는 불가능