티스토리 뷰

◆ 변수의 선언

(모던 자바스크립트 Deep Dive 책을 통해 공부한 글입니다.)

ES6가 나오기 이전에 변수를 선언하기 위해서는 var을 사용해야 했습니다.

var로 선언한 변수는 어떠한 문제점이 있고 특징이 있는지 살펴보고 letconst에 대해서 설명합니다.

 

◆ var 변수의 선언

var은 다음과 같이 변수를 선언합니다.

여러 타입을 변수에 넣을 수 있는데, 다음 예제와 같이 function과 같은 자바스크립트에서

이미 사용되고 있는 키워드는 변수 이름으로써 사용이 불가합니다.

var foo = 'this is foo';
var one = 1;

// 다음과 같은 function 키워드는 자바스크립트에서 함수로써 사용되고 있는 
// 키워드입니다. 따라서 변수 이름으로 지정할 수 없습니다.
var function = function(){}
// error : Uncaught SyntaxError: Unexpected token 'function'

 

var 변수 중복 선언 허용

var을 통해 선언한 변수는 같은 스코프 내에서는 중복 선언이 가능합니다.

다음과 같이 중복된 이름 foo 로 선언한 변수가 있습니다.

하지만 자바스크립트는 오류를 표출하지 않고 console.log 를 통해서 순서대로 할당한 값을 출력해줍니다.

var foo = 1;

console.log(foo);
// 출력 : 1

var foo = "test";

console.log(foo);
// 출력 : test

 

case2. var의 초기화문이 없는 중복 선언

그렇다면 다음과 같은 초기 선언에 초기화문을 할당해 주었지만, 

추후에 다시 선언한 값에는 할당안한 상황은 어떻게 될까요?

놀랍게도 초기화문이 없는 변수는 무시되며 초기 할당했던 값이 출력되는것을 확인할 수 있습니다.

var ffang = 1;

console.log(ffang);
// 출력 : 1

var ffang;

console.log(ffang);
// 출력 : 1

이와 같은 방식으로 예기치 못하게 중복으로 선언하고, 에러를 표출하지 않는 자바스크립트로 인해서 문제를 일으키게 되는 상황이 발생할 수 있습니다.

 

var 함수 레벨 스코프

var은 오로지 함수의 코드 블록만 지역 스코프로 인정합니다.

이러한 특성으로 인해 다음에서 소개되는 예기치 못한 상황이 발생되는것을 확인할 수 있습니다.

 

01. var의 if 문 에서의 블록 스코프

다음과 같은 어이없는 상황이 var을 사용하면 발생할 수 있습니다.

ffing이라는 변수는 전역 변수인데 if문의 코드블록에서 중복 선언된 변수의 값이 재할당 되어버렸습니다.

if문 내에 선언된 ffing 이라는 변수가 전역 변수가 되어버렸습니다.

var ffing = 1;

if (true) {
  var ffing = 10;
}

console.log(ffing);
// 출력 : 10

02. var의 for loop 문 에서의 블록 스코프

동일한 문제로 for loop 문에서 선언된 var 변수도 전역 변수가 됩니다.

의도하지 않았지만 for 문에서 실행된 i++로 인해 변수가 재 할당 되었습니다.

var i = 10;

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

console.log(i);
// 출력 : 5

03. var의 함수에서의 스코프

앞서 설명했듯이 함수에서의 var 중복 선언은 지역 변수의 역할을 해주기는 합니다...

var ffong = 1;

function ffongFunction() {
  var ffong = 10;
  console.log("scrope::", ffong);
  // 출력 : scope:: 10
}

ffongFunction();

console.log(ffong);
// 출력 : 1


var 변수 호이스팅

* 호이스팅이란 변수 선언문이 선두로 끌어 올려진 것처럼 동작하는 것을 말하는데, 

호이스팅의 자세한 내용은 다른 글에서 다뤄볼 예정입니다.

 

var을 통해 변수를 선언하면 변수 호이스팅에 의해서 선언되기 이전에 변수를 참조를 할 수 있게됩니다.

다만 할당하기 이전이라면 항상 undefined를 반환하게 됩니다.

console.log(hoisting);
// 출력 : undefined

var hoisting = 1;

위에서 부터 아래로 코드를 읽는 프로그램의 흐름상 위의 코드는 오류를 발생시키지는 않지만,

코드의 가독성이 저하되고 오류를 발생시킬 수도 있습니다.

 

let, const 키워드의 도입

이러한 여러 문제점을 통해 var 변수 선언문의 단점을 보완하기 위해서 ES6에서는 letconst가 나오게됩니다.

아래의 글에서는 let과 const를 var과 비교하며 설명한것을 바탕으로 정리하였습니다.

 

let 변수 선언문

var 변수 선언문과는 다르게 let 키워드를 통해서 변수를 선언하게 되면 문법 에러( syntaxError )가 발생하게 됩니다.

아래의 예제에서는 중복 선언한 let 변수에 의해서 이미 letFoo는 선언되었다는 에러가 발생합니다.

또한  var 선언문과 동일하게 let 변수를 선언과 동시에 초기화 시키지 않는다면 undefined가 할당되게 됩니다.

// 아래 두 varFoo의 선언은 오류를 발생시키지 않습니다.
var varFoo = 123;
var varFoo = 456;

// let 선언문은 같은 스코프 내에서 중복 선언을 할 수 없습니다.
let letFoo = 123;
let letFoo = 456;
// Uncaught SyntaxError: Identifier 'letFoo' has already been declared

let letValue;
console.log(letValue);
// 출력 : undefined

 

01. let 블록 레벨 스코프

앞서 설명한 var 은 함수만이 코드 블럭 스코프만을 지역 스코프로 인정하지만,

let모든 블록 코드( 함수, if 문, for 문, while 문, try/catch 문 등 )을 지역 스코프로 인정합니다. 

 

아래의 예제에서는 {} 를 통해서 코드 블록을 만들고 지역 변수를 확인해보았습니다.

전역으로 생성한 foo 변수는 console.log를 통해서 참조하여 출력이 가능하지만,

블럭 스코프 내에서 생성된 bar 변수는 지역 변수이므로 해당 스코프 내의 밖에서

참조하여 console.log를 출력하려 했지만 bar 변수는 선언되지 않았다는 오류를 발생시킵니다.

// 전역 변수
let foo = 1;

{
  // 지역변수
  let foo = 2;
  let bar = 3;
}

console.log(foo);
// 출력 : 1
console.log(bar);
// 출력 : Uncaught ReferenceError: bar is not defined

 

case2. 블록 레벨 스코프의 중첩

다음 예제와 같은 재밌는 코드 블럭의 중첩이 있습니다.

전역 변수로 선언된 i 에는 0을 할당, 함수 firstScope 스코프 내에서 선언된 i에는 100을 할당,

for 문 스코프 내에서 선언된 i에는 0을 할당하고 i++로 3번 반복합니다.

다음과 같이 선언되면 각각의 i는 각자의 스코프에서 동작해서 i는 각각 값이 다르게 출력이 됩니다.

let i = 0;
function firstScope() {
  let i = 100;

  for (let i = 0; i < 3; i++) {
    // 다음 i는 for문의 블록 스코프에 의해 for문 내에서 생성된 i 변수를 참조합니다.
    console.log(i);
    // 출력 : 0 1 2
  }

  console.log(i);
  // 출력 : 100
}

firstScope();

console.log(i);
// 출력 : 0

 

02. let 변수 호이스팅

let은 var과 달리 변수 호이스팅이 발생하지 않는 것처럼 동작합니다.

다음 예제에서는 선언되기 이전에 let 변수를 참조하려 했을때 참조 에러( ReferenceError )가 발생하게 됩니다.

// letHoisting을 아직 선언이 되지 않은 시점에서 참조 및 console.log로 출력하려고 하면,
// 선언 전에는 접근할 수 없다는 에러 메세지를 볼 수 있습니다.
console.log(letHoisting);
// 출력  : Uncaught ReferenceError: Cannot access 'letHoisting' before initialization

let letHoisting = 0;

 

다음의 let 변수의 생명 주기를 통해서 왜 let은 변수 호이스팅이 발생하지 "않는 것처럼" 동작하는지 알아보겠습니다. 

 

03. let vs var 변수의 생명 주기

1. var 의 생명주기

선언단계 -> 초기화 단계 -> 할당 단계

var 변수 선언은 런타임 이전에 자바스크립트 엔진에 의해서 암묵적으로 "선언 단계""초기화 단계" 가 

한번에 진행됩니다.

 

var 변수는 선언 단계에서 스코프에 변수 식별자를 등록해서 자바스크립트 엔진에 변수의 존재를 알립니다.

그리고 선언 단계에서 할당이 된 값이 없으면 그 즉시 초기화 단계에서 undefined로 변수를 초기화 하게 됩니다.

블록 스코프는 함수의 조건에서만 성립되고, 나머지 블록 스코프는 무시된다 했으니

해당 스코프 내에서 선언 된 var 변수에 한해서는 선언되기 이전에 참조를 하거나 console.log로 확인을 해보면

undefined로 출력이 됩니다. 

 

그리고 할당 단계를 통해서 값을 할당하게 되면 그 이후에 참조하게 되면 할당 된 값을 사용할 수 있습니다.

 

2. let 의 생명주기와 일시적 사각지대, 그리고 호이스팅의 재언급

선언단계 -> 일시적 사각지대(TDZ) -> 초기화 단계 -> 할당 단계

let 으로 선언한 변수는 "선언 단계""초기화 단계"가 분리되어 진행됩니다.

런타임 이전에 자바스크립트 엔진에 의해서 선언 단계가 먼저 실행되지만 초기화 단계는

변수 선언문에 도달했을 때 실행 되며, 변수의 선언문 이전까지는 변수를 참조할 수 없습니다.

 

2-1. 일시적 사각지대 TDZ( Temporal Dead Zone )

선언 단계초기화 단계 사이에 있는 구간을 일시적 사각 지대 TDZ라고 부르는데, 

위에서 설명한 초기화 단계의 시작인 변수의 선언문 이전까지 참조를 할 수 없는 구간입니다.

 

let foo = 0;

{
  console.log(foo);
  // 아래의 출력 메세지를 확인해보면 foo는 선언 전에 접근이 불가능하다는 ReferenceError를
  // 출력하고 있습니다.
  // 출력 : Uncaught ReferenceError: Cannot access 'foo' before initialization
  let foo = 1;
}

 

let으로 선언한 변수는 변수 호이스팅이 발생하지 않는 것 처럼 보이는데, 사실 let은 호이스팅이 발생합니다.

분명 let을 통해서 변수가 선언되기 이전에 참조를 할 수 없다고 했었는데 호이스팅이 발생한다는 것은

앞 뒤 말이 안 맞는것 같습니다.

 

2-2. 사실 호이스팅이 되고 있는 let , 그리고 모든 선언들

블록 스코프로 인해서 선언된 foo의 지역변수를 먼저 console.log를 통해서 참조하려 하면

다음과 같은 에러 메세지를 확인할 수 있습니다.

Uncaught ReferenceError: Cannot access 'foo' before initialization

이 에러는 초기화(initialization) 전에 접근(access)이 불가능하다는 참조 에러 메세지 입니다.

해당 에러의 메세지로 알 수 있는것은 해당 스코프 내에 선언되기 전에 참조를 했지만,

자바스크립트 엔진은  참조할 변수가 선언이 되었다는 것을 알고 있고 초기화 전에 참조를 하려해서

참조 에러를 발생시킨 것입니다.

 

따라서 호이스팅이 되지만 TDZ에 의해서 변수의 선언문이 작성된 초기화 단계 전까지는 참조를 할 수 없다고

에러가 발생하게 됩니다.

자바스크립트에서는 모든 선언(var, let, const, function, class 등)을 호이스팅합니다.

이중 ES6에서 도입된 let, const, class 선언문은 호이스팅이 발생하지 않는 것처럼 동작합니다.

 

다음의 예제를 보면 확실한 이해가 되실 수 있습니다.

{
  // 이전의 호이스팅 예제와 달리 let 선언문이 없는 상태에서는 
  // foo가 선언이 되지 않았다는 에러 메세지가 출력됩니다.
  console.log(foo);
  // Uncaught ReferenceError: foo is not defined
}

 

04. 전역 객체와 let

이 내용을 공부하기 전까지는 저는 전혀 몰랐던 사실이 있었습니다.

바로 var로 선언한 전역변수와 전역 함수, 선언하지 않은 변수에 값을 할당한 암묵적 전역은

전역 객체인 window 객체의 프로퍼티(property)가 됩니다.

또한 window를 생략하고 전역 객체의 프로퍼티를 참조할 수 있습니다.

var globalVar = 0;

function globalFunction() {
  console.log("globalFunction");
}

// 다음과 같이 window를 통해서 참조가 가능합니다.
console.log(window.globalVar);
// 출력 : 0
console.log(window.globalFunction);
/* 출력 : 
  ƒ globalFunction() {
    console.log("globalFunction");
  }
*/

 

하지만 let으로 선언한 변수는 전역 객체인 window의 프로퍼티가 아닙니다.

이 말은 다음과 같이 선언된 let 변수를 window 객체를 통해서 참조할 수 없다는 말입니다.

let globalLet = 0;

console.log(window.globalLet);
// 출력 : undefined

let 전역 변수(마찬가지로 const도 포함)는 보이지 않는 개념적인 블록 내에 존재하게 되어서 window 전역 객체를 통해서 접근이 가능하지 않습니다.

 

◆ const 변수

const 키워드는 constants의 줄임말로 상수를 선언하기 위해서 있는 변수 선언문입니다.

하지만 const로 선언된 변수는 반드시 상수만을 위해서 선언되지는 않습니다.

이 말은 불변, 재할당, 객체( 동적으로 생성, 삭제, 변경을 통해서 값이 변경이 될 수 있습니다. ) 라는 키워드와 연관이 있는데 후에 다뤄보도록 하겠습니다.

 

01. const 변수의 선언과 초기화

const 변수의 선언은 여느 var, let 변수 선언문과는 달리 선언과 동시에 초기화가 이루어져야 합니다.

이유는 const 변수의 재할당 금지 특징으로인해 선언 이후 값을 재할당하는것이 불가능 하기 때문입니다.

이 규칙을 지키지 않으면 다음 예제에서 notFoo를 const 변수 선언문으로 초기화를 안 했을때 일어나는 에러를

직면하실 수 있습니다.

// 다음과 같이 선언과 동시에 초기화 해주어야 합니다.
const foo = 1;
console.log(foo);
// 출력 : 1

const notFoo;
// Uncaught SyntaxError: Missing initializer in const declaration

 

02. const 변수의 호이스팅

const 변수도 앞서 위에서 let과 같이 설명한대로 호이스팅이 되지만 호이스팅이 발생하지 않는 것처럼 동작합니다.

마찬가지로 블록 레벨 스코프를 가지며 호이스팅은 동일 스코프 내에서 일어납니다.

하지만 TDZ에 의해 변수의 선언 이후에 참조가 가능합니다.  

{
  console.log(constScope);
  // Uncaught ReferenceError: Cannot access 'constScope' before initialization
  const constScope = "test";
  // 다음 console.log로 찍은 출력문은 로직 상 에러가 발생해서 출력이 되지 않지만, 
  // 만약 변수 선언 전에 출력한 console.log가 없었다면 "test" 가 출력 되었을 것입니다.
  console.log(constScope);
}

 

03. const 변수의 재할당 금지

const로 선언한 변수는 재할당이 금지되어 있습니다.

다음과 같이 콘솔에는 상수 변수에 할당하는 과정에서 TypeError가 발생했다고 표시됩니다.

const foo = 1;

foo = 2;
// Uncaught TypeError: Assignment to constant variable.

 

 

04. constants 상수

 

위에서 언급한 재할당 불가능하다는 것은 어떤 말일까요?

const로 선언한 변수에 원시 값( 최초로 초기화한 값 )을 할당하면 변경할 수 없습니다.

여기서 원시 값은 변경 불가능한 값( immutable value )으로 상수를 표현하곤 하는데,

상수는 재할당이 금지된 변수를 말합니다. 상수도 메모리상 값을 저장해야 하므로 변수라고 할 수 있습니다.  

 

case01. 상수의 활용

상수를 활용할 수 있는 방법은 많습니다.

상태 유지, 가독성, 유지보수의 편의 등으로 사용하곤 합니다.

 

다음과 같이 여러 페이지( 편의상 main페이지와 sub main페이지만 표출 했지만

여러 페이지가 있다고 가정합시다. )가 있는 웹 페이지가 있습니다.

// main
console.log(`Wow : 안녕하세요. 반갑습니다.`);

// sub main
console.log(`wow : 서브 페이지입니다.`)

해당 페이지에서 공통적으로 표시되는 Prefix 메세지가 있습니다. 위와 같은 상황에서는 Wow라는 메세지겠네요.

Wow라는 공통된 메세지는 자주 바뀌는 메세지라고 가정해봅니다.

페이지 수가 20개 이상이거나 많다면 Wow라는 메세지를 하나하나 다 바꿔야 할 것입니다.

 

다음과 같이 코드를 바꾸게 되면 어떻게 될까요?

const COMMON_PREFIX_MESSAGE = "Wow";

// main
console.log(`${COMMON_PREFIX_MESSAGE} : 안녕하세요. 반갑습니다.`);
// 출력 : Wow : 안녕하세요. 반갑습니다.

// sub main
console.log(`${COMMON_PREFIX_MESSAGE} : 서브 페이지입니다.`);
// 출력 : Wow : 서브 페이지입니다.

훨씬 해당 변수의 이름을 통해서 용도를 확실히 파악하기 쉬워져 가독성이 높아졌습니다.

그리고 크게는 유지보수가 하기 쉬워졌다는 큰 변화가 있었습니다.

 

위의 경우에서 확인할 수 있듯이 일반적으로 상수는 대문자와 언더스코어(_)를 활용

스네이크 케이스로 표현하는게 대부분입니다.

이러한 코드 스타일 규칙을 통상적으로 정하는 것을 코드 컨벤션(convention) 이라고 지칭하며 

규칙을 정하게 되면, 다른 사람들도 쉽게 이해할 수 있도록 가독성 있게 작성할 수 있습니다.

이 밖에도 파스칼 케이스, 카멜 케이스 등 여러가지 스타일이 있으니 확인하고 적절한 스타일을 적용하는 것을

추천드립니다.

 

05. const 와 객체

초반에 설명된 "반드시 상수만을 위해서 선언되지는 않습니다." 라는 내용은 객체를 통해서 설명이 가능합니다.

const 변수 선언문의 큰 특징인 "const로 선언된 변수에 원시 값을 할당한 경우에는 값을 변경할 수 없다."라는

내용을 기억하시나요?

 

위 말을 무시해버리는 상황이 있습니다. 바로 const에 객체 값을 할당한 경우입니다.

const car = {
  speed: 10,
};

console.log(car);
// 출력 : {speed: 10}

// 객체로 원시 값이 설정된 상수는 객체 값을 변경 하게되면 변경이 됩니다.
car.speed = 1000;

console.log(car);
// 출력 : {speed: 1000}

 

const 변수로 선언한 변수는 불변을 보장하지 않습니다. ( 이 내용은 react의 useState를 통한 상태 관리에서도 나오는 키워드인 불변성과 연관이 있습니다. ).

객체를 통한 프로퍼티 생성, 삭제, 변경( 객체를 통한)은 가능하며 변경이 되더라도 할당된 참조 값은 변경되지 않습니다.

 

 

정리해볼까요?

( 주관적인 내용으로 답변이 작성되어 있을수도 있습니다.

혹여나 오류가 있는 내용은 댓글을 통해서 알려주시면 감사하겠습니다.)

 

- 변수에서의 호이스팅이 뭔가요?

변수 선언문이 선두로 끌어올려진 것처럼 동작하는 것을 말합니다.

 

- 각 변수 선언문에서의 블록 레벨 스코프는 어떻게 동작하나요?

var 변수 선언문은 오로지 function 함수에 의해서만 블록 스코프가 동작합니다.

function에 의한 블록 스코프는 중첩이되며,

let과 const 변수 선언문은 모든 코드 블록이 스코프되며 중첩도 됩니다.

 

- var과 let, const 변수 선언문은 호이스팅이 일어나나요?

모든 변수 선언문은 호이스팅이 일어납니다. var은 선언과 동시에 초기화 단계가 진행되며 동일한 스코프 내에 값이 할당되기 이전에 참조되면 undefined 값이 할당된 것처럼 나오며, let과 const도 호이스팅은 되지만 일시적 사각지대(TDZ)인 변수의 선언이 되는 시점인 초기화 단계 전 참조가 되면 ReferenceError라는 참조 에러가 발생합니다. 따라서 블록 레벨 스코프에 의한 호이스팅으로 실행 주기를 파악하기 어려운 var을 사용하기 보다 let과 const를 사용해서 실행 주기를 확실히 파악해서 안전한 코딩을 해야합니다.

 

- let과 const의 큰 차이점은 무엇일까요?

let은 재할당이 가능하며 const는 재할당이 금지되어 있습니다.

하지만 const는 재할당이 금지되어 있는 것이지, 불변이라는 말은 아닙니다. 

const의 변수가 객체로 할당되어 있으면 property 생성, 삭제, 변경 ( property )을 통한 값의 변경은 가능합니다.

하지만 객체가 변경되어도 변수에 할당된 참조 값은 변경되지 않습니다.

 

 

마치며...

javascript의 변수 선언문인 var, let, const를 살펴 보았습니다.

var의 이후 let과 const가 생겨난 이유와 그 용도를 분명하게 알 수 있었습니다.

let 과 const를 적절히 사용하여 안전한 코딩을 하길 바랍니다.

 

 

 

 

 

출처

책 : 모던 자바스크립트 Deep Dive

댓글
최근에 올라온 글