스코프란...?

변수에 접근할 수 있는 범위이다.

  • 모든 식별자(변수 이름, 함수 이름, 클래스 이름 등)는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효범위가 결정된다.
  • 즉, 스코프는 식별자가 유효한 범위를 말한다.
  • 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이라고도 할 수 있다.
function add(x, y) {
	// 매개 변수는 함수 몸체 내부에서만 참조할 수 있다.
  	// 즉, 매개변수의 스코프(유효범위)는 함수 몸체 내부다.
  console.log(x, y); // 2 5
  return x + y; // 7
}

add(2, 5);

// 매개 변수는 함수 몸체 내부에서만 참조할 수 있다.
console.log(x, y); // ReferenceError: x is not defined

예제)

var x = 'global';

function foo() {
	var x = 'local'
    console.log(x); // 1번
}

foo();

console.log(x); // 2번
  1. var x = 'global'은 전역변수이고 var x = 'local'은 foo 함수 내부에 있는 지역변수이다.
  2. 1번을 출력하면 local이고 2번을 출력하면 global이다.

스코프의 종류

자바스크립트에서 스코프를 구분해보면 다음과 같이 2가지로 나눌 수 있다.

  • 전역 스코프 (Global scope) - 코드 어디에서든지 참조할 수 있다.
  • 지역 스코프 (Local scope or Function-level scope) - 함수 코드 블록이 만든 스코프로 함수 자신과 하위 함수에서만 참조할 수 있다.

모든 변수는 스코프를 갖는다. 변수의 관점에서 스코프를 구분하면 다음과 같이 2가지로 나눌 수 있다.

  • 전역 변수 (Global variable) - 전역에서 선언된 변수이며 어디에든 참조할 수 있다.
  • 지역 변수 (Local variable) - 지역(함수) 내에서 선언된 변수이며 그 지역과 그 지역의 하부 지역에서만 참조할 수 있다.

변수를 참조할 때 자바스크립트 엔진은 스코프 체인(스코프가 계층적 연결된 것)을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.

즉, 위를 따르면 상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조 가능하지만 하위 스코프에서 유요한 변수는 상위 스코프에서 참조할 수 없다.

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

  • 함수에 의해서만 지역 스코프가 생성된다.
  • var 키워드로 선언된 변수는 오로지 함수의 코드 블록만을 지역 스코프로 인정한다.
  • 즉, 이말은 var은 함수 밖에서는 모두 전역변수인 것이다.
var x = 1;
if (true) {
  var x = 10;
}
console.log(x); // 10
var i = 10;

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

console.log(i); // 5

렉시컬 스코프(lexical scope)

  • 자바스크립트는 렉시컬 스코프를 따르므로 함수를 어디서 호출했는지가 아니라 함수를 어디서 정의했는지에 따라 상위 스코프를 결정한다.
  • 함수가 호출된 위치는 상위 스코프 결정에 어떠한 영향도 끼치지 않는다.
  • 즉, 함수의 상위 스코프는 언제나 자신이 정의된 스코프다.
var x = 1;

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

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

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

암묵적 전역

var x = 10; // 전역 변수

function foo () {
  // 선언하지 않은 식별자
  y = 20;
  console.log(x + y);
}

foo(); // 30

y = 20을 window.y = 20으로 해석하여 프로퍼티를 동적 생성한다. 결국 y는 전역 객체의 프로퍼티가 되어 마치 전역 변수처럼 동작한다. 이러한 현상을 암묵적 전역(implicit global)이라 한다.
하지만 y는 변수 선언없이 단지 전역 객체의 프로퍼티로 추가되었을 뿐이다. 따라서 y는 변수가 아니다. 따라서 변수가 아닌 y는 변수 호이스팅이 발생하지 않는다.

변수의 생명주기

  • 지역 변수의 생명주기는 함수의 생명 주기와 일치한다.
function foo() { // 변수 x 생성
	var x = 'local' // 변수 x에 값 할당
    console.log(x); // local
  	return x; // 변수 x 소멸
}

foo();
console.log(x); // ReferenceError: x is not defined
  • var 키워드로 선언한 전역 변수의 생명주기는 전역 객체의 생명주기와 일치한다.

전역 객체는 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체다.
브라우저 환경에서 전역객체는 window이므로 브라우저 환경에서 var키워드로 선언한 전역 변수는 전역 객체 window 프로퍼티다.

전역 변수의 문제점

  1. 암묵적 결합 - 모든 코드가 전역 변수를 참조하고 변경할 수 있다.(즉, 변수의 유효 범위 클수록 코드 가독성 떨어지고 상태가 변경될 수 있다.)
  2. 긴 생명 주기 - 메모리 리소스를 오랜 기간 소비한다.
  3. 스코프 체인 상에서 종점에 존재 - 전역 변수의 검색 속도가 가장 느리다.
  4. 네임스페이스 오염 - 다른 파일 내에서 동일한 이름으로 명명된 전역 변수나 전역 함수가 같은 스코프 내에 존재할 경우 예상치 못한 결과 가져올 수 있다.

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

즉시 실행 함수

  • 모든 코드를 즉시 실행 함수로 감싸면 모든 변수는 즉시 실행 함수의 지역 변수가 된다.
(function () {
	var foo = 10; // 즉시 실행 함수의 지역 변수
    // ...
}());

console.log(foo);

네임스페이스 객체

  • 네임스페이스 역할을 담당할 객체를 생성하고 전역변수처럼 사용하고 싶은 변수를 프로퍼티로 추가하는 방법.
var MYAPP = {}; // 전역 네임스페이스 객체
MYAPP.name = 'Park';
console.log(MYAPP.name); // PARK

모듈패턴

  • 클래스를 모방해서 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 만든다. (클로저 기반 동작)
  • 전역 변수의 억제는 물론 캡슐화 구현할 수 있다.

    캡슐화는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것.

var Counter = (function() {
  // private 변수
	var num = 0;
  
  // 외부로 공개할 데이터나 메서드를 프로퍼티로 추가한 객체를 반환한다.
 	return {
    	increase() {
        	return ++num;
        },
      	decrease() {
        	return --num;
        }
    };
}());

// private 변수는 외부로 노출되지 않는다.
console.log(Counter.num); // undefined;
console.log(Counter.increase()); // 1
console.log(Counter.increase()); // 2
console.log(Counter.decrease()); // 1
console.log(Counter.decrease()); // 0

ES6 모듈

  • ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공한다.
<script type="module" src="lib.mjs"></script>
<script type="module" src="app.mjs"></script>

var 키워드로 선언한 변수의 문제점

  1. 변수 중복 선언 허용
var x = 1;
var y = 1;
// var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용한다.

// 초기화문이 있는 변수 선언문은 자바스크립트 엔진에 의해 var 키워드가 없는 것처럼 동작한다.
var x = 100;

// 초기화문이 없는 변수 선언문은 무시된다.
var y;

console.log(x); // 100
console.log(y); // 1
  1. 함수 레벨 스코프
  • var 키워드로 선언된 변수는 오로치 함수의 코드 블록만을 지역 스코프로 인정한다. 나머지는 다 전역변수가 된다.
var x = 1;

if (true) {
	// x는 전역 변수다. 이미 선언된 전역 변수 x 변수는 중복선언된다.
  // 이는 의도치 않게 변수값이 변경되는 부작용 발생
  var x = 10;
};

console.log(x); // 10
  1. 변수 호이스팅
  • var 키워드로 변수를 선언하면 변수 호이스팅에 의해 변수 선언문이 스코프의 선두로 끌어 올려진 것처럼 동작한다.
// 이 시점에는 변수 호이스팅에 의해 이미 foo 변수가 선언되었다.(1. 선언 단계)

// 변수 foo는 undefined로 초기화된다. (2. 초기화 단계)
console.log(foo); // undefined;

// 변수에 값을 할당(3. 할당 단계);
foo = 123;

console.log(foo); // 123

// 변수 선언은 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 실행된다.
var foo;

let 키워드

  • 변수 중복 선언 금지 (같은 스코프 내에서 중복 선언 허용X)
let bar = 123;
let bar = 456; // SyntaxError: Identifier 'bar' has already been declared
  • 블록 레벨 스코프
    let은 var과 다르게 함수 레벨 스코프만을 지역 스코프로 인정하지않고 (함수, if, while, for, try/catch 등)을 지역 스코프로 인정한다.
let foo = 1; // 전역 변수
{
	let foo = 2; // 지역 변수
  	let bar = 3; // 지역 변수
}
console.log(foo); // 1
console.log(bar); // ReferenceError: bar is not defined
  • 변수 호이스팅
    var 키워드로 선언한 변수와 달리 let 키워드로 선언한 변수는 변수 호이스팅이 발생하지 않은 것처럼 동작한다.
console.log(foo); // ReferenceError: foo is not defined 
let foo

let 키워드로 선언한 변수는 호이스팅이 발생하지 않는 것처럼 보이지만 실제로는 그렇지 않다. 자바스크립트는 ES6에서 도입된 let, const를 포함해서 모든 선언을 호이스팅한다.
단, ES6에서 도입된 let, const, class를 사용한 선언문은 호이스팅이 발생하지 않는 것처럼 동작한다.

let foo = 1; // 전역 변수
{
	console.log(foo); // ReferenceError: Cannot acces 'foo' before initialization
	let foo = 2; // 지역 변수
}
  • 전역 객체와 let
    var 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티가 된다. 하지만 let 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아니다.

const 키워드

  • const 키워드로 선언한 변수는 반드시 선언과 동시에 초기화 해야한다.
  • const 키워드도 마찬가지로 블록 레벨 스코프를 가지며, 변수 호이스팅이 발생하지 않는 것처럼 동작한다.
{
	// 변수 호이스팅이 발생하지 않는 것처럼 동작한다.
  console.log(foo); // ReferenceError: Cannot acces 'foo' before initialization
  const foo = 1;
  console.log(foo); // 1
}

// 블록 레벨 스코프를 갖는다.
console.log(foo); // ReferenceError: foo is not defined
  • 재할당 금지
const foo = 1;
foo = 2; // TypeError: Assignment to constant varibable.
  • 상수는 재할당이 금지된 변수를 말한다.

  • const 키워드와 객체

const person = {
	name: 'Park'
};

// 객체는 변경 가능한 값이다. 따라서 재할당 없이 변경이 가능.
person.name = 'Lee'

console.log(person); // {name: "Lee"}

var vs. let vs. const

  • ES6를 사용한다면 var 키워드 사용하지 않는다.
  • 재할당이 필요한 경우에 한정해 let 키워드를 사용한다. 이 때 변수의 스코프는 최대한 좁게 만든다.
  • 변경이 발생하지 않고 읽기 전용으로 사용하는 원시 값과 객체에는 const 키워드를 사용한다. const 키워드는 재할당을 금지하므로 var, let 키워드보다 안전하다.

위에서 let과 const는 실제로 호이스팅이 되지만 호이스팅이 안되는 것처럼 동작한다고 했다.
즉, 호이스팅은 모든 변수, 함수에서 일어난다.
Temporal Dead Zone 에 의해서 let, const의 호이스팅은 에러를 발생시킨다.
Temporal Dead Zone 에 의해 var 와 같은 실행을 막아주었다.
함수의 경우 호이스팅으로 가독성을 높일 수도 있다.

후기

이번에 공부한 스코프와 var, let, const에 대해서 어느정도 알고 있다라고 생각했는데 책을 통해서 좀 더 깊게 공부해보니
자세히 몰랐던 내용도 새로 알 수 있었고 특히 호이스팅에 대해서 좀 더 자세하게 공부할 수 있었다.

참고 PoiemaWeb

profile
꺾여도 하는 마음

0개의 댓글