프론트엔드/Javascript

자바스크립트 기본개념(1) - 호이스팅

.log('FE') 2021. 10. 14. 23:37
728x90
반응형
자바스크립트 기본개념 시리즈의 시작으로 호이 스팅에 대해서 다뤄보려고 합니다.
이 글을 이해하려면 변수, 스코프, 함수에 대해 최소한의 이해와 기본 지식이 바탕이 되어야 합니다.

 

호이스팅이란?

MDN
인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것을 의미합니다.

 

일단 호이스팅이란 사전적 의미는 끌어올리기, 끌어올림 또는 게양 이라는 용어로 해석될 수 있습니다.

자바스크립트에서의 호이스팅역시 선언된 변수나 함수가 끌어올려진다 라는 의미를 갖고있습니다. 예전엔 저도 그냥 끌어올려진다 그래서 변수나 함수는 어디서 선언하더라도 가장 먼저 실행된다 라고만 생각하고 있었습니다.

그러나 호이스팅은 제대로 파고들면 알아야할 연관 개념들이 있고 꽤 디테일한 로직을 갖고있습니다. 우선 변수의 관점에서 호이스팅을 확인해보겠습니다.

자바스크립트의 변수

자바스크립트에는 3가지의 변수를 사용할 수 있습니다.

var, let, const

호이스팅을 알아보기위해 조사를 하다보면 어떤 글에서는 var 만 호이스팅이 발생한다 또 어디서는 모든 변수가 호이스팅이 발생한다. 라는 나뉘어진 견해로 작성된 정보들이 있어 저도 처음엔 혼동이 왔었습니다. 개발자 관련 어떤 세션을 보는데 이런 말씀을 하시더라구요.

여러 블로그를 보다보면 잘못된 정보를 참고해서 계속 잘못된 정보가 재생산되는 경우가 많다.

바야흐로 대 개발자의 시대에 정보가 자유롭게 공유되고 번역을 통해 재생산되고 공부한것을 정리하고 매우 좋은 현상이라고 생각합니다. 다만 이게 정말 정확하고 올바른 정보인지 나는 잘못된 정보를 재생산하고있지는 않은지 생각해볼 필요성이 있습니다.

 

아무튼 다시 돌아와서 각 변수 키워드들의 특징에 대해서 확인해보겠습니다.

// 변수 선언
var a;
var d = "string";

// 변수 재선언
var a = 1;
var a = 123;

// 변수 재할당
var a = "Hello";
a = "world";

// 블록스코프
var a = 0;
if (a === 0) {
  var a = 1;
  console.log(a) // 1) ?
}
console.log(a) // ? 2)


// 함수스코프
var a = 0;
function change() {
  var a = 1;
  console.log(a) // 3) ?
}
change()
console.log(a) // 4) ?

 

과연 저 '?' 에 들어가야할 정답은 무엇일까요? 일단 var 로 선언한 식별자는 아무값도 할당해 주지않아도 선언이 가능합니다. 그리고 이 변수에는 어떤값도 들어갈 수 있습니다. 또한 재선언과 재할당이 아주 자유롭습니다. 여기까지는 어느정도 받아들일 수 있는 부분인데 블록스코프와 함수스코프일때 var 식별자의 결과가 달라질 수 있습니다.

 

블록스코프

1) 1

2) 1

 

함수스코프

3) 1

4) 0

 

왜 이런 차이가 생기는걸까요? 일단 단순하게 가장 처음 var 라는 변수를 접하면서 나오는 내용중하나가 var 는 함수레벨에서 또는 함수스코프내에서 동작한다라는걸 볼 수 있을텐데 함수 내에서 사용된 var 식별자는 지역변수가 됩니다. 예전의 저라면 아 그렇구나 하고 넘어갔을테지만 좀더 깊게 알아보려고합니다.

 

실행컨텍스트

실행컨텍스트란 코드가 실행되는 환경을 말합니다. 실행컨텍스트에의해서 코드의 실행 순서가 정해지집니다. 그렇다면 이 실행컨텍스트는 언제 생기는걸까요? 

 

실행컨텍스트는 크게 두가지로 나눌 수 있습니다. 전역실행컨텍스트함수실행컨텍스트 입니다. 전역실행컨텍스트에서 변수가 선언되면 전역변수가 되고 함수실행컨텍스트내에서 var 변수가 선언되면 지역변수가 됩니다. 즉 var 는 실행컨텍스트의 영향력 아래에 놓여진다고 볼 수 있을것같습니다.

 

좀더 정확하게 var 가 무엇인지 알아보려면 정확한 문서를 보면됩니다. 이제 여기저기 떠도는 글을 보는것이 아니라 공식적으로 작성된 문서를 보시길 추천합니다. ECMAScript2020 문서를 확인하면 var 에 대한 내용을 확인할 수 있습니다. 저 중에서도 var 에 대한 내용은

13.3.2 Variable Statement 를 찾아보시면 됩니다.

A var statement declares variables that are scoped to the running execution context's VariableEnvironment. Var variables are created when their containing Lexical Environment is instantiated and are initialized to undefined when created. Within the scope of any VariableEnvironment a common BindingIdentifier may appear in more than one VariableDeclaration but those declarations collectively define only one variable. A variable defined by a VariableDeclaration with an Initializer is assigned the value of its Initializer's AssignmentExpression when the VariableDeclaration is executed, not when the variable is created.
var 문은 실행 중인 실행 컨텍스트의 VariableEnvironment로 범위가 지정된 변수를 선언합니다. Var 변수는 포함하는 어휘 환경이 인스턴스화될 때 생성되고 생성될 때 정의되지 않음으로 초기화됩니다. VariableEnvironment의 범위 내에서 공통 BindingIdentifier는 둘 이상의 VariableDeclaration에 나타날 수 있지만 이러한 선언은 집합적으로 하나의 변수만 정의합니다. Initializer가 있는 VariableDeclaration으로 정의된 변수에는 변수가 생성될 때가 아니라 VariableDeclaration이 실행될 때 InitializerAssignmentExpression 값이 할당됩니다.

 

영문을 직접 해석할 자신은없어서 구글 번역을 돌렸습니다. 번역을 돌렸는데도 알수없는 단어들이 나오면서 한글도 이해하기 힘들 수 있을것같습니다. 일단 실행컨텍스트에는 두개의 컴포넌트가 존재합니다. (Table 24: Additional State Components for ECMAScript Code Execution Contexts) 바로 렉시컬 환경 컴포넌트변수환경 컴포넌트입니다. 그리고 이 두개의 컴포넌트는 실행컨텍스트 생성초기에 동일한 렉시컬 환경을 참조하게됩니다. 이때 이 두개의 컴포넌트는 실행컨텍스트스택의 가장 첫번째 컨텍스트일때만 구분하게되고 대부분의 식별자를 관리하는건 저둘이 참조하고있는 렉시컬환경을 중점으로 동작을 이해하면됩니다.

 

그래서 결국 저말을 좀더 풀어보자면 var 는 렉시컬환경이 생성될때 생성되고 이 과정을 선언이라고 합니다. 실행컨텍스트는 코드를 읽을때 곧바로 실행하는것이 아닌 변수나 함수식별자들을 만나게되면 식별자를 해결하기위한 선언을 수행합니다. 바로 이 선언이 우리가 알고있는 호이스팅이 발생하는 지점입니다. 변수나 함수가 어디에 위치해있더라도 엔진에의해서 가장 먼저 식별되고 선언됩니다.

 

그리고 var 는 초기화됩니다. 즉 선언과 동시에 초기화가되는데 이때 undefined 가 할당되어 초기화 시킵니다. 그래서 선언전에 var 변수는 호출하게되면 undefined 가 출력됩니다. 선언이 끝나면 그때 코드를 하나씩 실행하게되는데 이 실행하는 과정에서 실제 변수에 할당한 값들이 할당되게 됩니다. 이게 var 의 비밀(?)입니다.

 

let and const

let and const declarations define variables that are scoped to the running execution context's LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer's AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.
let 및 const 선언은 실행 중인 실행 컨텍스트의 LexicalEnvironment로 범위가 지정된 변수를 정의합니다. 변수는 포함하는 Lexical Environment가 인스턴스화될 때 생성되지만 변수의 LexicalBinding이 평가될 때까지 어떤 식으로든 액세스할 수 없습니다. Initializer가 있는 LexicalBinding에 의해 정의된 변수는 변수가 생성될 때가 아니라 LexicalBinding이 평가될 때 Initializer의 AssignmentExpression 값이 할당됩니다. let 선언의 LexicalBinding에 Initializer가 없으면 LexicalBinding이 평가될 때 변수에 정의되지 않은 값이 할당됩니다.

이제 let 과 const 에 대한 내용을 확인해보겠습니다. 일단 첫줄내용이 비슷하지만 다릅니다. var 는 실행컨텍스트가 갖고있던 두개의 컴포넌트중 환경변수 컴포넌트로 범위가 지정된다고 했는데 let 과 const 는 렉시컬환경 컴포넌트로 범위가 지정된 변수라고 하고있습니다. 그다음까지는 var 와 비슷하지만 이후 중요한 문장이 있습니다. 변수의 LexicalBinding이 평가될 때까지 어떤 식으로든 액세스할 수 없습니다. 바로 이 엑세스할 수 없는 구간이 TDZ(Templral Dead Zone) 일시적 사각지대가 되겠습니다. 

 

그리고 해당 변수는 생성될때가아닌 실제로 코드가 실행될때 값이 할당되는데 이때 let은 초기값이 정해지지 않으면 정의되지않은 값이 할당된다고 하는데 이때 undefined 가 할당됩니다. 한글과 영문을 적절하게 믹싱해서 보시면 좋을것같습니다.

 

// 변수 선언
let a;
const b = 0; // 선언과 동시에 초기화 하지않으면 에러

// 재선언 X
let a = 1;
let a = 2;
const b = 0;
const b = 2;

// 재할당
let a = 1;
a = 2;

const b = 1;
b = 2 // ❌ const 는 불가능

// 블록스코프
let a = 0
if (true) {
  let a = 2
  console.log(a) // 1) ?
}
console.log(a) // 2) ?

const b = 0;
if (true) {
  const b = 1;
  console.log(b) // 3) ?
}
console.log(b) // 4) ?


// 함수스코프
let a = 0
function test() {
  let a = 2
  console.log(a) // 1) ?
}
test()
console.log(a) // 2) ?

const b = 0;
function test() {
  const b = 2;
  console.log(b); // 3) ?
}
test()
console.log(b); // 4) ?

letconstvar 와 다르게 블록스코프과 함수스코프내에서 지역변수로 사용할 수 있습니다. 즉 스코프범위 지정 변수 라고 볼 수 있습니다. 

 

let , const 와 var 가 서로 다르게 알고리즘을 갖고있기때문에 실행컨텍스트내에서도 서로 다른 환경에서 관리되고 있습니다. 

 

전역실행컨텍스트 {
  전역 렉시컬환경 {
    전역 환경레코드 {
      객체환경레코드
      선언적 환경 레코드
    }
  }
}

 

각각의 내용은

객체환경레코드

선언적환경레코드

에서 확인하실 수 있습니다.

 

호이스팅 내용 하나 정리하려다가 자바스크립트의 모든 개념을 다 다루어야 할것같아서 여기까지만 정리하려고합니다. 어차피 다른 개념들 정리하다보면 반복되서 나오기 때문에 여기서 모든걸 다 정리할 필요는 없다고 생각했습니다. 

 

최종정리

  • 자바스크립트에서는 호이스팅 이라는 개념적 동작이 발생합니다.
  • 끌어올림이라고 해석되지만 물리적인 끌어올림이 아닌 자바스크립트 엔진이 코드를 해석하는 과정에서 발생하는 현상입니다.
  • 자바스크립트는 선언된 식별자들을 해결하기위해 선언된 변수와 함수를 가장 먼저 해석합니다.
  • 이때 var 같은경우 선언과 동시에 초기화가 진행되어 undefined 가 할당됩니다.
  • 함수도 선언과 동시에 function object 가 할당되어 어디서 호출하더라도 사용 가능합니다.
  • 코드가 해석되기이전 항상 전역실행컨텍스트가 생성됩니다. 함수실행컨텍스트는 함수가 호출욀때마다 생성됩니다.
  • var 는 함수스코프내에서 지역변수가 되기떄문에 이외에 다른곳에서의 선언은 모두 전역에서 선언한걸로 취급합니다.
  • var 는 재할당, 재선언이 가능해 코드양이 많아질경우 변수명의 중복이 발생했을때 디버깅이 쉽지않습니다.
  • let 과 const 도 호이스팅은 발생하지만 초기화가 코드실행단계해서 진행되기떄문에 선언전에 사용하면 에러가 발생합니다.
  • const 는 선언과 동시에 할당을 해주어야 합니다. 그리고 재할당이 불가능합니다.
  • let 과 const 는 동일 스코프내에서 재선언이 불가능합니다. 

 

 

실무적인관점

저는 실무에서 거의 대부분의 변수를 const 로 선언해서 사용하고 있습니다. 만약 변수에 재할당이 필요한 내용이 있다 라고 한다면 그때 let 을 사용합니다. const 가 다른 변수선언에 비해 제약이 많은편인데 제약이 많다는건 예측이 가능하고 디버깅이 쉬워진다는 얘기이기도합니다. const 가 제약이 있긴하지만 그렇다고 완전 불변한 값을 만드는것은 아닙니다. 값을 변화시킬 수 없는건 어디까지나 원시값에만 해당됩니다. 객체로 정의되어있다면 얼마든지 객체내의 값들을 수정하는것이 가능합니다.

그리고 보통 변수나 함수는 해당 스코프내의 최상단에서 선언해서 사용합니다. 

 

요약

  • 호이스팅은 단순히 끌어올림 그 이상의 개념을 포함하고 있다.
  • 호이스팅을 제대로 이해하려면 스코프, 함수, 실행컨텍스트 그리고 각각의 환경 레코드 들의 알고리즘에 대한 이해가 필요하다.
  • 결국 모든 변수와 함수선언식은 호이스팅이 발생한다는걸 확인 할 수 있었다.
  • 다만 선언과 초기화단계가 동시에 이루어지느냐 코드 실행단계에서 별도로 분리되어 이루어지느냐에 따라 에러가 발생할 수 있다.
  • 정확한 내용을 이해하고 알고싶으면 ECMAScript 문서를 확인하자 (사실 완전히 이해하기는 어렵다)

 

728x90
반응형

'프론트엔드 > Javascript' 카테고리의 다른 글

자바스크립트 배열 다루기  (0) 2021.12.09
리액트 라우터 이해하기  (0) 2021.11.15
Vanila Javascript Webpack Setting  (0) 2021.10.23