탱구탱구 개발자 일기

이전 포스팅에서 자바스크립트 스코프에 대해 설명하면서 기본적으로 자바스크립트는 함수 레벨 스코프 규칙을 따르고 변수 선언 키워드가 var밖에 존재하지 않았다고 말했다. ECMAScript 6부터 자바스크립트도 letconst라는 새로운 변수 선언 키워드가 추가되었고 이들은 블록 레벨 스코프 규칙을 따른다. 각각의 스코프 규칙이 무엇인지도 이전 포스팅에서 확인할 수 있다.

 

[변수 선언 키워드]

 

자바스크립트 변수 선언 키워드는 다음과 같은 특징을 가진다.

- var let const
스코프 규칙 함수 레벨 블록 레벨 블록 레벨
재선언 O X X
재할당 O O X

 

[var]

var 키워드는 ECMAScript 6 이전까지 유일한 변수 선언 키워드로 함수 레벨 스코프를 따르는 특징을 가지고 있다. 이 키워드의 특징을 다음의 코드에서 살펴보자.

var a = 'red'; // 함수 밖에서 선언 시 전역 변수
function f() {
    console.log('f함수 a:', a); // red
    
    var b = 'blue'; // 함수 안에서 선언시 해당 함수 내부에서 접근 가능한 지역 변수
    console.log('f함수 b:', b); // blue
}

f();
console.log('외부 a:', a); // red
console.log('외부 b:', b); // Uncaught ReferenceError: b is not defined

전역 변수 a는 프로그램 어디에서든 접근 가능하다. 반면에 지역 변수 b는 해당 함수 밖에선 참조 오류가 발생한다.

또한 var로 선언된 변수에는 동일한 이름으로 재선언 및 해당 변수에 재할당이 가능하다.

// 재선언 및 재할당
var aa = 10;
console.log("sdfsdf");
function f2() {
    var aa = 20; // 변수 aa 재선언
    console.log('aa:', aa); // 20

    aa = 30; // 변수 aa 재할당
    console.log('aa:', aa); // 30
}

console.log('aa:', aa); // 10

f2();

// aa : 10
// aa : 20
// aa : 30

이렇게 var로 선언된 변수는 함수 레벨 스코프를 가지면서 자유롭게 선언 및 할당이 가능하기 때문에 프로그램이 고도화되면 디버깅의 어려움이나 가독성 저하 때문에 유지보수 측면에서 불리하고 메모리 누수도 발생한다.

 

이러한 문제점을 해결하기 위해서 블록 레벨 스코프의 필요성이 대두되었고 해당 스코프를 갖는 let, const 키워드가 등장했다.

 

[let, const]

 

let과 const 키워드는 블록 레벨 스코프 특성을 공통으로 지닌다. let이나 const 키워드도 함수 밖에서 선언하면 var 키워드가 갖는 함수 레벨 스코프처럼 전역 변수로 생성되어 프로그램 전역에서 접근할 수 있다. 하지만 블록 안에서 선언하게 되면 변수가 정의된 블록과 그 하위 블록에서만 접근할 수 있다.

 

블록 레벨 스코프를 설명하기 위한 예제이다. 예제는 let만 가지고 설명하겠다.

// let, const
// 블록 레벨 스코프

let a = 10;
function f1() {
    console.log('a:', a); ??
    let a = 20;
    console.log('a:', a); // a: 20
    let a = 30;

    if (true) {
        let b = 5;
        for (let i = 1; i < 5; i++) {
            b = b + 1;
        }
        console.log('b:', b); // b: 9
    }
    console.log('b:', b); // ??
}

f1();

console.log('a:', a); // a: 10

기본적으로 f1 함수 밖에서 선언된 let a는 전역 변수로 생성되어 프로그램 전역에서 접근 가능하다. 또한 f1 함수 내에 있는 if 조건문 블록 안에서 선언된 let b는 if 블록과 그 하위 블록인 for문에서 접근할 수 있고 if 문 밖에 있는 콘솔에선 b를 참조할 수 없다.

 

블록 레벨 스코프를 설명하기 위해 정의한 콘솔로그가 정상적으로 찍힌 것처럼 말했지만, 실제로 위 코드를 실행하면 첫번째부터 에러를 발생시킨다. 처음에 let으로 선언한 변수 a는 전역 변수로 등록되었다. 여기까지는 var와 차이점이 없다. 그러나 함수 f1을 호출하는데 여기서 var와 다른 let과 const의 특징 중 하나인 TDZ 제약이 나타난다.

 

let이나 const 키워드로 된 변수 선언문도 var와 마찬가지로 hoisting 된다. 그러나 var는 선언과 동시에 undefined로 초기화가 되지만 let, const는 그렇지 않다.

console.log('x:', x); // undefined
var x = 'global';
console.log('x:', x); // global

// 위 코드는 실제로 아래와 같이 실행된다.

var x; // 변수 x선언과 동시에 undefined 초기화
console.log('x:', x); // undefined 출력

x = 'global' // global 할당
console.log('x:', x); // global 출력

[let, const의 공통점 : TDZ]

 

let이나 const는 TDZ(Temporal Dead Zone)이라는 어휘적 바인딩이 실행되기 전까지 액세스 할 수 없는 특성을 갖고 있다. 여기서 어휘적 바인딩과 관련된 사항은 이 글을 참고하면 좋을 것 같다.

  • 어휘적 바인딩 : 객체가 렉시컬 환경(어휘적 환경)에 프로퍼티로 등록될 때 객체 이름을 key로 해당 객체의 참조값을 value로 key와 value를 한 쌍으로 묶는 것을 말한다.
// 어휘적 바인딩
// 변수명 = 변수값
var a = 10;

 

즉, let이나 consthosting이 되지만 변수가 생성될 때가 아니라 제로 어휘적 바인딩이 이루어지는 실행 코드를 만나서 초기화가 되기 전까지는 접근할 수 없다는 것이다. 따라서 Uncaught ReferenceError: Cannot access 'a' before initialization라는 참조 오류가 발생한 것이다. 물론 초기화와 관련해서 let과 const의 차이가 있는데 const를 설명할 때 말하겠다.

console.log('x:', x);
let x = 'global';

// 위 코드의 실제 실행 구조
let x; // 변수 x선언은 되지만 초기화가 안 됨
console.log('x:', x);

// 초기화 전에 접근해서
// Uncaught ReferenceError: Cannot access 'a' before initialization 발생 후 프로그램 중지

x = 'global' // 어휘적 바인딩이 실행되어 실제로 값을 할당 받음

 

변수 x가 정상적으로 출력되기 위해선 아래와 같이 코드가 변경되어야 한다.

let x = 'global';
console.log('x:', x);


// 위 코드의 실제 실행 구조
let x; // 변수 x선언
x = 'global'; // 어휘적 바인딩이 실행되어 변수가 초기화 됨

console.log('x:', x); // x: global

 

다시 예제 코드로 돌아와서 보면 첫 번째 콘솔에러는 TDZ에 의한 오류라는 것이 밝혀졌다.

function f1() {
    console.log('a:', a); // let a가 초기화되기 전에 a를 참조하므로 참조 오류 발생
    let a = 20;
    console.log('a:', a);
    let a = 30;

    if (true) {
        let b = 5;
        for (let i = 1; i < 5; i++) {
            b = b + 1;
        }
        console.log('b:', b);
    }
}

그러나 첫 번째 콘솔을 없애도 위 함수에서 Uncaught SyntaxError: Identifier 'a' has already been declared가 발생한다. 바로 let, const 키워드의 두 번째 특징인 재선언 불가능을 설명하겠다.

 

[let, const 공통점 : 재선언 불가능]

 

var 키워드로 된 변수는 얼마든지 같은 이름으로 재선언이 가능했다. 그러나 동일한 블록 레벨에서 let, const는 재선언이 불가능하다. 함수 f1 블록안에서 a라는 이름을 가진 변수가 두 번 선언되었다. 첫 번째로 20이란 값을 할당받은 a 변수는 문제가 없지만 다음에 30이란 값을 가지고 동일한 이름으로 변수를 만들려고 했기 때문에 아래와 같이  Uncaught SyntaxError: Identifier 'a' has already been declared라는 구문 오류가 발생한 것이다.

 

이러한 let, const의 특성은 유지보수 측면에서 변수를 남발하거나 실수로 같은 이름의 변수를 선언할 때 프로그램이 오작동하는 문제를 해결하였다.

function f1() {
    let a = 20;
    let a = 30; // Uncaught SyntaxError: Identifier 'a' has already been declared

    if (true) {
        let b = 5;
        for (let i = 1; i < 5; i++) {
            b = b + 1;
        }
        console.log('b:', b);
    }
}

[let, const 차이점 : 재할당 가능 여부]

 

let과 const 키워드는 동일한 블록 레벨 스코프를 지니지만 바로 재할당 여부에서 차이점이 있다. 먼저 let은 var와 동일하게 재할당이 가능하다.

// let 변수 재할당

let a = 10;
a = 20;
console.log('a:', a); // a: 20

function f3() {
    let b = 100;
    b = 200;
    b = 'string';
    console.log('b:', b); // b: string
}

f3();

그러나 const는 var, let과 다르게 재할당이 불가능하다. 재할당을 하게 되면 아래와 같이 Uncaught TypeError: Assignment to constant variable 오류가 발생한다.

// const 변수 재할당
const a = 100;
a = 200; // Uncaught TypeError: Assignment to constant variable
console.log('a:', a);

function f4() {
    const b = 1000;
    b = 'string'; // Uncaught TypeError: Assignment to constant variable
    console.log('b:', b);
}

f4();

 

[let, const 차이점 : 초기화 문제]

 

TDZ를 설명할 때 어휘적 바인딩은 key와 value를 묶는 것이라 말했다. 그렇다면 let, const 변수를 선언할 때 어휘적 바인딩에 초기화하는 구문이 없다면 어떻게 될까?? 아래 코드에서 확인해보도록 하자.

let x;
console.log('x:', x); // x: undefined

const x; // Uncaught SyntaxError: Missing initializer in const declaration
console.log('x:', x);

let 선언 변수의 어휘적 바인딩에 초기화 구문이 없으면 어휘적 바인딩을 실행할 때, 변수에 undefined가 할당된다.

그러나 const 키워드는 읽기 전용 변수이다. 즉, 재할당을 할 수 없기 때문에 값이 변하지 않는 상수를 선언할 때 사용하고 선언 시 초기화하지 않으면 Uncaught SyntaxError: Missing initializer in const declaration 에러가 발생한다.

 

그렇다면 여기서 var 키워드와 헷갈릴 수 있다. var 키워드도 undefined로 초기화된다고 말했는데 let도 undefined인데 왜 let 변수 선언 전에는 console.log 에러가 발생하냐..!!??

 

다시 한번 말해두지만 var 키워드는 선언과 동시에 undefined로 초기화가 되는 것이다. 그러나 let, const는 TDZ에 의해 어휘적 바인딩을 만날 때 값에 초기화가 되는 것이므로 호이스팅이 되더라도 어휘적 바인딩이 실행되지 않았고 초기화되지 않았기 대문에 참조 에러가 발생한 것임을 아래 코드를 통해 분명하게 기억하자! 

 

console.log(a);
let a;

// 위 코드의 실제 실행 구문

let a; // 호이스팅 됨
console.log(a); // 어휘적 바인딩이 실행되지 않았는데 접근 => 참조 에러

// 위 코드의 수정 코드
let a;
console.log(a);

let a;
a = ?? // 어휘적 바인딩 실행 시 초기화 구문이 없음

a = undefined; // undefined 할당
console.log(a); // undefined 출력

 

[결론]

 

자바스크립트의 함수 레벨 스코프 규칙 아래 var 변수 선언 키워드의 재선언 및 재할당으로 인한 문제점이 많았기 때문에 let, const 키워드가 등장한 이후로 var 변수는 권장하지 않는다. 실제로 내가 쓰는 IDE인 intelliJ에서도 var을 통해 변수를 선언하면 var' used instead of 'let' or 'const'와 같이 경고가 발생하면서 let, const를 쓰라고 권장한다.

이 글을 공유합시다

facebook twitter kakaoTalk kakaostory naver band
loading