스코프와 전역 변수의 문제점

</>·2021년 11월 27일
5
post-thumbnail

목표

  • 13, 14장의 내용을 최대한 이해하고 정리하기

13장 스코프


13-1. 스코프(scope)란?

  • 모든 식별자(변수, 함수, 클래스 이름 등)는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다.
  • 즉, 스코프는 식별자의 유효한 범위를 말한다.
var x = "global";

function scope() {
  var x = "local";
  console.log(x);
}

scope();

console.log(x);

// 결과
"local"
"global"
  • 가장 바깥 부분과 scope 함수 내부에 같은 이름을 갖는 변수 x를 선언했고 함수 외부와 내부에서 각각 변수 x를 참조한다.
  • 이 때, 자바스크립트 엔진은 이름이 같은 두 개의 변수 중에서 어떤 변수를 참조 해야할 것인지를 결정해야 한다.
  • 이를 식별자 결정(Identifier resolution)이라 한다. 결국, 스코프란 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이라고 할 수 있다.
  • 자바스크립트 엔진은 코드를 실행할 때 코드의 문맥을 고려한다.

✏️ 참고

  • "코드가 어디서 실행되며 주변에 어떤 코드가 있는지"를 렉시컬 환경(lexical environment)이라고 부른다. 즉, 코드의 문맥은 렉시컬 환경으로 이루어진다.
  • 이를 구현한 것이 실행 컨텍스트(execution context)이며 모든 코드는 실행 컨텍스트에서 평가되고 실행된다.
  • 위 코드에서 가장 바깥 영역에 선언된 x 변수는 어디서든 참조할 수 있다.
  • 하지만, scope 함수 내부에 선언된 x 변수는 scope 함수 내부에서만 참조할 수 있고 외부에서는 참조할 수 없다.
  • 즉, 두 개의 변수 x 변수는 식별자 이름이 동일하지만 유효 범위 즉, 스코프가 다른 별개의 변수이다.

  • 식별자와 비슷한 예로 파일 이름은 하나의 파일 이름을 구별할 수 있는 식별자이다. 따라서, 파일 이름은 유일해야 한다.
  • 하지만, 우리는 파일 이름을 하나만 사용하지 않는 경우가 있다. 위의 그림처럼 index.js 와 같은 파일 이름을 중복해서 사용한다.
  • 이렇게 사용할 수 있는 이유는 폴더(디렉토리)라는 개념이 있기 때문이다.
  • 파일 이름이 폴더 안에서 유효한 범위를 갖는 것처럼 변수도 스코프(유효 범위)를 통해 식별자인 변수 이름의 충돌을 방지하여 같은 이름의 변수를 사용할 수 있게 된다.

✏️ 참고

  • var 키워드로 선언된 변수는 같은 스코프 내에서 중복이 허용된다. 이는 의도치 않은 변수 값이 재할당되는 부작용을 초래할 수 있다.
function scope() {
  var x = 1;
  var x = 2;
  console.log(x);
}
scope();
// 결과
2
  • let이나 const 키워드로 선언된 변수는 같은 스코프 내에 중복을 허용하지 않는다. 따라서, ES6 문법을 사용한다면 var 보다는 let과 const를 쓰는 것이 바람직하다.
function scope() {
  let x = 1;
  let x = 2;
  console.log(x);
}
scope();
// 결과
"SyntaxError: Identifier 'x' has already been declared"

13-2. 스코프의 종류

  • 스코프는 전역(global)지역(local)으로 구분할 수 있다.
    • 전역: 코드의 가장 바깥 영역
    • 지역: 함수 몸체 내부
  • 다음은 전역 스코프와 지역 스코프에 대한 코드이다.
var x = "global x";
var y = "global y";


function outer() {
  var z = "outer local z";
  
  console.log(x);		// global x
  console.log(y);		// global y
  console.log(z);		// outer local z
  
  function inner() {
    var x = "inner local x";
    
    console.log(x);		// inner local x
    console.log(y);		// global y
    console.log(z);		// outer local z
  }
  
  inner();
}

outer();

console.log(x);			// global x
console.log(y);			// global y
console.log(z);			// ReferenceError: z is not defined 

13-2-1. 전역과 전역 스코프

  • 전역은 전역 스코프를 만든다. 전역에 변수를 선언하면 전역 스코프를 갖는 전역 변수가 된다.
  • 전역 변수는 어디서든지 참조할 수 있다.

13-2-2. 지역과 지역 스코프

  • 지역도 지역 스코프를 만든다. 지역에 변수를 선언하면 지역 스코프를 갖는 지역 변수가 된다.
  • 지역 변수는 자신이 선언된 지역과 하위 지역(중첩 함수)에서만 참조할 수 있다. 즉, 자신의 지역 스코프와 하위 지역 스코프에서 유효하다.
  • 위 코드에서 outer 함수 내부에 선언된 변수 z는 지역 변수이므로 자신의 지역 스코프인 outer 함수 내부와 하위 지역 스코프인 inner 함수 내부에서 참조할 수 있다.
  • inner 함수에 선언된 x 변수와 전역 x 변수의 이름이 같을 때는 자바스크립트 엔진이 inner 함수 내부에 선언된 변수 x를 참조한다.
  • 이는 자바스크립트 엔진이 스코프 체인을 통해 참조할 변수를 검색했기 때문이다.

13-3. 스코프 체인

  • 위 코드에서 inner() 함수는 outer() 함수 내부에 정의된 함수로 중첩 함수(nested function)라 하고 중첩 함수를 포함한 outer() 함수외부 함수(outer function)라고 한다.
  • 이때, 외부 함수의 지역 스코프를 중첩 함수의 상위 스코프라고도 부른다.
  • 이처럼 함수는 중첩될 수 있으므로 함수의 지역 스코프도 중첩될 수 있다. 이는 스코프가 함수의 중첩에 의해 계층적 구조를 갖는다는 뜻이다.

  • 모든 스코프는 하나의 계층적 구조로 연결되며, 모든 지역 스코프의 최상위 스코프는 전역 스코프이다.
  • 이렇게 스코프가 계층적으로 연결된 것을 스코프 체인(scope chain)이라 한다.
  • 변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색 한다.
  • 즉, 상위 스코프에서 선언한 변수를 하위 스코프에서 참조할 수 있다는 것이다.

✏️ 참고

  • 스코프 체인은 물리적인 실체가 존재한다.
  • 자바스크립트 엔진은 코드를 실행하기 전 (위 그림과 유사한 자료구조로)렉시컬 환경을 실제로 생성한다. 변수 선언이 실행되면 변수 식별자가 렉시컬 환경의 키로 등록되고, 변수 할당이 일어나면 이 자료구조의 변수 식별자에 해당하는 값을 변경한다.

13.3.1 스코프 체인에 의한 변수 검색

  • inner 함수 내부에서는 어떻게 변수를 검색하는지 살펴보자. 위에서 살펴본 코드를 간략화하면 다음과 같다.
var x = "global x";
var y = "global y";

function outer() {
  var z = "outer local z";
  
  function inner() {
    var x = "inner local x";
    
    console.log(x);		// inner local x
    console.log(y);		// global y
    console.log(z);		// outer local z
  }
  
  inner();
}

// 생략
  1. 먼저, inner 함수의 지역 스코프에 x 변수가 선언되었는지 검색한다.
    • inner 함수에 x 변수가 선언되었으므로 그 변수를 참조하고 검색을 종료한다.

  2. y 변수 역시 inner 함수 지역 스코프에 선언되었는지 검색한다.
    • inner 함수 내부에는 y 변수가 선언되지 않았으므로 상위 스코프인 outer 함수의 지역 스코프로 이동해 검색한다.
    • outer 함수 내부에도 y 변수의 선언이 존재하지 않으므로 상위 스코프인 전역 스코프로 이동해 검색한다.
    • 전역 스코프에 y 변수의 선언이 존재하므로 검색된 변수를 참조하고 검색을 종료한다.

  3. z 변수도 마찬가지로 inner 함수를 먼저 검색하고 outer 함수를 검색한다.
    • outer 함수 내부에 z 변수가 선언되었으므로 검색된 변수를 참조하고 검색을 종료한다.
  • 중요한 점은 상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조할 수 있지만 하위 스코프에서 유효한 변수를 상위 스코프에서 참조할 수 없다는 것이다.

※ 13.3.2 스코프 체인에 의한 함수 검색은 생략


13-4. 함수 레벨 스코프

  • 대부분의 프로그래밍 언어는 함수 몸체만이 아니라 모든 코드 블록(if, for, while, try ~ catch)이 지역 스코프를 만든다. 이러한 특성을 블록 레벨 스코프(block level scope)라 한다.
  • 하지만, var 키워드로 선언된 변수는 오로지 함수의 코드 블록(함수 몸체) 만을 지역 스코프로 인정한다. 이러한 특성을 함수 레벨 스코프(function level scope)라고 한다.
var iter = 10;

for(var iter = 0; iter < 5; iter++) {
  console.log(iter);
}

console.log(iter);

// 결과
0 1 2 3 4
5
  • 블록 레벨 스코프를 지원하는 프로그래밍 언어에서는 for 문에서 반복을 위해 선언된 iter 변수는 for문의 코드 블록 내에서만 유효하다.
  • 하지만 var 키워드로 선언된 변수는 블록 레벨 스코프를 인정하지 않아 iter 변수는 전역 변수가 된다. 따라서, 전역 변수 iter는 중복 선언되고 그 결과 의도하지 않은 값이 재할당된다.
  • var 키워드로 선언된 변수는 오로지 함수의 코드 블록만을 지역 스코프로 인정하지만 ES6에서 도입된 let, const는 블록 레벨 스코프를 지원한다.

13-5. 렉시컬 스코프

다음 코드의 실행 결과는 무엇일까?

var x = 1;

function scope1() {
  var x = 10;
  scope2();
}

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

scope1();
scope2();
  • 위 코드의 실행 결과는 bar 함수의 상위 스코프가 무엇인지에 따라 결정된다.
  1. 함수를 어디서 호출했는지
    • scope2 함수의 상위 스코프는 scope1 함수의 지역 스코프와 전역 스코프이다.
  2. 함수를 어디서 정의했는지
    • scope2 함수의 상위 스코프는 전역 스코프 뿐이다.
  • 첫 번째 방식을 동적 스코프(dynamic scope)라고 한다. 동적 스코프는 함수가 호출되는 시점에 동적으로 상위 스코프를 정한다.
  • 두 번째 방식을 렉시컬 스코프(lexical scope) 또는 정적 스코프(static scope)라고 한다. 정적 스코프는 함수 정의가 평가되는 시점에 정적으로 상위 스코프를 정한다.
  • 자바스크립트는 렉시컬 스코프를 따르므로 함수를 어디서 정의했는지에 따라 상위 스코프를 결정한다.
  • 위 코드 같은 경우에는 scope2 함수는 전역에서 정의된 함수이기 때문에 전역 코드가 실행되기 전에 먼저 평가되어 함수 객체를 생성한다. 이 때 생성된 scope2 함수 객체는 자신이 정의된 스코프, 즉 전역 스코프를 기억하고 그 스코프를 사용한다.
  • 따라서 결과는 전역 변수 x의 값인 1을 두 번 출력한다.
  • 참고로 렉시컬 스코프는 클로저와 깊은 관계가 있다.

14장 전역 변수의 문제점

14-1. 변수의 생명 주기

  • 변수는 선언에 의해 생성되고 할당을 통해 값을 갖는다. 그리고 언젠가 소멸한다. 즉, 생명 주기(life cycle)가 있다.

14-1-1. 지역 변수의 생명 주기

  • 함수 내부에서 선언된 지역 변수는 함수가 호출되면 생성되고 함수가 종료하면 소멸한다.
function lifeCycle() {
  console.log(1);
  var life = "local";
  console.log(life);
  return life;
}

lifeCycle();
console.log(life);

// 결과 
1
"local"
"Uncaught ReferenceError: life is not defined"
  • 함수 내부에서 선언한 변수는 함수가 호출된 직후에 함수 몸체의 다른 문들이 순차적으로 실행되기 이전에 자바스크립트에 의해 먼저 실행된다.
  • 위의 코드에서 lifeCycle 함수를 호출하면 life 변수의 선언문이 가장 먼저 실행되어 undefined로 초기화된다.
  • 그 후, 순서대로 실행되어 1이 먼저 출력되고 life 변수에 local 이라는 문자열 값이 할당된다.
  • 그리고 함수가 종료하면 life 변수가 소멸되어 생명 주기가 종료된다.
  • 지역 변수 life는 함수가 호출되어 실행되는 동안에만 유효하다. 즉, 함수 몸체 내부에서 선언된 지역 변수의 생명 주기는 함수의 생명 주기와 일치한다.

✏️ 참고

  • 함수 몸체 내부에서 선언된 지역 변수의 생명주기는 함수의 생명 주기와 대부분 일치하지만 그렇지 않은 경우도 있다.

  • 일반적으로 변수의 생명 주기는 메모리 공간이 확보된 시점부터 메모리 공간이 해제되어 가용 메모리 풀에 반환되는 시점까지다.
  • 함수 내부에서 선언된 지역 변수는 함수가 생성한 스코프에 등록되는데 누군가가 이 스코프를 참조하지 않을 때 까지 생존하게 된다.
  • 예를 들어, 함수가 종료되면 함수가 생성한 스코프도 소멸된다. 하지만, 누군가 이 스코프를 참조하고 있다면 스코프는 해제되지 않고 생존하게 된다. 이는 클로저와 관련이 깊다.

14-1-2. 전역 변수의 생명 주기

  • 전역 변수의 생명 주기는 애플리케이션의 생명 주기와 같다. 즉, 마지막 문이 실행되어 더 이상 실행할 문이 없을 때 종료한다.
  • var 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 된다. 이는 전역 변수의 생명 주기와 전역 객체의 생명 주기가 일치하다는 것을 말한다.

✏️ 참고

  • 전역 객체는 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체다.
  • 클라이언트 사이드 환경(브라우저)에서는 window 객체, 서버 사이드 환경(Node.js)에서는 global 객체 를 의미한다.
  • 환경에 따라서 전역 객체를 가리키는 다양한 식별자(window, self, this, global 등)이 존재 했으나 ES11에서 globalThis로 통일되었다.
  • 브라우저 환경에서 전역 객체는 window이므로 브라우저 환경에서 var 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티다. window는 웹 페이지를 닫기 전까지 유효하다.

14-2. 전역 변수의 문제점

14-2-1. 암묵적 결합

  • 전역 변수는 코드 어디서든 참조하고 할당할 수 있다. 이는 모든 코드가 전역 변수를 참조하고 변경할 수 있는 암묵적 결합(implict coupling)을 허용하는 것이다.
  • 변수의 유효 범위가 크면 클수록 가독성은 나빠지고 의도치 않게 상태가 변경될 수 있는 위험성도 높아진다.

14-2-2. 긴 생명 주기

  • 전역 변수는 생명 주기가 길다. 따라서 메모리 리소스도 오랜 기간 소비한다.
  • var 키워드는 변수의 중복 선언을 허용하므로 생명 주기가 긴 전역 변수는 변수 이름이 중복될 가능성이 있다.

14-2-3. 느린 검색 속도

  • 전역 변수는 스코프 체인 상에서 제일 종점에 존재한다. 이는 변수를 검색할 때 가장 마지막에 검색된다는 것이다.
  • 검색 속도의 차이는 그다지 크지 않지만 전역 변수의 검색 속도가 가장 느리다.

14-2-3. 네임스페이스 오염

  • 자바스크립트의 가장 큰 문제점 중 하나는 파일이 분리되어 있다 해도 하나의 전역 스코프를 공유한다는 것이다.
  • 따라서 다른 파일 내에서 동일한 이름으로 명명된 전역 변수나 전역 함수가 같은 스코프 내에 존재할 경우 예상치 못한 결과를 가져올 수 있다.

14-3. 전역 변수의 사용을 억제하는 법

  • 전역 변수를 반드시 사용해야 할 이유를 찾지 못한다면 지역 변수를 사용해야 한다.
  • 변수의 스코프는 좁을 수록 좋다.

14-3-1. 즉시 실행 함수

  • 함수 정의와 동시에 호출되는 즉시 실행 함수를 사용하는 것이다.
  • 모든 코드를 즉시 실행 함수로 감싸면 모든 변수는 즉시 실행 함수의 지역 변수가 된다.
(function() {
  var num = 10;
}());

console.log(num);

// 결과
"Uncaught ReferenceError: num is not defined"
  • 이 방법을 사용하면 전역 변수를 생성하지 않으므로 라이브러리 등에 자주 사용된다.

14-3-2. 네임스페이스 객체

  • 전역에 네임스페이스(namespace) 역할을 담당할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가하는 방법이다.
var GLOBAL = {};

GLOBAL.person = {
  name: "Lee",
  address: "Seoul",
};

GLOBAL.print = function() {
  console.log(GLOBAL.person.name);
};

GLOBAL.print();

// 결과
"Lee"
  • 네임스페이스를 분리해서 식별자 충돌을 방지하는 효과는 있으나 네임스페이스 객체 자체가 전역 변수에 할당되므로 그렇게까지 유용하진 않다.

14-3-3. 모듈 패턴

  • 클래스를 모방해서 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 만드는 방법이다.
  • 모듈 패턴은 자바스크립트의 강력한 기능인 클로저를 기반으로 동작한다.
  • 모듈 패턴의 특징은 전역 변수의 억제는 물론 캡슐화(객체의 프로퍼티와 메서드를 하나로 묶은 것)까지 구현할 수 있다.
  • 자바스크립트는 public, private 등의 접근 제한자를 제공하지 않지만 정보 은닉을 구현하기 위해 사용한다.
var Counter = (function() {
  // private
  var num = 0;
  
  // 외부로 공개할 데이터나 메서드를 프로퍼티로 추가한 객체를 반환
  return {
    increase() {
      return ++num;
    },
    decrease() {
      return --num;	
    },
  }
}())

console.log(Counter.num);
console.log(Counter.increase());
console.log(Counter.decrease());

// 결과
undefined
1
0
  • 위 예제의 즉시 실행 함수는 객체를 반환한다.
  • 이 객체에는 외부에 노출하고 싶은 변수나 함수를 담아 반환한다. 이 때 반환되는 객체의 프로퍼티는 외부에 노출되는 퍼블릭 멤버(public member)이다.
  • 외부로 노출하고 싶지 않은 변수나 함수는 반환하는 객체에 추가하지 않으면 외부에서 접근할 수 없는 프라이빗 멤버(private member)가 된다.

14-3-4. ES6 모듈

  • ES6 모듈을 사용하면 전역 변수를 사용할 수 없다. 파일 자체의 독자적인 모듈 스코프를 제공하기 때문이다.
  • 따라서 모듈 내에서 var 키워드로 선언한 변수는 더는 전역 변수가 아니며 window 객체의 프로퍼티도 아니다.
<script type="module" src="lib.mjs"></script>
  • script 태그에 type="module" 어트리뷰트를 추가하면 로드된 자바스크립트 파일은 모듈로서 동작한다.
  • 모듈의 파일 확장자는 mjs를 권장한다.

✏️ 참고

  • ES6 모듈은 IE를 포함한 구형 브라우저에서는 동작하지 않으며, 브라우저의 ES6 모듈 기능을 사용하더라도 트랜스 파일링이나 번들링이 필요하기 때문에 브라우저가 지원하는 ES6 모듈 기능보다는 Webpack 등의 모듈 번들러를 사용하는 것이 일반적이다.
profile
개발자가 되고 싶은 개발자

2개의 댓글

comment-user-thumbnail
2021년 11월 29일

저도 이 책 공부하고 있는데 유익한 정보 감사드립니다 ! :)

1개의 답글