자바스크립트 스코프(Scope)란? 렉시컬 스코프와 스코프 체인

스코프는 변수 같은 식별자를 참조할 수 있는 범위이며, 렉시컬 스코프는 코드가 작성된 위치에 따라 그 범위를 결정하는 규칙이다. 둘 다 여러 프로그래밍 언어에서 쓰이는 개념이며, 자바스크립트도 렉시컬 스코프를 따른다.

JavaScript

스코프(scope)란?

  1. 프로그래밍 언어에서 변수를 선언하면 그 변수는 코드의 정해진 범위 안에서만 참조할 수 있다.

    • 다음은 함수 안에서 선언한 변수를 참조할 수 있는 범위를 보여주는 자바스크립트 예시이다.

      function showMessage() {
        const message = "안녕하세요"; // showMessage 안에서 선언한 지역 변수
        console.log(message); // 함수 안에서는 참조 가능
      }
      
      showMessage(); // "안녕하세요" 출력
      
      // ReferenceError가 발생한다.
      console.log(message); // 함수 밖에서는 이 지역 변수를 참조할 수 없다. 
  2. 이렇게 식별자가 유효한 범위를 스코프라고 한다.

  3. 스코프가 있기 때문에 같은 이름의 변수가 서로 다른 범위에서 충돌 없이 존재할 수 있다.

    • 이름이 같아도 서로 다른 스코프에서 선언하면 별개의 변수이다.

      const score = 100; // 바깥 스코프의 score
      
      function showScore() {
        const score = 50; // 함수 안의 별도 변수: 바깥 score를 수정한 것이 아님
        console.log(score); // 50: 현재 스코프의 score를 참조
      }
      
      showScore();
      console.log(score); // 100: 바깥 score는 그대로 유지됨
  4. 함수 안에서는 가까운 쪽의 score가 바깥쪽의 같은 이름을 가린다.

scope가 결정되는 기준

  1. 스코프를 정하는 방식은 크게 두 가지로 나뉜다.
    • 실행 중인 호출 관계를 따라 호출자의 환경에서 식별자를 찾는 방식을 동적 스코프라고 한다.
    • 함수를 어디서 정의했는지를 기준으로 정하는 방식을 렉시컬 스코프(정적 스코프)라고 한다.
  2. 자바스크립트는 렉시컬 스코프를 따른다.
  3. 즉, 함수의 상위 스코프는 코드를 작성한 위치에 따라 정해지고, 호출 위치와는 관계가 없다.
  4. 이해가 되지 않으면 아래 예시를 확인하고 다시 올라오자.

예시로 보는 동적 스코프 vs 렉시컬 스코프

const x = 1;

function foo() {
  const x = 10;
  bar(); // 호출된 위치: foo내부
}

// 정의된 위치: 전역
function bar() {
  console.log(x);
}

foo(); // 1
bar(); // 1
  1. bar는 foo 안에서 호출되었지만, 정의된 위치는 전역이다.

  2. 그래서 bar의 상위 스코프는 foo가 아니라 전역 스코프가 된다. 왜냐하면 어디서 정의했는지를 기준으로 정하는 방식이 렉시컬 스코프이기 때문이다.

  3. 결과적으로 bar는 전역에 선언된 x, 즉 1을 참조한다.

    const x = 1; // 전역에 선언된 x는 요거
    
    function foo() {
      const x = 10; // 이건 foo 내부에서 선언되었다.
      bar();
    }
    
    // 전역에서 선언
    function bar() {
      console.log(x);
    }
    
    foo(); 
    bar(); // 1
  4. 만약 자바스크립트가 동적 스코프를 따랐다면 foo()를 호출했을 때 10이 출력되었을 것이다. 왜냐하면 동적 스코프는 실행된 위치를 기준으로 식별자를 찾기 때문이다.

    const x = 1; 
    
    function foo() {
      const x = 10;
      bar(); // 동적 스코프였으면 bar()는 바로 위에 있는 x = 10을 참조한다.
    }
    
    function bar() {
      console.log(x);
    }
    
    foo(); // 10: foo 안에서 호출됐으므로 foo의 x를 참조
    bar(); // 1: 전역에서 호출됐으므로 전역 x를 참조

스코프 체인 (중첩된 함수)

  1. 함수 안에 함수를 정의하면 스코프도 겹겹이 중첩된다.

  2. 중첩된 스코프는 안쪽에서 바깥쪽으로 이어지는 연결 구조를 만든다. 이 연결 구조를 스코프 체인이라고 한다.

    const a = 'global'; // 전역 변수
    
    function outer() {
      const b = 'outer'; // outer 내부에서 선언
      function inner() {
        const c = 'inner'; // outer -> inner 내부에서 선언
        console.log(a, b, c);
      }
      inner();
    }
    
    outer(); // global outer inner
  3. inner는 식별자를 찾을 때 먼저 자신의 스코프를 확인한다.

  4. 찾지 못하면 한 단계 바깥인 outer의 스코프로 올라간다.

  5. 거기서도 없으면 전역 스코프까지 올라가며 탐색을 이어간다.

  6. 이 탐색은 안쪽에서 바깥쪽으로 진행하므로, 바깥 코드가 안쪽 지역 변수를 그 이름으로 직접 참조할 수는 없다.

    function outer() {
      const a = "바깥 변수";
    
      function inner() {
        const b = "안쪽 변수";
        console.log(a); // 가능: 안쪽에서 바깥 변수 참조
      }
    
      inner();
      console.log(b); // ReferenceError: 바깥에서 안쪽 변수 참조 불가
    }
    
    outer();

참고 자료

  1. MDN: Closures — Lexical scoping

댓글