JAVASCRIPT DEEP DIVE 2회독 24장 - 클로저

김명성·2022년 6월 15일
0

클로저

클로저는 실행 컨텍스트에 대한 사전 지식이 있으면 이해하기 어려운 개념은 아니다.
클로저는 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.

일급 객체
일급 객체(first-class object)란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.
보통 함수에 인자로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 한다.
즉 값으로도 쓰이고, 변수에 할당도 가능하며, 함수의 연산 결과(return)로도 쓰일 수 있을 때 일급 객체라 한다.

클로저는 MDN에서 다음과 같이 정의한다

클로저는 함수그 함수가 선언된 렉시컬 환경과의 조합이다.

렉시컬 스코프

렉시컬 스코프는 함수를 어디서 호출했는지가 아니라, 함수를 어디에서 정의했는지에 따라 상위 스코프를 결정하며 이를 렉시컬 스코프라 한다.

const x = 1;

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

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

foo(); // 1
bar(); // 1

위 예제처럼 함수의 호출 위치는, 상위 스코프 결정에 어떠한 영향을 주지 못하기에, foo함수와 bar 함수의 실행 결과는 모두 1이다.

함수의 상위 스코프를 결정한다는 것은, "렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 값을 결정한다"는 것과 같은 말이다.

렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경에 의해 결정된다.
이것이 바로 렉시컬 스코프이다.

함수 객체의 내부 슬롯 [[Environment]]

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

정의한 함수가 평가되어 함수 객체를 생성하는 시점은 함수가 정의된 환경, 즉 상위 함수가 평가 또는 실행되고 있는 시점이며, "실행중인 실행 컨텍스트"는 기준이 되는 함수의 "상위 함수 실행 컨텍스트"이기 때문이다.

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

또한 자신이 호출되었을 때 생성될 함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장될 참조값이다.

함수 객체는 내부슬롯 [[Envorionment]]에 저장한 렉시컬 환경의 참조, 즉 상위 스코프를 자신이 존재하는 한 기억한다.

클로저와 렉시컬 환경

const x = 1;

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

// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스텍에서 팝되어 제거된다.

const innerFunc = outer();

innerFunc() // 10

위 코드의 실행 결과는 10이다.
외부 함수보다 중첩 함수(내부 함수)가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료된 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다.

예제의 실행 컨텍스트 순서

outer 함수를 호출하면 outer 함수의 렉시컬 환경이 생성되고 outer 함수 객체의 [[Environment]]에 "전역 렉시컬 환경"을
outer 함수가 갖는 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조" 에 할당한다.
그리고 중첩 함수 inner가 평가된다.
inner 함수는 함수 표현식으로 정의했기 때문에 런타임에 평가된다.
(inner 함수가 호출되어 실행 컨텍스트가 생성된다는 말이 아니라, outer 함수의 렉시컬 환경의 일부로서 코드의 실행 단계에서 평가된다는 뜻)

이때 중첩 함수 inner는 자신의 [[Environment]] 내부 슬롯에 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 outer 함수의 렉시컬 환경을 상위 스코프로서 저장한다.

outer 함수의 실행이 종료되면 inner 함수를 반환하면서 outer 함수의 생명 주기가 종료된다.
즉 outer 함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거된다.
이때 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer 함수의 렉시컬 환경까지 소멸하는 것은 아니다.

outer 함수의 렉시컬 환경은 inner 함수의 [[Environment]]내부 슬롯에 의해 참조되고 있고, inner 함수는 전역 변수 innerFunc에 의해 참조되고 있으므로 G.C의 대상이 되지 않기 때문이다.
가비지 컬렉터는 누군가가 참조하고 있는 메모리 공간을 함부로 해제하지 않는다.

outer 함수가 반환한 inner 함수를 호출하면 inner 함수의 실행 컨텍스트가 생성되고, 실행 컨텍스트 스택에 푸시된다.

그리고 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에는 inner 함수 객체의 [[Environment]] 내부 슬롯에 저장되어 있는 참조값이 할당된다.

중첩함수 inner는 외부 함수 outer보다 더 오래 생존했다. 이때 외부 함수보다 더 오래 생존한 중첩 함수는 외부 함수의 생존 여부와 상관없이 자신이 정의된 위치에 의해 결정된 상위 스코프를 기억한다.

이처럼 중첩 함수 inner의 내부에서는 상위 스코프를 참조할 수 있으므로 상위 스코프의 식별자를 참조할 수 있고, 식별자의 값을 변경할 수도 있다.


자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로 모든 함수는 클로저다.

하지만 모든 함수를 클로저라고 하지 않는다.

클로저가 아닌 내부함수
1. 상위 스코프의 식별자를 참조하지 않는 경우.
대부분의 모던 브라주어즌 최적화를 통해 상위 스코프를 기억하지 않는다. 참조하지 않는 식별자를 기억하는 것은 메모리 낭비이기 떄문이다.
2. 외부 함수보다 일찍 소멸하는 경우
생명 주기가 종료된 외부 함수의 식별자를 참조할 수 있다는 클로저의 본질에 부합하지 않는다.

즉 클로저는, 중첩 함수가 상위 스코프의 식별자를 참조하며, 외부 함수보다 더 오래 유지되어, 자신의 렉시컬 스코프를 통해 외부 함수의 식별자를 참조할 수 있는 함수를 클로저라 부른다.

클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라 부른다.
클로저란 자유 변수에 묶여있는 함수를 뜻한다.


0개의 댓글