클로저와 실행 컨텍스트

정지훈·2020년 12월 8일
0

클로저! 클로저는 무엇일까?

ECMAScript에선 클로저를 알려주지는 않습니다.
그래서 Mdn 자료를 찾아보면
Mdn에선 클로저를 그 함수와 그 함수가 선언된 렉시컬 환경의 조합이라고 합니다.

이 말만 들으면 이해하기 어려울 수도 있습니다.

하지만 실행 컨텍스트와 렉시컬 스코프를 이해하고 나서 Mdn에서 클로저를 설명한 것을 무릎을 치면서 알아차릴 수 있을 것 입니다.(물론 이해 못할 수도 있습니다)

일단 렉시컬 스코프는 무엇일까?

자바스크립트는 정적 스코프를 사용합니다. 즉 호출과는 별개로 함수가 선언된 자리에서 상위 스코프를 결정합니다. 이것이 렉시컬 스코프입니다.

var x = 10;
function foo() {
      const x = 1;
      const y = 2;

      // 일반적으로 클로저라고 하지 않는다.
      function bar() {
        const z = 3;

        debugger;
        // 상위 스코프의 식별자를 참조하지 않는다.
        console.log(z);
      }

      return bar;
    }

    const bar = foo();
    bar();

bar함수의 전역 스코프는 foo함수 입니다.
foo함수의 지역 스코프에 위치한 bar함수는 지역 함수 입니다.
foo함수는 전역에 있으므로 전역 함수입니다.

실행 컨텍스트

이걸 실행 컨텍스트로 설명 하면 소스코드 평가 전 전역 객체가 만들어 집니다.

전역객체는 애플리케이션을 키자마자 생기는 것입니다.

실행 컨텍스트 스택에 전역 실행 컨텍스트가 생성이 되고
전역 렉시컬 환경이 만들어 집니다.
렉시컬 환경의 컴포넌트로 2개가 만들어 지는데 하나는 전역 환경 레코드와 외부 렉시컬 환경 참조가 만들어 집니다.

스코프의 역할을 하는 전역 환경 레코드는 객체 환경 레코드 그리고 선언적 환경 레코드가 구성이 됩니다. 객체 환경 레코드의 프로퍼티로 BindingObject를 통해 전역 객체와 연결이 됩니다.

위에 코드로 설명하면 소스 코드 평가 즉 런타임 이전에 변수 선언과 함수 선언을 먼저 합니다.
var x는 객체 환경 레코드에 BindingObject를 통해 전역 객체에 키로 등록이 되는데 var 함수의 경우 평가때 선언과 초기화를 같이 합니다.
그래서 전역객체의 키로 x 값으로는 undefined로 저장 됩니다.

이게 호이스팅이 이루어 지는 이유입니다.
몇몇 분들이 var x가 올라간 것 처럼 생각하지만 실행 컨텍스트의 관점으로 보았을때 이미 평가때 (런타임 이전) 선언과 초기화가 먼저 되어 있는 상태입니다.

console.log(x) // 에러? 아니 undefined -> 호이스팅
var x = 10;

그리고 소스코드 실행이 되면 10을 할당합니다.

만약 let이나 const으로 선언을 했다면 전역 객체로 가지 않고 위에 실행컨텍스트 그림을 보면 선언적 환경 레코드에 등록이 됩니다.
하지만 let이나 const는 평가때 선언과 초기화를 따로 합니다.
그래서 선언을하고 초기화 전까지 일시적 사각지대에 빠지게 됩니다. 그리고 우리가 알아볼수 없는 값으로 초기화를 합니다.
그래서 변수를 할당 전에 참조를 하면 참조 에러가 발생해서 호이스팅이 일어나지 않은 것 처럼 보이지만 실은 let과 const도 호이스팅이 이루어 입니다.

let x = 10;
{
  console.log(x); // 10이 나올꺼 같지만 참조에러발생
  let x = 20;
}

다시 실행 컨텍스트로 돌아가서 전역 함수인 foo 함수도 평가때 전역객체에 선언이 되고 함수 객체가 만들어 집니다. 이때 함수 객체의 내부슬롯인 [[Emvironment]]에 현재 실행 중인 컨텍스트의 렉시컬 환경이 저장 되는데
전역 컨텍스트가 실행 하고 있기 때문에 전역 렉시컬 환경이 저장 됩니다.

이 함수가 호출이 되면 전역 실행 컨텍스트 foo함수 실행 컨텍스트에 쌓이게 되는데 foo함수 실행 컨텍스트의 foo함수 렉시컬 환경에 외부 렉시컬 환경 참조에 [[Emvironment]]의 참조값으로 들어가게 됩니다.

이런 형태이고
이것으로인해 스코프 체인이 만들어 지는 것입니다.

foo함수 중첩함수인 bar가 호출이 되고 실행이 종료되면 bar함수의 실행 컨텍스트부터 스택에서 빠지게 됩니다. foo함수가 빠지게 됩니다. 최종적으로 코드가 끝날시 전역 실행 컨텍스트도 빠지게 됩니다.

클로저로 돌아가서 Mdn에서 '그 함수와 그 함수가 선언된 렉시컬 환경'
로 하면 mdn에서의 자료에서 보면 위의 그림처럼 함수와 그 함수가 선언된 렉시컬 환경의 조합이니까 mdn에 말대로라면 모든 함수가 클로저라고 합니다 하지만 클로저는 아닙니다!

var x = 10;
function foo() {
      const x = 1;
      const y = 2;

      // 일반적으로 클로저라고 하지 않는다.
      function bar() {
        const z = 3;

        debugger;
        // 상위 스코프의 식별자를 참조하지 않는다.
        console.log(z);
      }

      return bar;
    }

    const bar = foo();
    bar();

이것을 보면 클로저다? 일반적으로 함수와 그 선언된 렉시컬 환경이 있으니 클로저다? 일단 클로저가 되려면 조건이 있습니다.

첫 번째 조건으로 클로저는 외부함수보다 중첩함수가 더 오래 살아남아야 하고
두 번째 조건으로 외부함수보다 더 오래 살아남는 중첩함수가 외부함수의 식별자를 바라보고 있어야 합니다.

이 조건을 만족해야지 클로저라고 할 수 있습니다.

위 함수는 debugger로 하면 위에 처럼 나옵니다.

그러면 어떻게 외부 함수보다 중첩 함수가 더 오래 살아남을 수 있을까?

그러러면 중첩함수를 리턴해서 전역에서 변수에 할당을 하면 외부함수는 스택에서 빠지게 되지만 중첩함수는 변수에 참조하고 있으니 중첩 함수보다 더 오래 살아남습니다.

코드로 보면

var x = 10;
function foo() {
	var inner = 10;
    return function bar() {
    	return console.log(inner); // 10
    }
}
var bar = foo(); 
bar();

이게 클로저 입니다. 이미 foo의 실행 컨텍스트 제거가 된 상태 입니다.

위에 코드를 디버그 해보면 위에 처럼 나옵니다.

위에 실행 컨텍스트 그림에서 foo Execution Context가 없다고 보면 됩니다.

이제 bar 중첩함수가 호출이 되면 bar실행 컨텍스트가 생성이 되고 값이 출력 됩니다.

이때! 왜 foo 함수 실행 컨텍스트가 스택에서 제거 되었는데 inner의 변수를 참조 할 수 있을까? 이것은 실행 컨텍스트 스택에서 빠져도 foo함수 렉시컬 환경은 살아 있기 때문입니다. bar함수가 외부 함수의 식별자를 바라보고 있기 때문입니다. 이때 이 외부함수의 식별자를 자유 변수라고 합니다.

즉 누군가가 참조하고 있으면 가비지 컬렉터의 대상에서 제외 되는 것입니다.

만약 inner자유 변수를 바라보고 있지 않는다면 렉시컬 환경까지 제거가 될까? 아니다! 렉시컬 환경은 제거가 되지 않는다.
렉시컬 환경과 실행 컨텍스트 스택은 독립적으로 분리되어 있다.
렉시컬 환경에 외부 렉시컬 환경 참조에 참조값으로 [[Emvironment]] 내부슬롯이 참조값으로 갖고 있기 때문에 가비지컬렉터는 누군가가 참조를 하고 있으면 제거 되지 않는다.

하지만! 어느 브라우저에서는 성능 최적화 때문에 필요없다 브라우저가 필요없다 생각하면 가비지컬렉터 대상이 되서 제거 됍니다.

하지만 자유 변수를 바라보고 있지 않다는건 중첩 함수에서 외부 함수 식별자를 사용을 안하는거고 외부 함수 변수를 바라보지 않는다는건 두 번째 조건에서 탈락이다.

클로저.. 이건 왜 쓰는 것일까?

그것은 안전하게 상태를 유지하고 변경하기 위해서 사용을 하는 것 과 최적화 용도로도 사용할 수 있습니다.

만약 전역에 전역변수를 사용하면 의도치 않게 변경이 되고 훼손되기 때문입니다.

즉 더 안전하게 사용하려면 즉시실행함수로 한번만 호출하면서 자유변수를 사용할 수 있습니다.

const counter = (function () {
  // 카운트 상태 변수
  let num = 0;

  // 클로저인 메서드를 갖는 객체를 반환한다.
  // 객체 리터럴은 스코프를 만들지 않는다.
  // 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경이다.
  return {
    // num: 0, // 프로퍼티는 public하므로 은닉되지 않는다.
    increase() {
      return ++num;
    },
    decrease() {
      return num > 0 ? --num : 0;
    }
  };
}());

console.log(counter.increase()); // 1
console.log(counter.increase()); // 2

console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0

즉시 실행 함수는 한번 실행 되자 마자 스택에서 빠지고 객체안에 메서드를 호출 할때마다 자유 변수인 num을 사용해서 안전하게 쓸 수 있습니다.

마지막으로 실행 컨텍스트 스택이라고 부르기도 하고 콜 스텍이라고 부르기도 합니다~~

0개의 댓글

관련 채용 정보