기존 변수 선언 키워드인 var
를 포함하여
ES2015가 발표되어 도입된 변수 선언 키워드인 let
과 const
의 차이를 구분해보자.
사실... 그냥 이 3가지 키워드에 대해서 집중적으로 블로깅 하려고 했으나, Scope
와 Hoisting
설명없이 다루는 것은 불가능하다고 판단하여 먼저 위 두 가지 개념부터 차근차근 정리해보았다.
🚨 올바르지 않은 내용이 있을 경우 댓글로 남겨주시면 감사드리겠습니다.
Scope란 단어를 간단하게 번역하면 '범위'이다. 자바스크립트에서 스코프란 여러가지 설명이 있지만 코드에 접근할 수 있는 유효범위 정도로 설명된다. 간단히 아래 코드를 보자.
function foo() {
var a = 1;
console.log(a);
}
foo(); // 1
foo
라는 이름의 함수를 선언하고,foo
함수를 실행한다.foo
함수가 실행되면,a
라는 변수를 출력하고자a
를 탐색한다.foo
함수 블록 내부에a
라는 변수가 선언되어있고, 그 변수의 값은1
로 할당되어 있기 때문에1
을 출력한다.
function foo(b) {
var a = 1;
console.log(a + b);
}
foo(2); // 3
foo
라는 이름의 함수를 선언하고,foo
함수를 매개변수에2
를 할당하여 실행한다.foo
함수가 실행되면,(a + b)
를 출력하고자a
와b
를 탐색한다.foo
함수 블록 내부에 선언된a
라는 변수와b
라는 매개변수가 있고 각각 변수의 값은1, 2
로 할당 때문에 결국3(1 + 2)
를 출력한다.
여기서 알 수 있는 것은 foo함수 내에서 선언된 a라는 변수는 해당 함수 범위 안에서 언제든지 접근하여 사용할 수 있다.
즉, "변수 a는 foo라는 함수 스코프에 속해있다." 라고 표현할 수 있다.
스코프는 크게 2가지(전역 스코프, 지역스코프)로 구분될 수 있다.
var a = "Global Scope"; /* 변수 a는 전역 변수라고 부릅니다.
전역 스코프에서 선언된 전역 변수 a는
모든 영역(함수 내부)에서 접근 가능합니다 */
function foo() {
console.log(a);
}
function poo() {
console.log(a);
}
foo(); // "Global Scope"
poo(); // "Global Scope"
function foo() {
var a = "Local Scope"; /* 변수 a는 지역 변수라고 부릅니다.
지역 스코프에서 선언된 지역 변수 a는
foo 함수 내부에서만 접근 가능합니다 */
console.log(a);
}
function poo() {
console.log(a);
}
foo(); // "Local Scope"
poo(); // Uncaught ReferenceError: a is not defined
스코프는 하나의 상위 스코프-하위 스코프 계층을 만들고, 그 계층은 서로 연결되어 있다. 이것을 스코프 체인이라고 한다. 예시를 통해 먼저 알아보자.
var a = "global Scope";
function parent() { // parent 함수 스코프, global의 하위, child의 상위 스코프
var b = 0;
function child() { // child 함수 스코프, parent 함수의 하위 스코프
var c = 0;
console.log(a);
}
child();
}
parent(); // "global Scope"
parent
함수가 실행되면 직후에child
함수가 실행됩니다.child
함수가 실행되면,a
라는 변수를 콘솔에 출력하기 위해a
변수의 정보를 먼저child
함수 스코프에서 탐색한다.a
라는 변수는 해당 스코프에서 선언되지도, 새로 값이 할당되지도 않았기 때문에a
라는 변수를 찾지 못하고 상위 스코프인parent
함수 스코프를 탐색한다.a
라는 변수는parent
함수 스코프에서 선언되지도, 새로 값이 할당되지도 않았기 때문에a
라는 변수를 찾지 못하고 상위 스코프인전역(Global)
스코프를 탐색한다.- 전역 스코프에서 선언된 변수
a
의 값을 찾게되어, 해당a
값"global Scope"
을 콘솔에 출력하게 된다.
함수가 선언된 위치를 기준으로, 만약 해당 함수 스코프에서 원하는 정보를 찾지 못하며 원하는 정보를 찾을 때까지 스코프 체인을 통해 상위 스코프로 올라가 단계적으로 정보를 탐삭하게 된다. 만약 전역 스코프에도 해당 정보를 찾지 못한다면 결국 에러 (ex. Uncaught ReferenceError: a is not defined)가 발생할 것이다.
스코프 체인에 중요한 특징 중 하나는, 하위 스코프에서 상위 스코프에 대한 정보는 접근이 가능하지만, 상위 스코프에서 하위 스코프에 대한 내부 정보 접근은 불가능하다는 점이다. 아래 예시를 참고하면 되겠다.
function foo() {
var a = 1; // a 변수는 foo 함수 스코프의 지역 변수
function poo() {
var b = 1; // b 변수는 poo 함수 스코프의 지역 변수
}
console.log(b); // 내부 함수 poo 스코프에 대한 정보 접근 불가
}
console.log(a); // Uncaught ReferenceError: a is not defined
foo(); // Uncaught ReferenceError: b is not defined
렉시컬 스코프란 번역하자면 '어휘적 범위 지정'이라고 해석되는데 이러면 이해가 잘 되지 않는다.
그냥 쉽게 말하자면 함수의 상위 스코프 등을 포함한 주변 환경은 해당 함수가 실행될 때가 아니라 선언될 때 결정되는 방식이 렉시컬 스코프라고 이해하면 되겠다. 아래 예시 코드를 보자.
var a = "global";
function foo() {
var a = "Local";
poo();
}
function poo() { // 함수가 선언되는 위치를 기준으로 상위스코프가 결정
console.log(a);
}
foo(); // "global"
위 예제를 보면 poo
함수가 호출될 때가 아닌 선언될 때 상위 스코프가 결정되기 때문에 함수 내부에서 a
값은 상위 스코프인 글로벌 스코프를 탐색하여 해당 값을 출력하게 된다.
렉시컬 스코프는(Lexical Scope)는 정적 스코프(Static Scope)으로도 불리우며 반대 개념으로 해당 함수가 호출될 때 상위 스코프가 결정되는 동적 스코프(Dynamic Scope)가 있다.
일단 javascript는 렉시컬 스코프(정적 스코프) 방식으로 따르기 때문에, 동적 스코프에 대해서는 차후 다루어보도록 하겠다.
호이스팅이란 한국말로 번역하면 '계양', '들어올림' 등으로 해석되어진다.
자바스크립트에서는 특정 스코프에서 변수를 선언하게 되면 해당 변수 선언이 해당 스코프에 최상단으로 끌어올려지게 되는데 이러한 현상을 Hoisting이라고 한다.
여기서 눈여겨 봐야할 것은, 호이스팅되는 것은 변수 선언이며, 변수 할당은 호이스팅 되지 않는다는 것이다.
console.log(a); // undefined
var a = 1;
해당 구문은 실제로 아래와 같이 실행된다.
var a; // 변수 a를 선언
console.log(a); // undefined을 출력
a = 1; // 변수 a에 1이라는 값을 할당
변수 선언은 Global Scope내에서 선언되었기 때문에 Global Scope 최상단으로 이동하게 되고, 값이 할당되기 전이기 때문에 a
값은 undefined
로 이다.
호이스팅은 전역 스코프 뿐만 아니라 지역(함수) 스코프 내에서도 동일하게 일어난다.
var a = 1;
function foo() {
console.log(a);
var a = 5;
}
foo(); // ?
위 예제는 얼핏보면 foo함수 내에서 a
를 콘솔에 출력하는 문장 상단에 아무런 코드가 없기 때문에 해당 a
값을 함수 스코프에서 찾지 못하여 상위 스코프를 탐색하고 a
값인 1
을 출력할 것처럼 보이지만, 함수(지역) 스코프에서도 마찬가지로 호이스팅 현상이 일어나기 때문에, 실제로 아래와 같이 실행된다.
var a = 1;
function foo() {
var a;
console.log(a);
a = 5;
}
foo(); // undefined
ES2015 이전에 javascript에서 변수를 선언할 수 있는 키워드는 var
가 유일했다. 하지만 var
는 자바스크립트 코드안에서 여러가지 문제를 발생시킬 여지(변수 중복 선언 가능, var 키워드 생략 가능 등)가 다분한 관계로 자바스크립트를 코드를 보다 엄격하게 만들고자 ES2015에서let
, const
라는 변수 선언 키워드가 추가되었다. 앞서 소개한 Scope와 Hoisting 을 기준으로 차이점을 알아보자.
var
는 함수 레벨 스코프(Function-level scope)
를 따른다. 함수의 코드 블록만을 스코프로 인정한다. 즉, 함수 내부에서 선언된 변수는 함수 내부에서만 유효 범위를 가지며, 함수 외부에서 해당 변수에 접근할 수 없다.
function foo() {
var a = 3; // 변수 a는 foo함수의 지역 변수
}
console.log(a); // undefined
var b = 1;
if (b === 1) {
var c = 2; // 변수 c는 전역 변수
}
console.log(c); // 2
console.log(a); // undefined
var a = 1;
console.log(a); // 1
var a = 1;
console.log(a); // 1
a = 2;
console.log(a); // 2
var a = 3;
console.log(a); // 3
let
, const
는 블록 레벨 스코프(Block-level scope)
를 따른다. 모든 코드 블록(if문, for문, while문 등)을 스코프로 인정한다. 즉, 코드 블록 내부에서 선언된 변수는 코드블록 내부에서만 유효 범위를 가지며, 코드 블록 외부에서 해당 변수에 접근할 수 없다.
for (var i = 0; i < 5; i++) {
var a = 10; // 변수 a는 전역 변수, 변수 i도 전역 변수
}
for (let j = 0; j < 5; j++) {
let b = 10; // 변수 b는 지역 변수, 변수 j도 지역 변수 상위 스코프에서 접근 불가
}
console.log(i); // 5
console.log(a); // 10
console.log(j); // Uncaught ReferenceError: j is not defined
console.log(b); // Uncaught ReferenceError: b is not defined
console.log(a); // undefined
var a = 1;
console.log(a); Uncaught ReferenceError: a is not defined
let a = 1;
let
, const
의 경우 var
과 동일하게 변수 선언이 스코프 내 최상단으로 호이스팅 되는데 undefined
라는 값으로 자동 초기화되는 var
과는 달리 let
, const
는 호이스팅 될 때 값이 자동으로 초기화되지 않기 때문에 해당 선언문에 도달하기 전에 변수를 사용할 경우 ReferenceError가 출력되게 된다.
해당 선언문에 도달하고 나서야 변수의 초기값이 설정되고, 그 이후부터 변수에 접근할 수 있게 된다.
// Temporal Dead Zone Start
var b = 10;
var c = 15;
console.log(a); Uncaught ReferenceError: a is not defined
//Temporal Dead Zone End
let a = 1;
let
, const
를 통해 선언된 변수가 호이스팅되고 해당 스코프의 최상단에서부터
변수 선언문에 도달하는데 까지를 Temporal Dead Zone(일시적 사각지대) 라고 한다.
변수 선언문에 도달하기까지 해당 변수를 사용할 수 없다... 이 현상은 변수 선언이 정말 호이스팅 되는지 헷갈리게 만들 수 있다. 그러면 let
과 const
가 정말 호이스팅 되는지는 어떻게 알 수 있을까?
let a = 1;
{
console.log(a); //Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 3;
}
위 예제를 통해 알 수 있듯이 블록안에 a
변수는 전역 변수 a
를 참조하여 콘솔에 출력하려고 하는 것이 아니라 블록 스코프 안에 존재하는 지역 변수a
를 참조하려고 한다는 것을 알 수 있다.(즉 let a = 3
문에 도달하기 전에 이미 a
라는 변수가 스코프 안에 '존재'한다는 것을 인지하고 있다는 뜻) 즉 let
을 통한 변수 선언은 분명 블록 스코프 최상단으로 분명히 호이스팅 되었고, 단지 그 값이 자동으로 초기화되지 않았기 때문에(Cannot access 'a' before initialization) 접근이 불가능하여 ReferenceError가 발생했다는 것을 확인할 수 있다.
var a = 1;
console.log(a); // 1
a = 2;
console.log(a); // 2
let a = 3; // uncaught SyntaxError: Identifier 'a' has already been declared
const
의 경우
라는 측면에서 let
과 거의 동일하지만
- 변수 재할당이 불가능하다.
라는 점에서 차이가 발생한다.
const a = 3;
console.log(a); // 3
a = 2; // Uncaught TypeError: Assignment to constant variable.
변수의 값을 재할당하는 것이 불가능 할 뿐이지, 참조하고 있는 객체의 속성 변경은 가능하다.
const a = {
name : "chan",
age : 20
};
console.log(a.age); // 20
a.age = 30;
console.log(a.age); // 30
const b = [];
b[0] = 4;
b.push(5);
console.log(b); // [4, 5];
실제로 코드를 작성할 때 변수에 값을 재할당하는 경우는 생각보다 많이 발생하지 않기 때문에, 기본적으로 변수 선언 시 const
로 변수를 생성하고 코드를 작성하다가 변수를 재할당 할 필요가 있을 때 해당 변수 선언 키워드를 let
으로 변경하는 것이 의도치 않은 변수 재할당으로 인한 오류를 줄일 수 있는 효율적인 방법이라고 하니, 코드를 작성시 참고하면 좋을 것 같다.