Variable Declaration

MaxlChan·2020년 6월 23일
1

기존 변수 선언 키워드인 var를 포함하여
ES2015가 발표되어 도입된 변수 선언 키워드인 letconst의 차이를 구분해보자.

사실... 그냥 이 3가지 키워드에 대해서 집중적으로 블로깅 하려고 했으나, ScopeHoisting 설명없이 다루는 것은 불가능하다고 판단하여 먼저 위 두 가지 개념부터 차근차근 정리해보았다.

🚨 올바르지 않은 내용이 있을 경우 댓글로 남겨주시면 감사드리겠습니다.

Scope

1. 스코프(Scope)란?

Scope란 단어를 간단하게 번역하면 '범위'이다. 자바스크립트에서 스코프란 여러가지 설명이 있지만 코드에 접근할 수 있는 유효범위 정도로 설명된다. 간단히 아래 코드를 보자.

function foo() {
  var a = 1;
  console.log(a);
}

foo(); // 1
  1. foo라는 이름의 함수를 선언하고, foo함수를 실행한다.
  2. foo함수가 실행되면, a라는 변수를 출력하고자 a를 탐색한다.
  3. foo함수 블록 내부에 a라는 변수가 선언되어있고, 그 변수의 값은 1로 할당되어 있기 때문에 1을 출력한다.
function foo(b) {
  var a = 1;
  console.log(a + b);
}

foo(2); // 3
  1. foo라는 이름의 함수를 선언하고, foo함수를 매개변수에 2를 할당하여 실행한다.
  2. foo함수가 실행되면, (a + b)를 출력하고자 ab를 탐색한다.
  3. foo함수 블록 내부에 선언된 a라는 변수와 b라는 매개변수가 있고 각각 변수의 값은 1, 2로 할당 때문에 결국 3(1 + 2)를 출력한다.

여기서 알 수 있는 것은 foo함수 내에서 선언된 a라는 변수는 해당 함수 범위 안에서 언제든지 접근하여 사용할 수 있다.

즉, "변수 a는 foo라는 함수 스코프에 속해있다." 라고 표현할 수 있다.


스코프는 크게 2가지(전역 스코프, 지역스코프)로 구분될 수 있다.

  • 전역 스코프(Global Scope)

    코드의 모든 영역에서(전역)에서 접근하여 사용할 수 있는 범위를 말한다.
var a = "Global Scope"; /* 변수 a는 전역 변수라고 부릅니다. 
			전역 스코프에서 선언된 전역 변수 a는
            		모든 영역(함수 내부)에서 접근 가능합니다 */
function foo() {
  console.log(a);
}

function poo() {
  console.log(a);
}

foo(); // "Global Scope"
poo(); // "Global Scope"
  • 지역 스코프(Local Scope, Function-level 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

2. 스코프 체인(Scope Chain)

스코프는 하나의 상위 스코프-하위 스코프 계층을 만들고, 그 계층은 서로 연결되어 있다. 이것을 스코프 체인이라고 한다. 예시를 통해 먼저 알아보자.

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"
  1. parent 함수가 실행되면 직후에 child 함수가 실행됩니다.
  2. child 함수가 실행되면, a라는 변수를 콘솔에 출력하기 위해 a변수의 정보를 먼저 child 함수 스코프에서 탐색한다.
  3. a라는 변수는 해당 스코프에서 선언되지도, 새로 값이 할당되지도 않았기 때문에 a라는 변수를 찾지 못하고 상위 스코프인 parent 함수 스코프를 탐색한다.
  4. a라는 변수는 parent 함수 스코프에서 선언되지도, 새로 값이 할당되지도 않았기 때문에 a라는 변수를 찾지 못하고 상위 스코프인 전역(Global) 스코프를 탐색한다.
  5. 전역 스코프에서 선언된 변수 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

3. 렉시컬 스코프(Lexical Scope)

렉시컬 스코프란 번역하자면 '어휘적 범위 지정'이라고 해석되는데 이러면 이해가 잘 되지 않는다.
그냥 쉽게 말하자면 함수의 상위 스코프 등을 포함한 주변 환경은 해당 함수가 실행될 때가 아니라 선언될 때 결정되는 방식이 렉시컬 스코프라고 이해하면 되겠다. 아래 예시 코드를 보자.

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

호이스팅이란 한국말로 번역하면 '계양', '들어올림' 등으로 해석되어진다.

자바스크립트에서는 특정 스코프에서 변수를 선언하게 되면 해당 변수 선언이 해당 스코프에 최상단으로 끌어올려지게 되는데 이러한 현상을 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

Variable Declaration

ES2015 이전에 javascript에서 변수를 선언할 수 있는 키워드는 var가 유일했다. 하지만 var는 자바스크립트 코드안에서 여러가지 문제를 발생시킬 여지(변수 중복 선언 가능, var 키워드 생략 가능 등)가 다분한 관계로 자바스크립트를 코드를 보다 엄격하게 만들고자 ES2015에서let, const라는 변수 선언 키워드가 추가되었다. 앞서 소개한 Scope와 Hoisting 을 기준으로 차이점을 알아보자.

1. var

- 함수 레벨 스코프(Function-level scope)를 따른다.

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

- 호이스팅 시 undefined로 값이 초기화된다.

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

2. let

- 블록 레벨 스코프(Block-level scope)를 따른다.

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(일시적 사각지대) 라고 한다.

변수 선언문에 도달하기까지 해당 변수를 사용할 수 없다... 이 현상은 변수 선언이 정말 호이스팅 되는지 헷갈리게 만들 수 있다. 그러면 letconst가 정말 호이스팅 되는지는 어떻게 알 수 있을까?

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

3. const

const의 경우

  • 블록 레벨 스코프(Block-level scope)를 따르고
  • 호이스팅 시 값이 자동으로 초기화 되지 않고
  • 변수 재선언이 불가능하다.

라는 측면에서 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으로 변경하는 것이 의도치 않은 변수 재할당으로 인한 오류를 줄일 수 있는 효율적인 방법이라고 하니, 코드를 작성시 참고하면 좋을 것 같다.

참고

profile
한가지를 알아도 제대로 알자

0개의 댓글