Closure - (a)

10258 단어 JavaScriptJavaScript

A Closure is the combination of a function and the lexical environment within which that function was declared
클로저 - Javascript | MDN

MDN에서는 클로저에 대해 다음과 같이 정의하고 있다. 정의가 너무 난해해서 잘 와닿지 않는다. 예제를 통해 실습을 해보고 클로저에 대해 완벽하게 이해하자.

const x = 1;

function outerFunc () {
  const x = 10;
  
  function innerFunc () {
    console.log(x); // 10
  }
  
  innerFunc();
}

outerFunc();

outerFunc 함수 내부에서 중첩 함수인 innerFunc가 정의되고 호출되었다. 이때 innerFunc의 상위(부모) 스코프는 외부 함수 outerFunc의 스코프이다.

따라서 innerFunc 내부에서 자신을 포함하고 있는 외부 함수(부모) outerFunc의 x 변수에 접근할 수 있다. (자기 자신의 부모 스코프에 있는 변수에 접근 가능하다고 이해)

만약 innerFunc 함수가 outerFunc 내부에서 정의된 중첩 함수가 아니라면(외부에 따로 선언된 또 다른 하나의 함수라면) innerFunc함수를 outerFunc 함수의 내부에서 호출한다고 해도 outerFunc 함수의 변수에 접근할 수 없다.

const x = 1;

function outerFunc () {
  const x = 10;
  innerFunc();
}

function innerFunc () {
  console.log(x); // 1
}

outerFunc();

이는 자바스크립트가 렉시컬 스코프를 따르는 언어이기 때문이다.
(위 예제는 사실 너무나도 당연하다)

렉시컬 스코프

렉시컬 스코프에 대해서는 이전에 공부를 했었다. 렉시컬 스코프의 관점에서 실행 컨텍스트를 바라보자.

자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의 했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프(정적 스코프)라 한다.

=> 위 예제에서 innerFunc를 outerFunc 내부에서 호출하기는 했지만 innerFunc가 정의된 위치는 아예 다른 곳(외부)이기 때문에 그기 있는 변수를 참조할 수 없다.

const x = 1;

function foo () {
  const x = 10;
  bar();
}

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

foo(); // (1) ??
bar(); // (2) ??

(1) = 1, (2) = 1

foo 함수를 호출한다 -> foo 함수의 스코프 안에서 x는 10으로 할당된다 -> bar 함수를 호출한다 -> 콘솔로 x를 찍어보는데 이 때 x는 전역 스코프의 1이다(10으로 할당한 x는 foo 함수에서만 적용되는 지역 스코프의 x이기 때문) -> 콘솔에 1이 찍히고 foo함수 종료 후 bar 함수를 호출한다 -> 위와 같은 맥락으로 전역 변수 x의 값 1이 출력된다

다시 한번 언급하지만,
함수의 상위 스코프는 함수를 어디서 호출했냐가 아니라 어디서 정의했느냐에 따라 결정되는 것을 잘 이해하면 뒤에 내용을 이해하는데 무리는 없을 것이다.
=> foo함수와 bar함수의 상위 스코프는 전역이다.

실행 컨텍스트에서 공부했듯이 스코프의 실체는 실행 컨텍스트의 렉시컬 환경이다. 이 렉시컬 환경은 자신의 "외부 렉시컬 환경에 대한 참조"를 통해 상위 렉시컬 환경과 연결된다. 이것이 바로 스코프 체인이다.

함수가 정의된 환경(위치)과 호출되는 환경(위치)은 다를 수 있다. 따라서 렉시컬 스코프가 가능하려면 함수는 자신이 호출되는 환경과는 상관없이 자신이 정의된 환경, 즉 상위 스코프(함수 정의가 위치하는 스코프가 바로 상위 스코프다)를 기억해야 한다.

이를 위해 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.

다시 말해, 함수 정의가 평가되어 함수 객체를 생성할 때 자신이 정의된 환경(위치)에 의해 결정된 상위 스코프의 참조를 함수 객체 자신의 내부 슬롯에 저장한다.

이때 자신의 내부 슬롯에 저장된 상위 스코프의 참조는 현재 실행중인 실행 컨텍스트의 렉시컬 환경을 가리킨다. 왜냐하면 함수 정의가 평가되어 함수 객체를 생성하는 시점은 함수가 정의된 환경, 즉 상위함수(또는 전역 코드)가 평가 또는 실행되고 있는 시점이며, 이때 실행중인 실행 컨텍스트는 상위함수의 실행 컨텍스트이기 때문이다.

정리해보면, 함수 객체를 생성할 때 자기 위치에 대해서 상위 스코프의 참조를 저장하는데 이는 바로 현재 실행중인 실행 컨텍스트이다. 왜냐하면 그 상위함수가 이미 평가 또는 실행이 되고 있을 때 해당 함수 객체가 생성되기 때문이다.

따라서 함수 객체의 내부 슬롯에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프다.

또한 자신이 호출되었을 때 생성될 함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장될 참조값이다. 함수 객체는 내부 슬롯에 저장한 렉시컬 환경의 참조, 즉 상위 스코프를 자신이 존재하는 한 기억한다.

(이제 슬슬 어려워지기 시작한다..)

위의 예제에서 bar 함수는 자신의 상위 스코프 즉, 전역 렉시컬 환경을 내부 슬롯에 저장하여 기억하는 것이다.

클로저와 렉시컬 환경

const x = 1;


function outer() {  // (1)
  const x = 10;
  const inner = function () {
    console.log(x); // (2)
  }
  return inner;
}

// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // (3)
innerFunc(); // (4) 10

??????

내 머릿속 : 어떻게 10이 나올 수 있지? 분명히 outer 함수는 inner 함수를 반환하고 죽잖아 그럼 당연히 outer의 지역 스코프에 있던 x = 10 도 없어지는게 아닌가

이게 바로 클로저라는 개념이다. 천천히 알아보자.

outer 함수를 호출(3)하면 outer 함수는 중첩 함수 inner를 반환하고 생명 주기를 마감한다. 즉, outer 함수의 실행이 종료되면 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거(pop)된다.

이때 지역 변수 x와 변수 값 10을 저장하고 있던 outer 함수의 실행 컨텍스트가 제거되었으므로 outer 함수의 지역 변수 x 또한 생명 주기를 마감한다. 따라서 outer 함수의 지역 변수 x는 더는 유효하지 않게 되어 x 변수에 접근할 수 있는 방법은 딱히 없어 보인다. 아니 그냥 없다.

=> 딱 여기까지 너무 당연하다. 너무나 명쾌하다.

그러나 위 코드의 실행 결과(4)는 outer 함수의 지역 변수 x의 값인 10이다. 이미 생명 주기가 종료되어 실행 컨텍스트 스택에서 제거된 outer 함수의 지역 변수 x가 다시 부활이라도 한 듯이 동작하고 있다.

(어디 다른 공간에 예비로? 혹시 모르니까? 따로 저장.. 해놨던 건 아닐까?)

이처럼 외부 함수보다 중첩 함수가 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수를 참조할 수 있다.
이러한 중첩 함수를 클로저(Closure)라고 부른다.



배가 많이 고픈 관계로 잠시 쉬고

To be continue..

좋은 웹페이지 즐겨찾기