본문 바로가기
나의 개발일지/javascript

[javascript | JS] var, let, const 변수의 생성과정, 호이스팅, TDZ, 스코프, 클로저와 은닉화(캡슐화)

by stella_gu 2022. 7. 19.

var, let, const

if (true) {
  var x = 3;
}

console.log(x);		// 3

if (ture) {
  const y = 3;
}

console.log(y);		// Uncaught ReferenceError: y is not defined

x는 정상적으로 출력 되는데 y는 에러가 발생하는 이유는?

var name = 'Mike';
console.log(name);	// Mike

var name = 'Jane';
console.log(name);	// Jane
let name = 'Mike';
console.log(name);	// Mike

let name = 'Jane';	// error!
console.log(name);	// Uncaught SyntaxError: Identifier 'name' has already been declared

var는 문제가 되지 않는데 let은 에러가 나는 이유는?

→ 변수의 생성과정, 스코프, TDZ에 대해 알아보자.

 

  • 변수의 생성 과정
    1. 선언 단계
    2. 초기화 단계
    3. 할당 단계
  • var의 변수 생성 과정
    1. 선언 및 초기화 단계 (동시에)
    2. 할당 단계
  • let의 변수 생성 과정
    1. 선언 단계
    2. 초기화 단계
    3. 할당 단계
  • const의 변수 생성 과정
    1. 선언 + 초기화 + 할당

 

TDZ(Temporal Dead Zone) : 스코프의 시작 지점부터 초기화 시작 지점까지의 구간

출처 : https://dmitripavlutin.com/javascript-variables-and-temporal-dead-zone/

스코프의 시작 지점부터 초기화 전까지 Temporal Dead, 말 그대로 죽어버린다.
→ 초기화 전에 엑세스를 하면 에러가 나는 이유는 TDZ의 영향을 받기 때문.

→ 코드와 var, let, const의 변수 실행 과정 차이를 보며 어떤 의미인지 자세히 알아보자.


호이스팅(hoisting) : 스코프 내부 어디서든 변수 선언은 최상위에 선언된 것처럼 행동함.

▶ var로 선언한 변수는 코드가 실제로 이동하진 않지만 코드가 끌어 올려진 것처럼 동작.

▶ 단, 선언(name)은 호이스팅 되지만, 할당('Mike')은 호이스팅 되지 않음.
▶ var의 변수 생성 과정에서 선언과 동시에 초기화가 일어나므로 초기화 되기 전에 액세스를 하면 일어나는 referenceError가 발생하지 않음.
▶ 할당은 3번째 줄에서 처리.
→ 에러가 나지 않지만 콘솔창에는 undefined가 찍힘.

▶ var는 호이스팅 되고, 변수 선언과 동시에 초기화가 되므로 a 변수가 호이스팅 될 때 초기화도 동시에 일어나 'undefined'가 됨.

▶ 첫 번째 console.log(a)는 undefined가 되고, 두 번째 줄 a = 1로 a의 값을 할당한 후 네 번째 줄에서 나온 console.log(a)의 값은 1이 나옴.

 

▶ let의 경우에도 호이스팅이 일어나지만 호이스팅은 스코프 단위로 일어남.

  • 위의 경우, age의 값으로 30이 찍히는 것이 아닌, 함수 스코프 내에 위치한 let age = 20;의 호이스팅이 일어나서 레퍼런스 에러가 남. (ReferenceError: Cannot access 'age' before initialization)
  • let의 변수 선언 과정을 보면, 선언과 초기화가 따로 일어남. → 호이스팅 되며 선언만 될뿐, 초기화가 일어나지 않음 → 초기화 되기 전에 액세스를 하면 일어나는 referenceError가 발생. (let age =20;이 호이스팅 되었더라도 초기화가 일어나지 않음. 초기화 되기 전 consolo.log로 age에 엑세스를 한 것이므로 에러 발생)
  • cf) 함수 내에 let age = 20;이 없었다면, 호이스팅이 일어날 게 없어 함수 외부의 let age = 30;을 참조하여 console창에 30을 찍음. (스코프 내부에 있다면 그걸 참조하고 없다면 외부의 전역 스코프를 참조하므로)

즉, let 또한 호이스팅이 되지만 선언만 되고 초기화가 일어나지 않으므로

TDZ 구간에 의해 메모리가 할당이 되질 않아 참조 에러(ReferenceError) 발생

( TDZ 구간 : a가 초기화 되기 전까지 넌 a 접근 못 해!)



※ const의 경우 let과 비슷하지만, 선언+초기화+할당이 동시에 일어나므로 값의 재할당도 불가.

▶ const의 경우 선언과 동시에 초기화 + 할당이 일어나므로 값을 할당 안 해주면 바로 에러 발생.
▶ const gender; 선언 후 gender = 'male';로 값을 할당 해주는 것 불가능. const gender; 자체가 불가능.



※ 그렇다면 const와 let 간의 차이는?

const a = 0;
a = 1;		// Uncaught TypeError: Assignment to constant variable.

let b = 0;
b = 1;		// 1

const c; 	// Uncaught SyntaxError: Missing initializer in const declaration
  • const는 한 번 값을 할당하면 다른 값을 할당할 수 없고, 다른 값을 할당할 수 없다.
  • 또한, 초기화 할 때 값을 할당하지 않으면 에러가 발생한다.
  • 따라서 const로 선언한 변수를 상수라고 부르기도 한다.
const a = { name: "jane", age: 13 };
a.name = "john";
console.log(a.name);  // john
  • 단, 원시 값이 아닌 객체의 경우 값의 재할당 가능

 

 

 

스코프

1. 함수 스코프 : var / 함수만이 유일하게 벗어날 수 없는 스코프 (함수 내에서 선언된 건 함수 내에서만)
2. 블록 스코프 : let, const / 코드 블럭 내에서 선언된 변수는 코드 블럭 내에서만 유효. 외부에서 접근 불가. (지역변수)

  • 코드블럭 : 함수, if문, for문, while문, try/catch문 등

 

▶ var는 함수 스코프의 영향을 받아 함수 안에서 선언된 var b =1을 불러오지 못 함. (ReferenceError)

▶ 하지만 var는 함수만 지역변수로 호이스팅이 되고 나머지는 다 전역변수로 올려버려, 위처럼 for문 안의 var i를 for문 밖에서 호출해도 에러가 나지 않음. 

▶ var는 심지어 변수 재선언 가능. 마치 똑같은 주민번호를 가진 사람이 또 있는 것. 

  그래서 let이 새로 생김.

 

 

function doSomething(someVal) {
  // Function scope
  typeof variable; // => undefined
  if (someVal) {
    // Inner block scope
    typeof variable; // throws `ReferenceError`
    let variable;
  }
}
doSomething(true);

이 코드는 2개의 스코프를 가진다.

  1. 함수 스코프
  2. let 변수가 선언된 내부 블록 스코프
  • typeof 연산자는 변수가 현재 스코프 안에 선언되었는지 확인할 때 유용하다.
  • 함수 스코프 내의 typeof variable;은 선언된 variable 변수가 없으므로 콘솔에 undefined가 찍힌다. 이때, if 내부의 let variable;은 영향을 주지 않는다.
  • if 안의 inner block scope에서 typeof variable;은 let variable;의 TDZ가 영향을 주어 RefenceError가 발생함.

 

렉시컬 스코프(Lexical scope)

렉시컬 스코프(Lexical scope)는 보통 동적 스코프(Dynamic scope)와 많이 비교한다.

위키피디아를 보면 동적 스코프와 렉시컬 스코프를 다음과 같이 정의하고 있다.

  • 동적 스코프
    The name resolution depends upon the program state when the name is encountered which is determined by the execution context or calling context.
  • 렉시컬 스코프 (정적 스코프(Static scope) 또는 수사적 스코프(Rhetorical scope))
    The name resolution depends on the location in the source code and the lexical context, which is defined by where the named variable or function is defined.

동적 스코프는 프로그램의 런타임 도중의 실행 컨텍스트나 호출 컨텍스트에 의해 결정되고, 렉시컬 스코프에서는 소스코드가 작성된 그 문맥에서 결정된다. 현대 프로그래밍에서 대부분의 언어들은 렉시컬 스코프 규칙을 따르고 있다.

동적 스코프와 렉시컬 스코프는 자바스크립트와 Perl을 비교하여 확인할 수 있다. 아래는 자바스크립트와 Perl로 같은 코드를 작성하였을 때 나오는 결과이다.

 

  • 자바스크립트는 렉시컬 스코프 규칙을 통해 global, global을 출력하였으며, Perl은 동적 스코프 규칙을 통해 local, global을 출력하였다. (참고로 Perl에서 local대신 my키워드를 사용하면 변수의 유효범위를 제한하여, 자바스크립트와 같은 결과를 얻을 수 있다.)
  • foo()가 호출 되면 function foo() { var x = 'local'; ~~을 통해 전역 스코프의 x의 값이 재할당 되는 게 아니라 var의 특성에 의해 foo() 함수 스코프 내에 var x = 'local'이 새로 선언된 것으로, var는 마치 주민번호가 똑같은, 즉 똑같은 변수 x가 생성되어도 에러가 나지 않는다. (멍충이..)
  • 렉시컬 스코프 규칙을 따르는 자바스크립트의 함수는 호출 스택과 관계없이 각각의 (this를 제외한)대응표를 소스코드 기준으로 정의하고, 런타임에 그 대응표를 변경시키지 않는다. (사실 런타임에 렉시컬 스코프를 수정할 수 있는 방법들(eval, with)이 있지만, 권장하지 않는다.)

→ 하지만 위에서 function foo() 내부의 'var'를 빼버리면?

var x = "global";

function foo() {
  x = "local";
  bar();
}

function bar() {
  console.log(x);
}

bar();	// global
foo();	// local
  • bar();를 호출하면 function bar() { console.log(x); }가 호출되어 x를 찾아 콘솔에 찍는다. 이때 함수 스코프 내에 x가 없으므로 외부 전역 스코프로 나가 찾게 된다.
  • 다음 foo();를 호출하면 function foo()가 호출되면서 x = 'local';이 x 변수가 있는지 찾는다 → 함수 내부에 없으니 전역 스코프로 나가 x가 선언 된 걸 찾는데, 위의 경우 var x = 'global';이 선언돼 있으므로 var의 특성에 따라 x의 값을 'local'로 재할당한다.
  • 다음 bar();를 호출하여 console.log(x);가 호출 되면, 재할당 된 x의 값인 local이 콘솔에 찍히게 된다.
var x = "global";

function foo() {
  x = "local";
  bar();
}

function bar() {
  console.log(x);
}

foo();
bar();
  • 참고로 위와 같이 foo();호출 후 bar();를 호출하면, foo();를 호출 할 때 x 값의 재할당이 이루어져 이때 이미 x = 'local'로 변해있는 상태라, 다음 bar();를 호출해 console.log(x);를 하게 되면 'local'이 찍힌다.

 

 

 

 

함수 선언식 vs 함수 표현식

1. 함수 선언식 - Function Declarations

 

2. 함수 표현식 - Function Expressions

 

 

함수 선언식과 표현식의 차이점

함수 선언식은 호이스팅에 영향을 받지만, 함수 표현식은 호이스팅에 영향을 받지 않는다.

// 실행 전
logMessage();
sumNumbers();

function logMessage() {
  return 'worked';
}

var sumNumbers = function () {
  return 10 + 20;
};
  • 함수 선언식은 호이스팅이 일어나, 첫째줄 logMessage()의 경우 에러 없이 실행이 되어 'worked'를 리턴한다.
  • 함수 표현식은 호이스팅이 일어나지만 함수의 호이스팅이 아닌, var sumNumbers 변수의 호이스팅이 일어나며 뒤의 함수는 빠진, var sumNumbers의 변수 선언과 초기화가 일어난다. (아래와 같이 인식함)
/ 실행 시
function logMessage() {
  return 'worked';
}

var sumNumbers;

logMessage(); // 'worked'
sumNumbers(); // Uncaught TypeError: sumNumbers is not a function

sumNumbers = function () {
  return 10 + 20;
};

  • sumNumbers() 함수를 호출하였지만, 호이스팅 된 sumNumbers는 변수이므로 TypeError가 발생한다.
  • 가장 아래의 sumNumbers = function() { ~~ 으로 함수값 할당이 일어나야 함수로 인식 되고 이 이후에 함수를 호출하여야 에러가 발생하지 않는다.

→ 그러므로 함수는 가급적 코드 상단부에서 선언하자.

 

 

※ 그렇다면 함수 표현식을 쓰는 이유는? 

→ 클로져로 사용 / 콜백으로 사용

 

 

 

클로저(closure)

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.

https://meetup.toast.com/posts/86 - 이걸 보고 클로저가 이해 되었다. 아래 [참고]에도 있음.

자바스크립트의 클로저

  • 자바스크립트에서 클로저는 함수가 생성되는 시점에 생성된다.
    = 함수가 생성될 때 그 함수의 렉시컬 환경을 포섭(closure)하여 실행될 때 이용한다.

따라서 개념적으로 자바스크립트의 모든 함수는 클로저이지만, 실제로 우리는 자바스크립트의 모든 함수를 전부 클로저라고 부르지는 않는다.

function foo() {
    var color = 'blue';
    function bar() {
        console.log(color);
    }
    bar();
}
foo();

bar함수는 우리가 부르는 클로저일까 아닐까?

 

일단 bar는 foo안에 속하기 때문에 foo스코프를 외부 스코프(outer lexical environment) 참조로 저장한다. 그리고 bar는 자신의 렉시컬 스코프 체인을 통해 foo의 color를 정확히 참조할 것이다.

 

그럼 클로저라 볼 수 있지 않을까?

 

아니다. 우리가 부르는 클로저라고 하기에는 약간 거리가 있다. bar는 foo안에서 정의되고 실행되었을 뿐, foo밖으로 나오지 않았기 때문에 클로저라고 부르지 않는다.

 

대신, 다음 코드는 우리가 실제로 부르는 클로저를 나타내고 있다.

var color = 'red';
function foo() {
    var color = 'blue'; // 2
    function bar() {
        console.log(color); // 1
    }
    return bar;
}
var baz = foo(); // 3
baz(); // 4

이게 바로 클로저다. 그냥 단순하게 보면 "이 당연하게 왜?"라고 생각할 수 있지만, 조금 더 자세히 따져보도록 하자.

 

일단 중요한 부분은 2~4번, 그리고 7번이다. bar는 자신이 생성된 렉시컬 스코프에서 벗어나 global에서 baz라는 이름으로 호출이 되었고, 스코프 탐색은 현재 실행 스택과 관련 없는 foo를 거쳐 갔다. baz를 bar로 초기화할 때는 이미 bar의 outer lexical environment를 foo로 결정한 이후이다. 때문에, bar의 생성과 직접적인 관련이 없는 global에서 아무리 호출하더라도 여전히 foo에서 color를 찾는 것이다. 이런 bar(또는 baz)와 같은 함수를 우리는 클로저라고 부른다.

여기에서 다시 한번 강조하지만 JS의 스코프는 렉시컬 스코프, 즉 이름의 범위는 소스코드가 작성된 그 문맥에서 바로 결정되는 것이다.

 

추가로, foo의 렉시컬환경 인스턴스는 foo();수행이 끝난 이후 GC가 회수해야 하는데 사실을 그렇지 않다. 앞에 설명했듯 bar는 여전히 바깥 렉시컬 환경인 foo의 렉시컬 환경을 계속 참조하고 있고, 이 bar는 baz가 여전히 참조하고 있기 때문이다.(baz(=bar) -> foo)

 

foo() 함수 실행 후 bar() 함수로 넘어 갈 때, bar()는 foo() 내부의 함수라 foo() 렉시컬 스코프를 계속 참조하고 있어 GC가 회수하지 않음 (스코프 체인)

 

 

 

 

 

 

클로저와 변수 은닉화(캡슐화)

var makeCounter = function() {
      var privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        increment: function() {
          changeBy(1);
        },
        decrement: function() {
          changeBy(-1);
        },
        value: function() {
          return privateCounter;
        }
      }
    };

    var counter1 = makeCounter();
    var counter2 = makeCounter();
    alert(counter1.value()); /* 0 */
    counter1.increment();
    counter1.increment();
    alert(counter1.value()); /* 2 */
    counter1.decrement();
    alert(counter1.value()); /* 1 */
    alert(counter2.value()); /* 0 */
  • 두 개의 카운터가 어떻게 다른 카운터와 독립성을 유지하는지 주목해보자. 
  • 각 클로저는 그들 고유의 클로저를 통한 privateCounter 변수의 다른 버전을 참조한다. 
  • 각 카운터가 호출될 때마다, 하나의 클로저에서 변수 값을 변경해도 다른 클로저의 값에는 영향을 주지 않는다.
  • 이처럼 외부에서 클로저 된 privateCounter에 직접 접근하지 못하므로 은닉화(캡슐화) 되었다 표현.

→ 이런 방식으로 클로저를 사용하여 객체지향 프로그래밍의 정보 은닉과 캡슐화 같은 이점들을 얻을 수 있다.

 

 

 

 

 

▼ 간단한 실습

let b = 1;		// 전역 변수 b

function hi () {

const a = 1;

let b = 100;

b++;

console.log(a,b);

}

//console.log(a);	// ReferenceError

console.log(b);		// (1)

hi();			// (2)

console.log(b);		// (3)
  1. 콘솔에 찍힐 b 값을 예상해보고, 어디에서 선언된 “b”가 몇번째 라인에서 호출한 console.log에 찍혔는지, 왜 그런지 설명해보세요.
    • (1) b = 1;  →  전역 변수 b를 참조함. 
    • (2) a = 1; b = 100;  →  함수 스코프 내의 값을 참조.
    • (3) b = 1;  →  전역 변수 b를 참조함. (2)는 콘솔에 찍힌 것뿐. b가 100이 된 건 아님.
  2. 주석을 풀어보고 오류가 난다면 왜 오류가 나는 지 설명하고 오류를 수정해보세요.
    • a가 함수 내에 선언돼어 있지만, 함수 스코프 내의 영향을 받아 밖으로 빠져나오지 못함. 즉, 밖에서 함수 스코프 내의 a를 참조하지 못 하고, 전역 변수에 지정된 것도 없으므로 ReferenceError가 발생함.

 

 

 

 

※ 이걸 정리하고 보니 이전에 작성했던 JS의 콜 스택(호출 스택)과 실행 컨텍스트, 전역 컨텍스트를 더 잘 이해할 수 있을 것 같다.

 






[참고] https://dmitripavlutin.com/javascript-variables-and-temporal-dead-zone/

 

Don't Use JavaScript Variables Without Knowing Temporal Dead Zone

Temporal Dead Zone forbids the access of variables and classes before declaration in JavaScript.

dmitripavlutin.com

[참고] https://ui.toast.com/weekly-pick/ko_20191014

 

TDZ을 모른 채 자바스크립트 변수를 사용하지 말라

간단한 질문을 하나 하겠다. 아래 코드 스니펫에서 에러가 발생할까? 첫 번째 코드는 인스턴스를 생성한 다음 클래스를 선언한다.

ui.toast.com

[참고] https://noogoonaa.tistory.com/78

 

TDZ(Temporal Dead Zone)이란?

함께보면 좋은 글 2020/07/05 - [프로그래밍 언어/Javascript] - 자바스크립트 호이스팅(Hoisting)이란? 오늘은 TDZ(Temporal Dead Zone)에 대해서 알아보도록 하겠습니다. 이번 포스팅은 자바스크립트의 호이스.

noogoonaa.tistory.com

[참고] https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures

 

클로저 - JavaScript | MDN

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.

developer.mozilla.org

[참고] https://meetup.toast.com/posts/86

 

자바스크립트의 스코프와 클로저 : NHN Cloud Meetup

자바스크립트의 스코프와 클로저

meetup.toast.com

[참고] https://meetup.toast.com/posts/90

 

클로저, 그리고 캡슐화와 은닉화 : NHN Cloud Meetup

클로저, 그리고 캡슐화와 은닉화

meetup.toast.com

[참고] https://www.youtube.com/watch?v=ocGc-AmWSnQ&list=PLZKTXPmaJk8JZ2NAC538UzhY_UNqMdZB4

[참고] https://www.youtube.com/watch?v=fETYLCU2YYc 

[참고] https://www.youtube.com/watch?v=HsJ4oy_jBx0&t=22s 

[참고] https://www.youtube.com/watch?v=EWfujNzSUmw