4 minute read

모던 자바스크립트 Deep Dive 13~15장을 정리한 글입니다.

스코프

모든 식별자는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효범위가 결정된다. 즉, 스코프는 식별자가 유효한 범위이다. 자바스크립트 엔진에서 스코프를 통해 어떤 변수를 참조해야 하는지 식별자 결정을 하며 이 떄 스코프를 이용하여 규칙을 정하고 수행한다.

또한 스코프를 통해 식별자(변수 이름)의 충돌을 방지할 수 있고 같은 이름의 변수를 중복하여 사용할 수 있는 네임스페이스 역할을 한다. 스코프의 종류는 크게 2가지로 나뉜다.

  • 전역 스코프

    코드의 가장 바깥쪽 영역에 선언하여 어디서든 참조 가능

  • 지역 스코프 함수 몸체 내부에 선언하여 자기 자신과 하위 지역 스코프에서만 유효하게 참조 가능

스코프 체인

함수는 전역 또는 함수 몸체 내부에 중첩으로 정의가 가능하다. 따라서 함수 정의 위치에 따라서 스코프도 중첩에 의해 계층적 구조를 가진다. 최상위 스코프를 전역 스코프로 고정하고 바깥에 정의된 외부 함수에서 중첩 함수 방향으로 계층적인 구조를 가지면서 스코프를 연결한 것이 스코프 체인이다.

스코프 체인을 이용하여 자바스크립트 엔진은 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다. 코드를 실행하기 전 엔진에서 렉시컬 환경(Lexical Environment)를 실제로 생성하며 변수 선언 시 key 값을 등록하고, 변수 할당 시 값을 변경하는 식의 자료구조를 이룬다.

function foo() {
  console.log("global function foo");
}

function bar() {
  function foo() {
    console.log("local function foo");
  }
  foo();
}

bar(); // local function foo

위와 같이 전역 함수와 지역 함수로 선언되어진 foo 함수가 있다. 앞서 함수에 관련된 정리에서 보았듯 함수 선언문은 런타임 이전에 함수 객체가 먼저 생성하고 암묵적으로 식별자를 선언하고 할당해준다. 따라서 bar 함수가 실행되고 foo 함수를 호출하기 위해 식별자 foo를 스코프 체인에서 검색하게 되고 내부 함수 foo를 호출한다.

렉시컬 스코프

함수의 상위 스코프를 결정하는 방식이 2가지 존재한다.

  • 동적 스코프(dynamic scope)

    함수가 호출되는 시점에 동적으로 상위 스코프를 결정하는 방식.

  • 렉시컬 스코프(lexical scope)

    동적으로 변하지 않고 함수가 정의되는 위치에 따라 스코프가 결정되고 자바스크립트를 비롯한 대부분의 언어가 채택하는 방식이다. 렉시컬 스코프를 따르게 되면 함수를 어디서 호출했는지가 아닌 어디서 정의했는지만 파악하면 상위 스코프를 알 수 있다.

var x = 1;

function foo() {
  var x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); // 1
bar(); // 1

자바스크립트는 렉시컬 스코프 방식으로 스코프가 결정되므로 bar가 선언되어 함수 객체를 생성할 때 자신이 정의된 전역 스코프를 기억한다. 그리고 bar의 위치에 관계없이 변하지 않고 언제나 전역 스코프를 상위 스코프로 사용하여 걀과적으로 전역 변수 x의 값 1을 두 번 출력한다.

전역 변수

앞서 말했듯 지역 스코프는 함수가 생성될 때 만들어지고 지역 변수도 생성한 스코프에 등록된다. 반면에 전역 변수는 함수 호출과 같이 특별한 진입점이 없어서 코드가 로드되지마자 바로 실행된다. 따라서 전역에 선언된 var 키워드는 전역 객체의 프로퍼티가 된다. 이렇게 생성된 전역 변수들은 몇가지 문제점들을 낳을 수 있다.

  • 암묵적 결합 (Implicit coupling) - 모든 코드에서 전역 변수를 참조하고 변경할 수 있게 되어 의도치 않은 변경이 생길 수 있다.

  • 긴 생명주기 - 생명주기가 길어 메모리 리소스를 오래 소비하며 의도치 않은 재할당이 일어날 수 있다.

  • 스코프 체인의 종점에 존재 - 변수를 검색할 때 스코프 체인의 가장 마지막에 존재하므로 검색 시 비효율적이다.

  • 네임스페이스 오염 - 파일이 분리되어 있어도 하나의 전역 스코프를 공유하는 자바스크립트의 특징 때문에 다른 파일 내에 동일한 이름으로 선언된 전역 변수, 함수가 있을 시 예상치 못한 결과를 가져올 수 있다.

전역 변수 사용 억제하는 방법

  1. 전역에 네임스페이스 담당할 객체 생성

    var MYAPP = {};
    
    MYAPP.name = "Moon";
    MYAPP.person = {
      name: "Moon",
      address: "Seoul",
    };
    

    하지만 이 또한 객체를 전역 변수에 할당되는 형태이므로 근본적인 해결책은 아니다.

  2. 즉시 실행 함수

    (function () {
      var foo = 10; // 즉시 실행 함수의 지역 변수
      ...
    }());
    
    console.log(foo) // ReferenceError: foo is not defined
    

    foo는 즉시 실행 함수의 지역 변수가 되고 전역에서 참조할 수 없게 된다. 아래의 예시와 같이 전역에 특정값을 초기화하고 변수들을 전역 스코프에 남기지 않기 위한 방식으로 자주 사용된다.

    var globalVar = (function (num = 0) {
      var localVar = 100;
      return localVar + num;
    })(50);
    
    console.log(globalVar); // 150
    console.log(localVar); // ReferenceError: localVar is not defined
    
  3. 모듈 패턴

    모듈화는 전역 변수의 억제와 캡슐화를 동시에 할 수 있는 방식이다. 클래스를 모방하여 관련된 변수, 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 만든다.

    var Counter = (function () {
      var num = 0;
    
      return {
        increase() {
          return ++num;
        },
        decrease() {
          return --num;
        },
      };
    })();
    
    console.log(Counter.num); // undefined
    console.log(Counter.increase()); // 1
    console.log(Counter.decrease()); // 0
    

let, const, var 키워드

지역 스코프를 만드는 기준이 크게 2가지 존재한다.

  • 블록 레벨 스코프 (Block level scope)

    함수 몸체 뿐만 아니라 모든 코드 블록(if, for, while, try/catch 등)이 지역 스코프를 만든다

  • 함수 레벨 스코프 (Function level scope)

    함수의 코드 블록에서만 지역 스코프를 생성한다. 자바스크립트의 var 키워드로 선언된 변수가 이와 같이 동작한다. ES6에 도입된 let, const 키워드는 블록 레벨 스코프를 지원한다.

var i = 100;
for (var i = 0; i < 5; i++) {
  console.log(i); // 0 1 2 3 4
}
console.log(i); // 5

따라서 위와 같이 var에 의해 반복문 등의 코드 블록에서 의도치 않은 값의 변경이 일어날 수 있다.

console.log(foo); // undefined
console.log(bar); // ReferenceError - 초기화 진행되지 않아 일시적 사각지대에 놓인다

// 초기화 단계
var foo;
let bar;
console.log(foo); // undefined
console.log(bar); // undefined

// 할당 단계
foo = 1;
bar = 1;
console.log(foo); // 1
console.log(bar); // 1

var 키워드로 변수를 선언시 선언과 초기화가 동시에 진행된다. 반면에 let, const의 경우 선언과 초기화가 분리되어 진행되어 초기화 단계 전까지는 참조할 수 없게 되어 변수 호이스팅이 발생하지 않는 것처럼 보인다.

const는 let과 마찬가지로 블록 레벨 스코프를 가지고 변수 호이스팅이 발생하지 않는 것처럼 동작한다. 하지만 let과 달리 재할당이 금지된다.

const person = { name: "Kim" };
person.name = "Park";
console.log(person); // {name: 'Park'}

원시타입의 값을 const에 할당하였으면 원시값 자체가 immutable한 값이므로 재할당 이외에는 변경할 수 있는 방법이 없다 값을 변경할 수 없게된다. 하지만 객체는 재할당 없이 직접 변경이 가능하므로 객체를 할당한 경우에는 값을 변경할 수 있다. const는 재할당을 금지할 뿐 값의 변경 즉, 불변을 의미하지는 않는다

Categories:

Updated: