스코프

등록일: 2014. 09. 17

퍼펙트 자바스크립트: 한 권으로 끝내는 모던 자바스크립트 프로그래밍

  • 이노우에 세이이치로, 츠치에 타쿠로, 하마베 쇼타 지음
  • 황선유 옮김
  • 740쪽
  • 32,000원
  • 2013년 02월 14일

스코프란 이름(변수명이나 함수명)의 유효범위를 말합니다. 스코프에 대해서는 ‘5-3 변수와 프로퍼티’와 ‘5-4 변수명의 해석’도 참고해 주십시오.

자바스크립트의 스코프는 다음의 두 가지입니다.

  • 전역 스코프
  • 함수 스코프

전역 스코프는 함수 바깥(탑 레벨 스코프)의 스코프입니다. 함수의 바깥에서 선언한 이름은 전역 스코프가 됩니다. 소위 말하는 전역 변수나 전역 함수입니다.

함수 안에서 선언한 이름은 함수 스코프를 가지며, 그 함수 안에서만 이름이 유효합니다. 전역 스코프와 대비해서 지역 스코프라고 하거나, 전역 변수에 대비해서 지역 변수라고 부르기도 합니다. 함수의 형식 인자에 해당하는 매개변수도 함수 스코프입니다.

함수 스코프의 동작 방식은 자바(및 다른 많은 프로그래밍 언어)의 지역 스코프와 미묘하게 다릅니다. 자바의 메서드에서 지역 변수는 선언한 행 이후의 스코프를 가집니다. 하지만 자바스크립트의 함수 스코프는 선언한 행과 무관합니다.

예제 6.3의 코드를 봐 주십시오.

예제 6.3 함수 스코프의 주의할 점

var x = 1;
function f() {
    print('x = ' + x);  // 변수 x에 접근
    var x = 2;
    print('x = ' + x);  // 변수 x에 접근
}
// 예제 6.3 호출
js> f();
x = undefined
x = 2

함수 f 안에서 첫 번째 print는 언뜻 전역 변수 x를 표시하는 것처럼 보입니다. 하지만 이 x는 다음 행에서 선언하고 있는 지역 변수 x입니다. 왜냐하면 지역 변수 x의 스코프는 함수 f 전체에 해당하기 때문입니다. 그리고 이 시점에서 아직 값을 대입하고 있지 않으므로 변수 x의 값은 undefined 값입니다. 즉, 함수 f는 다음 코드와 같은 의미입니다.

// 예제 6.3과 의미가 같은 코드
function f() {
    var x;
    print('x = ' + x);
    x = 2;
    print('x = ' + x);
}

예제 6.3과 같은 코드는 매우 알기 어려운 버그의 원인이 됩니다. 그렇기 때문에 지역 변수는 함수의 선두에 모아서 선언하는 방식을 권장합니다.

이는 자바 등 다른 언어에서 변수는 사용하기 직전에 선언해야 한다는 규칙과 다르므로 주의해야 합니다.

6-4-1 웹 브라우저와 스코프

클라이언트 사이드 자바스크립트에서는 각 윈도우(탭), 각 프레임(iframe 포함)마다 전역 스코프가 있습니다. 윈도우 간에는 서로 전역 스코프 이름에 접근할 수 없습니다. 하지만 부모와 프레임 사이에는 서로 접근이 가능합니다.

자세한 내용은 이 책의 3부에서 설명하겠습니다.

6-4-2 블록 스코프

자바스크립트(ECMAScript)에는 블록 스코프가 없습니다. 이것은 다른 여러 프로그래밍 언어와 다른 점입니다. 예를 들어, 실습 6.1을 살펴봅시다. 블록 스코프가 있다고 생각하면 두 번째 print에서 1을 출력할 것으로 예상하겠지만 실제로는 2를 출력합니다.

js> var x = 1;  // 전역 변수
js> { var x = 2; print('x = ' + x); }
x = 2
js> print('x = ' + x); // 1을 예상?
x = 2

실습 6.1 블록 스코프로 착각

실습 6.1은 블록 안에서 블록 스코프의 변수 x를 새롭게 선언하고 있는 것처럼 보이지만 실제로는 전역 변수 x에 값 2를 대입합니다. 즉, 다음 코드와 같은 의미입니다.

// 실습 6.1과 같은 의미의 코드
js> var x = 1;  // 전역 변수
js> { x = 2; print('x = ' + x); }
x = 2
js> print('x = ' + x);
x = 2

블록 스코프로 착각하는 것은 함수 스코프에서도 일어납니다. for 문 안에서 루프 변수를 선언하는 것은 확립된 관용구이지만 루프 변수의 스코프는 for 문에서 끝나지 않습니다. 다음 코드는 단순히 지역 변수 i를 사용하고 있을 뿐입니다.

function f() {
    var i = 1;
    for (var i = 0; i < 10; i++) {
        // 생략
    }
    // 여기서 변수 i의 값은 10
}

6-4-3 let과 블록 스코프

ECMAScript 5에 블록 스코프는 없지만 자바스크립트의 독자 확장에 블록 스코프를 사용할 수 있는 let이 있습니다. let을 사용하는 구문은 let 정의(let 선언), let 문, let 식의 세 가지가 있습니다. 구문은 다르지만 원칙은 같습니다. 각각 순서대로 설명하겠습니다.

let 정의(let 선언)는 var 선언처럼 사용할 수 있습니다. 즉, 다음과 같은 구문으로 변수를 선언할 수 있습니다.

let var1 [= value1] [, var2 [= value2]] [, ..., varN [= valueN]];

let 선언에서 선언한 변수는 블록 스코프입니다. 스코프를 빼면 var로 선언한 변수와 차이는 없습니다. 자의적인 예지만 예제 6.4처럼 됩니다.

예제 6.4 let 선언

function f() {
    let x = 1;
    print(x);       // 1을 출력
    {
        let x = 2;  // 2를 출력
        print(x);
    }               // let x = 2의 스코프는 여기서 끝
    print(x);       // 1을 출력
}
// 예제 6.4 호출
js> f();
1
2
1

스코프의 차이를 빼면 let 변수(let 선언에서 선언한 변수)는 var 변수와 매우 유사하게 동작합니다. 설명은 예제 6.5의 주석을 참고하십시오.

예제 6.5 let 변수의 구체적인 동작 방식의 예

// 이름 탐색
function f1() {
    let x = 1;
    {
        print(x); // 1을 출력, 블록 바깥쪽을 향해 이름을 검색
    }
}

// let 선언보다 앞이라도 이름은 유효
function f2() {
    let x = 1;
    {
        print(x);   // 여기는 let x = 2의 스코프.
        let x = 2;  // 단지 대입하기 전이므로 let 변수 x의 값은 undefined
        print(x);   // 2를 출력
    }
}
// 예제 6.5 호출
js> f1();
1

js> f2()
undefined
2

for 문의 초기화 식에서 var를 선언하는 것을 다음과 같이 let 변수로 하면 스코프가 for 문 안으로 한정되므로 직관적으로 이해하기가 쉬워집니다. 이는 for in 문과 for each in 문에도 마찬가지입니다.

for (let i = 0, len = arr.length; i < len; i++) {
    print(arr[i]);
}
// 여기는 let 변수 i의 스코프를 벗어난다

let 문은 다음 구문을 사용합니다. let 변수의 스코프는 문장에서 닫힙니다.

let (var1 [= value1] [, var2 [= value2]] [, ..., varN [= valueN]]) 문장;

let 문의 구체적인 예는 다음과 같습니다.

let (x = 1) {  // 블록문
    print(x);  // 1을 출력
}              // let 변수의 스코프는 여기까지

var 선언과 let 문을 섞은 구체적인 예를 예제 6.6에서 보겠습니다.

예제 6.6 var 선언과 let 문

function f() {
    var x = 1;
    let (x = 2) {
        print(x); // 2를 출력
        x = 3;
        print(x); // 3을 출력
    }
    print(x); // 1을 출력
}
// 예제 6.6 호출
js> f();
2
3
1

let 문 안에서 let 변수와 같은 이름의 변수를 선언하면 TypeError가 발생합니다. 아래 예제를 보겠습니다.

// let으로 같은 이름의 변수는 선언할 수 없다
    let (x = 1) {
    let x = 2;
}
TypeError: redeclaration of variable x:

// var로도 같은 이름의 변수는 선언할 수 없다
let (x = 1) {
    var x = 2;
}
TypeError: redeclaration of let x:

let 식은 다음과 같은 구문입니다. let 변수의 스코프는 식에서 닫힙니다.

let (var1 [= value1] [, var2 [= value2]] [, ..., varN [= valueN]]) 식;

let 식의 구체적인 예를 보겠습니다.

js> var x = 1;
js> var y = let(x = 2) x + 1; // x+1의 식에서는 let 변수(값은 2)가 사용된다
js> print(x, y); // var 변수 x의 값에는 영향을 미치지 않는다
1 3

6-4-4 중첩 함수와 스코프

자바스크립트의 함수는 중첩으로 선언할 수 있습니다. 즉, 함수 안에서 다른 함수를 선언할 수 있습니다. 이때 안쪽 함수의 내부에서 바깥쪽 함수의 스코프에 접근할 수 있습니다. ‘5-4 변수명의 해석’의 반복이 되지만 형식적으로는 안쪽에서 바깥쪽을 향해 이름을 찾습니다. 마지막으로 찾는 것은 전역 스코프의 이름입니다.

예제 6.7에서 구체적인 예를 들어보겠습니다. 예제 6.7은 함수 선언문으로 작성돼 있지만 함수 리터럴 식으로 작성해도 마찬가지입니다.

예제 6.7 중첩 함수와 스코프

function f1() {
    var x = 1;      // 함수 f1의 지역 변수

    // 중첩 함수 선언
    function f2() {
        var y = 2;  // 함수 f2의 지역 변수
        print(x);   // 함수 f1의 지역 변수에 접근
        print(y);   // 함수 f2의 지역 변수에 접근
    }

    // 중첩 함수 선언
    function f3() {
        print(y);   // 전역 변수 y가 없으면 ReferenceError 발생
    }

    // 중첩 함수 호출
    f2();
    f3();
}
// 예제 6.7 호출
js> f1();
1
2
ReferenceError: y is not defined

6-4-5 셰도잉

셰도잉은 약간 전문적인 용어지만 스코프가 작은 같은 이름의 변수(나 함수)로 스코프가 큰 이름을 숨기는 것을 가리킵니다. 대부분은 의도하지 않은 버그의 원인이 됩니다. 예를 들어, 다음 코드에서는 전역 변수 n을 지역 변수 n이 감추고 있습니다.

js> var n = 1; // 전역 변수
js> function f() {
        var n = 2; // 지역 변수로 셰도잉
        print(n);
    }

// 함수 호출
js> f();
2

언뜻 보면 동작 방식도 명료하고, 예제 6.3이나 실습 6.1 같은 함수 스코프나 블록 스코프에 관련된 셰도잉보다 무해해 보입니다. 하지만 코드가 좀 복잡해지면 의외로 발견하기 어려운 버그가 되므로 주의해 주십시오.