스코프
다른 코드가 식별자를 참조할 수 있는 유효 범위
식별자가 유효한 범위
식별자를 검색하는 규칙
- 식별자의 이름이 같아도 스코프가 다르기 때문에 같은 이름의 변수를 사용할 수 있다.
스코프 구분
- 코드의 가장 바깥 영역(전역)에 선언된 변수는 전역 스코프를 갖는 전역 변수로, 어디서든 참조 가능하다.
- 함수 몸체 내부에서 선언된 변수는 지역 스코프를 갖는 지역 변수로, 함수 내부와 내부 함수에서 참조 가능하다.
함수 레벨 스코프
자바스크립트의 var 키워드는 함수의 코드 블록만을 지역 스코프로 인정한다. 이러한 특성을 함수 레벨 스코프라고 한다.
var i = 10;
// for문에서 선언한 i는 전역 변수다. 이미 선언된 전역 변수 i가 있으므로 중복 선언되어 전역 변수의 값이 재할당된다.
for (var i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}
console.log(i); // 5
❗️ES6에 도입된 let, const 키워드는 블록 레벨 스코프를 지원한다.
블록 레벨 스코프
대부분의 프로그래밍 언어는 모든 코드 블록(If, for, while, try/catch 등)이 지역 스코프를 만든다. 이러한 특성을 블록 레벨 스코프라고 한다.
스코프 체인
스코프가 계층적으로 연결된 것
하위 스코프에서 상위 스코프에서 선언한변수를 참조할 때, 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 선언된 변수를 차례대로 검색한다.
렉시컬 스코프
자바스크립트는 함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정한다.
다음의 예제 코드를 보자.
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // ?
bar(); // ?
위의 코드는 함수의 상위 스코프를 결정하는 두 가지 방식에 따라 결과가 다르게 나온다.
- 동적 스코프 ➡️ 함수를 어디서 호출했는지에 따라
- 렉시컬 스코프 (정적 스코프) ➡️ 함수를 어디서 정의했는지에 따라
자바스크립트는 렉시컬 스코프를 따른다.
위의 예제 코드를 실행하면 bar 함수 선언문은 런타임 이전에 실행되어 함수 객체를 생성하면서 자신이 정의된 스코프인 전역 스코프를 기억한다. 따라서 bar 함수는 어디서 호출되든 전역 스코프를 상위 스코프로 사용하고, 1을 두번 출력한다.
전역 변수의 문제점
1️⃣ 암묵적 결합
모든 코드가 전역 변수를 참조하고 변경할 수 있는 암묵적 결합을 허용한다. 이에 따라 가독성이 나빠지고 의도치 않은 상태 변경이 발생한다.
2️⃣ 긴 생명주기
전역 변수는 생명 주기가 길어 메모리 리소스도 오랜 시간 소비하고, 전역 변수의 상태가 변경 가능한 시간과 기회가 늘어난다.
3️⃣ 스코프 체인 상 종점에 존재
변수 검색 시 전역 변수는 가장 마지막에 검색되므로 검색 속도가 가장 느리다.
4️⃣ 네임스페이스 오염
전역 객체는 코드가 실행되기 전 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체이다.
클라이언트 사이드 환경(browser) ➡️ window 객체
서버 사이드 환경(Node.js) ➡️ global 객체
var 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 되고, 전역 변수의 생명 주기는 전역 객체의 생명 주기와 같다.
자바스크립트는 파일이 분리되어 있어도 하나의 전역 스코프를 공유한다. 따라서 다른 파일 내 동일한 이름의 전역 변수나 전역 함수가 같은 스코프 내에 존재하면 예상치 못한 결과를 가져올 수 있다.
전역 변수 사용 억제
전역 변수의 무분별한 사용을 억제해야 한다.
전역 변수를 반드시 사용해야 할 이유를 찾지 못한다면 지역 변수를 사용해 스코프를 좁게 하는 것이 좋다.
전역 변수의 사용을 억제할 수 있는 방법은 다음과 같다.
즉시 실행 함수
- 즉시 실행 함수는 함숭 정의와 동시에 단 한번만 호출된다.
- 모든 코드를 즉시 실행 함수로 감싸면 모든 변수는 즉시 실행 함수의 지역 변수가 된다.
- 이 방법은 전역 변수를 생성하지 않아 라이브러리 등에 자주 사용된다.
(function () {
var foo = 10; // 즉시 실행 함수의 지역 변수
// ...
}());
console.log(foo); // ReferenceError : foo is not defined
클로저
- 모듈 패턴은 클래스를 모방해 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 만든다.
- 모듈 패턴은 전역 변수의 억제가 가능하고 캡슐화 구현이 가능하다.
- 모듈 패턴은 자바스크립트의 클로저 기능을 기반으로 동작한다.
클로저 챕터에서 더 자세히 알아보도록 한다.
ES6 모듈
- ES6 모듈을 사용하면 더는 전역 변수를 사용할 수 없다.
- ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공한다.
따라서, 모듈 내에 var 키워드로 선언한 변수는 더는 전역 변수가 아니고, window 객체의 프로퍼티도 아니다. - 다음과 같이 script 태그에 type="module" 어트리뷰트를 추가하면 로드된 자바스크립트 파일은 모듈로서 동작한다.
<script type="module src="lib.mjs"></script>
<script type="module src="app.mjs"></script>
- 브라우저가 ES6 모듈 기능 사용을 위해서는 트랜스파일링이나 번들링이 필요하기 때문에 아직은 Webpack 등의 모듈 번들러를 사용하는 것이 일반적이다.
let, const 키워드와 블록 레벨 스코프
var 키워드로 선언한 변수의 문제점
ES5까지는 var가 변수를 선언할 수 있는 유일한 키워드였다.
1. 변수 중복 선언 허용
- var 키워드로 선언한 변수는 같은 스코프 내에서 중복 선언이 가능하다.
- 초기화문이 있는 변수 선언문은 중복 선언이 되어 값이 변경될 수 있다.
- 초기화문이 없는 변수 선언문은 무시된다. (에러 발생 x)
var x = 1;
var y = 1;
var x = 100; // 변수 선언과 초기값 할당이 동시에 이루어지면 변수 중복 선언된다.
var y;
console.log(x); // 100
console.log(y); // 1
따라서, 동일한 이름의 변수가 선언되어 있는 것을 모르고 변수를 중복 선언했는데 값까지 변경된다면 의도치 않은 오류를 발생시킬 위험이 있다.
2. 함수 레벨 스코프
- var 키워드로 선언한 변수는 함수 레벨 스코프를 지원한다.
- var 키워드로 선언한 변수는 함수의 코드 블록만을 지역 스코프로 인정한다.
var x = 1;
if (true) [
var x = 10;
}
console.log(x); // 10
var i = 0;
for (var i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}
console.log(i); // 5
함수 레벨 스코프는 전역 변수의 가능성이 높아져 전역 변수가 중복 선언되는 경우가 많이 발생한다.
3. 변수 호이스팅
- var 키워드로 변수를 선언하면 변수 호이스팅에 의해 변수 선언문이 스코프의 선두로 끌어 올려진 것처럼 동작되어 변수 선언문 이전에 참조할 수 있다. var 키워드로 변수를 선언하면 런타임 이전에 자바스크립트 엔진에 의해 선언 단계와 초기화 단계가 한번에 진행되기 때문이다.
- 따라서, 할당문 이전에 변수를 참조해도 undefined를 반환한다. 초기화 단계에서 undefined 값으로 초기화 되기 때문이다. 그러므로 변수 선언문 이전에 변수에 접근해도 에러가 발생하지 않는다.
console.log(foo); // undefined
foo = 123; // 변수 값 할당
console.log(foo); // 123
// 변수 선언은 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 실행된다.
var foo;
이는 에러를 발생시키지는 않지만 프로그램의 흐름에 맞지 않고 가독성을 떨어뜨린다.
let 키워드의 특징
var 키워드의 단점 보완을 위해 ES6에서 let, const라는 새로운 변수 선언 키워드가 나왔다.
1. 변수 중복 선언 금지
- 이름이 같은 변수를 let으로 중복 선언하면 문법 에러(Syntax Error)를 발생시킨다.
2. 블록 레벨 스코프
- let 키워드로 선언한 변수는 블록 레벨 스코프를 따른다.
- 따라서, 모든 코드 블록(함수, if 문, for 문, while 문, try/catch 문 등)을 지역 스코프로 인정한다.
let i = 10; // 전역 변수
function foo() { // 함수 레벨 스코프
let i = 100;
for (let i = 1; i < 3; i++) { // 블록 레벨 스코프
console.log(i); // 1 2
}
console.log(i); // 100
}
foo();
console.log(i); // 10
3. 변수 호이스팅
- let 키워드로 선언한 변수는 런타임 이전에 자바스크립트 엔진에 의해 선언 단계가 먼저 실행되고, 초기화 단계는 변수 선언문을 만났을 때 실행된다.
- 따라서, 스코프 시작 지점 부터 변수 선언문을 만나 변수 초기화되기 전까지는 변수를 참조할 수 없다. 이 참조 불가능한 구간을 일시적 사각지대(Temporal Dead Zone: TDZ)라고 부른다.
// ------ 일시적 사각지대 --------
console.log(foo); // ReferenceError: foo is not defined
// ------ 일시적 사각지대 --------
let foo; // 변수 선언 (초기화 단계, undefined로 초기화한다.)
console.log(foo); // undefined
foo = 1; // 변수에 값 1을 할당한다. (할당 단계)
console.log(foo); // 1
🤔 그렇다면 let 키워드로 선언한 변수는 변수 호이스팅이 발생하지 않는가?
그렇지 않다. 실제로는 선언문을 호이스팅 하지만 호이스팅이 발생하지 않는 것처럼 동작한다.
let foo = 1; // 전역 변수
{
console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
let foo = 2; // 지역 변수
}
let으로 선언한 변수는 호이스팅이 발생하지 않는다고 가정하면 foo를 콘솔창에 출력했을 때 전역 변수의 1이라는 값이 출력되야 하는데 결과는 그렇지 않다.
콘솔창에는 초기화 이전에 변수에 접근했다는 참조 에러가 나타났다. 이때 지역 변수 foo는 let으로 선언한 변수로 런타임 이전에 자바스크립트 엔진에 의해 선언은 되었지만 초기화는 되지 않았기 때문에 이러한 에러가 발생하는 것이다.
이를 통해 let으로 선언한 변수도 호이스팅이 발생한다는 것을 알 수 있다.
4. let 키워드로 선언한 변수는 전역 객체의 프로퍼티가 아니다.
- let 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아니어서 전역 객체의 프로퍼티를 참조할 수 없다.
let x = 1;
console.log(window.x); // undefined
console.log(x); // 1
const 키워드의 특징
상수를 선언하기 위해 사용한다.
1. const 키워드로 선언한 변수는 반드시 선언과 초기화를 동시에 해야한다.
2. const 키워드로 선언한 변수는 재할당이 금지된다.
const foo = 1;
foo = 2; // TypeError: Assignment to constant variable
- const 키워드로 선언된 변수에 원시 값을 할당하면 할당된 값을 변경할 수 없다. 왜냐하면 원시 값은 변경 불가능한 값이고, const 키워드에 의해 재할당이 금지되기 때문이다.
- 일반적으로 상수의 이름은 대문자로 나타내고, 여러 단어로 이루어지면 언더 스코어(_)를 써서 스네이크 케이스로 표현한다.
const TAX_RATE = 0.1;
3. const 키워드로 선언한 변수에 객체를 할당하면 값을 변경할 수 있다.
- 변경 가능한 값인 객체는 직접 변경이 가능하다. 이때, 변수에 할당된 참조 값은 변경되지 않는다.
const person = {
name: 'Lee'
};
person.name = 'Kim';
console.log(person); // {name: 'Kim'}
var vs let vs const
- const 키워드를 사용하면 의도치 않은 재할당을 막기 때문에 안전하다. 따라서, 변수 선언 시 기본적으로는 const를 사용하자.
- 재할당이 필요한 경우에는 let 키워드를 사용하는 것이 좋다. 이때, 변수의 스코프를 좁게 만들어라.
'JavaScript > 모던 자바스크립트 Deep Dive' 카테고리의 다른 글
13. 일급 객체 (0) | 2022.07.31 |
---|---|
12. 생성자 함수에 의한 객체 생성 (0) | 2022.07.31 |
10. 함수 (0) | 2022.07.28 |
9. 원시 값과 객체의 비교 (0) | 2022.07.26 |
8. 객체 리터럴 (0) | 2022.07.25 |