자바스크립트 스코프(Scope)

1. 스코프 (Scope)

프로그래밍 언어의 기본 패러다임 중 하나는 변수에 값을 저장하고 저장된 값을 가져다 쓰고 수정하는 것이다. 이 기능은 프로그램에서 상태를 나타낼 수 있게 해준다. 하지만 변수를 프로그램에 추가하면 다음과 같은 궁금증이 생긴다.

  • 변수는 어디에 저장되는걸까?
  • 저장된 변수를 어떻게 찾을 수 있을까?

이 질문을 통해 알 수 있는 것은 특정 장소에 변수를 저장하고 나중에 그 변수를 찾기 위해서 잘 정의된 규칙이 필요하다는 점이다. 이러한 규칙이 없다면 변수가 이리저리 뒤섞여서 프로그램의 상태를 관리하는 일이 매우 어려워질테니 말이다.

변수를 관리하는 규칙을 살펴볼때 등장하는 개념이 바로 스코프(Scope)이다.

스코프를 직역하면 영역, 범위라는 뜻이지만 프로그래밍 언어에서는 변수에 대한 접근성과 변수의 생존기간을 결정하는 범위를 뜻한다.


2. 자바스크립트 변수와 스코프의 특징

  1. var 키워드가 없어도 변수를 선언할 수 있다.
  2. 변수명을 중복해서 선언할 수 있다.
  3. 스코프가 함수 단위 또는 블록 단위로 생성된다.
  4. 스코프 체인을 통해 변수를 검색한다.
  5. 스코프는 렉시컬하게 동작한다.

2.1 var 키워드가 없어도 변수를 선언할 수 있다.

예시코드

function scopeExam() {
  scope = 20;  console.log("scope = " + scope);
}

function scopeExam2() {
  console.log("scope = " + scope);
}

scopeExam();
scopeExam2();

실행결과

scope = 20
scope = 20

다른 프로그래밍 언어의 경우 변수를 선언할 때 intchar와 같은 변수형을 쓰지 않을 경우 에러가 발생하지만, 자바스크립트는 변수형(var)을 생략해도 에러가 발생하지 않고 값이 출력된다.

위 예시코드를 보면 scope라는 변수는 var 없이 scopeExam 함수 안에서 선언된 상태다. 하지만 실행결과를 보면 scopeExam 함수와 scopeExam2 함수 둘 다 scope 변수를 정상적으로 참조했다는걸 알 수 있다.

scopeExam2 함수가 어떻게 scope 변수에 접근할 수 있었던 걸까?

여기에 대한 답은 자바스크립트 프로그램이 파싱되는 과정에서 찾을 수 있다. 프로그램이 전역 레벨에서 파싱될때 var가 생략된 변수는 루트객체의 프로퍼티로 추가되기 때문이다. (프로그램이 실행되는 환경이 웹 브라우저일때 루트객체는 window 객체가 된다.) 따라서 위 예시코드를 브라우저에서 동작시켰다는 가정 하에 scope 변수는 window 객체의 프로퍼티가 되고 전역에서 참조할 수 있게 된다.

console.log(window.scope); // 20

2.2 변수명을 중복해서 선언할 수 있다.

예시코드

var scope = 10;

function scopeExam() {
  var scope = 20;  console.log("scope = " + scope);
}

scopeExam();

실행결과

scope = 20

자바스크립트는 같은 이름으로 변수를 여러번 선언할 수 있다. 동일한 이름의 변수가 여러개 일 경우 가장 가까운 스코프의 변수를 참조한다.

실행결과를 보면 함수 내에서 scope를 호출했을 때 전역 변수 scope를 참조하는 것이 아니라 같은 함수 내에 있는 지역변수 scope를 참조했다는 것을 알 수 있다.


2.3 스코프가 함수 단위 또는 블록 단위로 생성된다.

자바스크립트의 스코프는 함수 단위 또는 블록 단위로 정해진다.

함수 단위 스코프

예시코드

function scopeTest() {
  var a = 0;
  if (true) {
    var b = 0;
    for (var c = 0; c < 5; c++) {
      console.log("c = " + c);
    }
    console.log("c = " + c);
  }
  console.log("b = " + b);
}

scopeTest();

실행결과

c = 0
c = 1
c = 2
c = 3
c = 4
c = 5
b = 0

함수 내부의 모든 변수들이 같은 스코프를 공유하게 되고, 서로를 자유롭게 참조할 수 있다.


블록 단위 스코프

함수가 가장 일반적인 스코프 단위이자 현재 자바스크립트에서 통용되는 가장 널리 퍼린 디자인 접근법이기는 하지만, 블록 단위 스코프 역시 존재한다.

자바스크립트의 블록 단위 스코프는 다양한 요소들을 통해서 구현할 수 있지만 가장 메인이 되는 구현 요소는 ES6에서 도입된 letconst이다.

{
  let fruits = {
    name: "strawberry",
    taste: function() {
      console.log("sweet!");
    },
  };

  fruits.taste(); // sweet!
}

console.log(fruits.name);
// Uncaught ReferenceError: fruits is not defined

let이나 const로 선언된 변수는 자신을 둘러싼 함수 (또는 글로벌) 스코프가 아니라 가장 가까운 임의의 블록에 속하며, 스코프 밖에서는 접근이 불가능하다.

블록 스코프는 중괄호({ })로 감싼 코드 블록마다 생성된 스코프를 말한다.


2.4 스코프 체인을 통해 변수를 검색한다.

예시코드

var a = 4;

function foo(x) {
  var b = a * 4;

  function bar(y) {
    var c = y * b;
    return c;
  }

  return bar(b);
}

console.log(foo(a));

실행결과

256

위 예시코드의 스코프를 그림으로 나타내보면 다음과 같다.

nested scope

하나의 블록이나 함수가 다른 블록이나 함수 안에 중첩될 수 있는 것 처럼 스코프 역시 다른 스코프 안에 중첩될 수 있다. 자바스크립트 엔진은 스코프가 중첩되어 있을 때 찾고자 하는 변수를 다음과 같은 순서로 찾아 나간다.

현재 스코프 → 다음 바깥 스코프 → 그 다음 바깥 스코프 → ... → 전역 스코프

이런식으로 꼬리에 꼬리를 물며 검색을 해나가는 방식을 스코프 체인이라고 하며, 가장 바깥에 위치한 전역 스코프(global scope)에 도달하면 변수를 찾든 못찾든 검색을 멈춘다.

따라서 가장 안쪽에 있는 함수 스코프는 자신을 둘러싼 외부함수들과 전역 스코프가 가지고 있는 변수에 접근이 가능하고, 이러한 이유로 위 예시의 출력결과는 256이 된다.

주의할 점은 이와 반대로 전역 스코프나 그 외의 외부함수(위 이미지에서 foo함수 스코프)는 자신보다 안쪽에 있는 내부함수 스코프의 변수에 접근할 수 없다는 점이다. 예시코드에서 foo(a) 대신 bar(b)를 출력하려고 하면 Uncaught ReferenceError: bar is not defined라는 에러가 뜰 것이다.


2.5 스코프는 렉시컬하게 동작한다.

자바스크립트는 렉시컬 스코프 방식으로 동작한다. 렉시컬 스코프의 렉시컬(lexical)은 컴파일 과정 중 하나인 렉싱(lexing)에서 유래했다. 렉싱이란 문자열을 ‘토큰(token)‘이라 불리는 유의미한 문자 조각으로 만드는 과정이다. 자세한 내용은 흐름상 생략하고 나중에 다시 다시 다뤄보기로 한다.

여기서 중요한건 자바스크립트의 스코프는 렉시컬 스코프이고, 렉시컬하게 동작한다는 말은 곧 스코프가 함수를 선언하는 시점에 생성된다는 것을 의미한다. 호출이 아니라 선언하는 시점이다! 아래 예시 코드를 보자.

예시코드

function f1() {
  var a = 10;
  f2();
}

function f2() {
  return a;
}

f1();

실행결과

Uncaught Reference Error: a is not defined

함수 f1 내부에서 f2를 호출했으니 f2가 f1의 내부함수가 된 것 마냥 변수 a의 값을 참조하고 10을 출력할 것 만 같다.

하지만 자바스크립트 스코프의 렉시컬한 특성을 기억해야 한다. 즉 스코프는 함수를 호출할 때가 아니라 선언할 때 생긴다. 변수 a를 검색할 때 f2가 실행된 시점이 아닌 정의된 시점의 환경을 참조하게 된다. f2 함수의 내부와 전역 스코프를 차례대로 탐색하며 a라는 변수를 찾는다. 하지만 어디에서도 변수 a를 찾을 수 없기 때문에 a is not defined가 출력되는 것이다.



Reference


@Reese
Sin Prosa Sin Pausa