실행컨텍스트를 통해 호이스팅, 스코프체인, 클로저 이해하기

younoah·2021년 8월 2일
2

[My 자바스크립트]

목록 보기
2/17

실행 컨텍스트란?

자바스크립트에서 실행 가능한 코드인 전역코드(global), 함수, 코드블럭({...}) 은 실행 컨텍스트를 갖는다.

실행 컨텍스트는 이런 실행 가능한 코드가 실행되기 위해 필요한 아래와 같은 환경 정보를 갖고 있는 객체이다.

실행 컨텍스트가 갖고 있는 환경 정보
  • 변수 : 전역변수, 지역변수, 매개변수, 객체의 프로퍼티
  • 함수 선언
  • 변수의 유효범위(스코프)
  • this

실행 컨텍스트도 하나의 객체인데 구체적인 형태를 보면 아래와 같다.

실행 컨텍스트의 구체적인 형태
  • VariavleEnvironment : Lexical Environment와 동일하지만 최초의 스냅샷 (초기값)
    • environmentRecord(snapshot)
    • outerEnvironmentReference(snapshot)
  • Lexical Environment : 현재 실행컨텍스트의 식별자 정보를 갖는다. (진행에 따라 변화함!)
    • environmentRecord : 현재문맥의 식별자(hoisting)
    • outerEnvironmentReference : 외부 식별자(scope chain), 외부 참조역할
  • ThisBinding : 현재 실행컨텍스트의 this 정보를 갖는다.

*식별자 : 쉽게 말해 변수명, 함수명, 매개변수명 등이다.

실행 가능한 코드를 실행 했을 때 동작방식은 아래와 같다.

  1. 해당 실행 가능한 코드에 대한 실행 컨텍스트가 생성된다.

  2. environmentRecord (변수선언, 함수선언) 를 수집한다. (호이스팅)

  3. 한줄 씩 코드를 진행한다.

  4. 식별자와 값이 필요할 때는 자기 자신의 컨텍스트의 environmentRecord 를 탐색한다.

  5. 없을 경우 outerEnvironmentReference 를 참조하여 한 단계 위의 외부 컨텍스트를 탐색한다.

    타겟 식벽자를 찾거나 전역 컨텍스트까지 계속해서 외부를 참조해 나간다. (스코프 체인)

실행 가능한 코드가 끝나면 실행 컨텍스트는 사라진다. (가비지 컬렉팅)

다만! 실행 컨텍스트가 외부에서 참조해야만 하는 상황에서는 사라지지 않는다. (클로저)

실행 컨텍스트에 대한 개념을 알아보면서 너무나 중요한 호이스팅, 스코프 체인, 클로저 라는 개념을 언급했는데.

각 개념들을 다시 한번 정리해보자.

호이스팅

호이스팅이란 함수가 실행 가능한 코드가 실행되기 전에 필요한 변수들을 모두 한번씩 읽어서 미리 초기화 하는 작업이다.

선언한 것들을 마치 최상단에 끌어 올려 놓는것과 같아서 호이스팅이라고 한다.

var, let, const, function, class 등 모든 선언 키워드들은 호이스팅이 된다.

쉽게 말해 일단 실행에 필요한 재료들을 한번 싹 읽어 오는것이다. 사용할수 있냐 없냐는 그 다음 문제이다.

실행 가능한 코드 안에서

var 는 자동으로 undefined 로 초기화 된다. 따라서 초기값이 있든 없든 선언전에 사용이 가능한다. 다만 초기값이 없다면 초기화 이전까지는 undefined 라는 값으로 할당이 되어 있는것이다.

let , constvar 처럼 자동으로 초기값을 할당하지 않는다. 따라서 선언부 이전까지는 사용이 불가능하다. 이것이 바로 TDZ(Temporal Dead Zone)이다.

정리

var : 선언 및 초기화 ➡️ 할당
let, const : 선언 ➡️ TDZ ➡️ 초기화 ➡️ 할당

스코프 체인

스코프 체인이란 말 그대로 스코프가 서로 연결되어 있다는 뜻인데

지역에서 외부변수 혹은 전역변수를 사용할 수 있게 하기 위한 실행 컨텍스트의 메커니즘이다.

클로저

함수가 선언된 한경의 스코프(혹은 실행 컨텍스트)를 기억하여 함수가 스코프 밖에서 실행될 때에서 실행되었을때의 스코프를 기억하여 해당 실행 컨텍스트 정보들을 사용하는 문법이다.

예제를 통해서 클로저가 무엇인지 보자.

function makeGreeting(name) {
  const greeting = 'Hello, ';

  return function () {
    console.log(greeting + name);
  };
}

const world = makeGreeting('World!');
const kevin = makeGreeting('kevin~!!');

world(); // Hello, World!
kevin(); // Hello, kevin~!!

원래 같으면 makeGreeting 함수 내의 greeting 변수는 makeGreeting 함수가 종료되면 makeGreeting 함수와 함께 가비지컬렉팅 대상이 되어 삭제된다.

하지만 makeGreeting 함수의 리턴값인 함수에서 greeting 변수를 사용하고 있는 상태에서 worldkevin 라는 변수에 할당을 해주었다.

따라서 helloWorldhelloYoonho 라는 변수가 makeGreetinggreeting 을 계속 참조하고 있기 때문에 즉, 계속 연결되어 있기 때문에 makeGreeting 함수와 greeting 변수가 가비지 컬렉팅 대상이 되지 않아 살아남아 있고 worldkevin 라는 변수가 계속해서 참조하고 있게 된다.

이렇게 해서 worldkevin 라는 변수에 할당되는 함수가 생성되었던 시점의 실행 컨텍스트(혹은 스코프)를 기억하고 사용할 수 있게 된다.

클로저의 활용

은닉화

클로저를 이용하면 변수와 함수를 숨길 수 있다.

그 밖에 활용 용도가 많겠지만

클로저를 잘 알아야 하는 이유는 클로저를 유용하게 사용기 보다 알기 힘든 버그를 잘 수정하기 위해서 꼭 이해하고 있어야한다. 🤣

클로저 대표 문제

function counting() {
  let i = 0;
  for (i = 0; i < 5; i++) {
    setTimeout(function () {
      console.log(i);
    }, i * 1000);
  }
}
counting();

위의 예제의 실행 결과는 무엇일까???

0
1
2
3
4

라고 예상할 수 있지만 아니다!!!

답은

5
5
5
5
5

이다.

이유는

setTimeout 함수의 대기 시간이 끝나 콜백함수가 실행된 시점에서는 루프가 종료가 된 상태이고 전역 변수 i 는 5가 되었기 때문이다. 🤗

그렇다면 0, 1, 2, 3, 4 가 출력 되게 하기 위해서는 어떻게 해야할까??

첫번재 방법은 "즉시 실행 함수(IIFE)"를 활용하는 것이다.

잠깐 "즉시 실행 함수"란??

함수를 정의하자마자 바로 호출하는 것을 즉시실행함수라고 한다.

"즉시 실행 함수"를 사용하는 이유는???

즉시 실행 함수는 한 번의 실행만 필요로하는 초기화 코드에서 많이 사용된다.

그런데 왜 초기화 코드에서 굳이 즉시실행함수를 사용할까?

변수를 전역으로 선언하는것을 피하기 위해서이다.

전역에 변수를 추가하지 않아도 되기 때문에 코드 충돌 없이 구현할 수 있어서 라이브러리 등을 만들때 많이 사용한다.

function counting() {
  let i = 0;
  for (i = 0; i < 5; i++) {
    (function (number) {
      setTimeout(function () {
        console.log(number);
      }, number * 1000);
    })(i);
  }
}
counting();

즉시 실행함수를 이용해서 각 매번의 루프마다 클로저를 만드는 것이다.

각 매번의 루프마다 즉시실행함수가 생성되고 함수의 매개변수 number 의 값이 0, 1, 2, 3, 4 로 각각 저장이 되는데 setTimeout 함수가 각 즉시실행함수가 생성될 때의 환경의 number 를 사용하게된다.

두번째 방법은 "let"을 이용하는 방법이다.

function counting() {
  for (let i = 0; i < 5; i++) {
    setTimeout(function () {
      console.log(i);
    }, i * 1000);
  }
}
counting();

let 은 블록 수준 스코프이기 때문에 매 루프마다 클로저가 생성된다.

각 매번의 반복문마다 서로 다른 하나의 스코프가 생성된다고 생각하자 그리고 그런 스코프를 setTimeout 이 기억하고 있고 당시의 스코프가 갖고있던 지역변수 i 를 사용하게된다.


정리

두 방식 모두 본질은 변수 i를 매 반복문에서 당시의 i의 값을 실행컨텍스트로 감싸서 값을 기억하게 하고 이런 실행컨텍스트를 setTimeout함수가 기억하고 있게 하는것이다.

profile
console.log(noah(🍕 , 🍺)); // true

0개의 댓글