클로저의 의미 및 원리 이해

2024년 6월 20일 by임재도
thumbnail_5-1.png

클로저(Closure), JS를 초급자를 넘어서 깊이 사용하다보면 자주 접하게 되는 용어입니다.
보통 코드 은닉 등에서 많이 활용하곤 하는데, 과연 여러분은 그 원리를 제대로 이해하고 계신가요?
같이 한번 클로저의 세계로 다이빙 해봅시다!

클로저(Closure)의 어원은?

영어에서 클로저(Closure)말은 정말 다양하게 쓰이고 있습니다.

네이버 사전에서 검색을 해보면 다음과 같은 뜻이 있죠.

💡
Closure
  1. 1.
    명사
    (공장·학교·병원 등의 영구적인) 폐쇄(되는 상황)
  2. 2.
    명사
    (도로·교량의 일시적) 폐쇄
  3. 3.
    명사
    (힘든 일의) 종료[종결]

즉, 보통은 Close의 명사형으로써 폐쇄, 종료, , 닫힘 등의 의미로 쓰인다는 것을 볼 수 있죠.

이걸 기억하고 보시면 클로저가 상당히 어렵게 다가옵니다. JS에서 클로저의 어원은 위에 기반한 것이 아니라, 수학과 컴퓨터 과학에서 폐합을 의미하는 Closure에서 유래되었기 때문이죠.
폐합은 보통 ~~에 닫혀있다 라는 표현으로 많이 쓰입니다.

예를 들어서, 정수 집합은 덧셈 연산에 대해 닫혀있다. 혹은 정수 집합은 덧셈 연산에 대해 폐합을 가진다. 와 같은 표현으로 쓰입니다.

closure_mean_1
closure_mean_2

위와 같은 그림을 보시면 아시겠지만, 정수(Integer) 연산의 경우를 보면, 사칙연산(+,-,*,/)를 해도 그 결과 값은 정수 집합에 다시 해당하게 됩니다.

즉, 어떤 행위의 결과가 자신의 집합으로 귀결되는 것을 Closure라고 합니다.

JS에서의 클로저(Closure)는 어떻게 표현되는가?

그러면 저희가 사용하는 js에서의 클로저도 한번 생각해볼까요?

closure_graph_1

JS에서 클로저를 그림으로 그리면 위와 같이 표현할 수 있습니다.
outer함수 내부에 있는 inner함수가 outer함수 외부에서 outer함수 범위(scope)안에 있는 변수 a를 참조하는 것이지요.

말이 어렵죠? 당장은 이해하지 않으셔도 됩니다.

이제부터 차근차근 알아보면서 단계별로 이해를 하고, 다시금 이 그림을 살펴보는 과정을 통해 함께 이해해봅시다!

클로저(Closure)를 다른 사람들은 어떻게 표현하고 있는가?

클로저(Closure)는 사실 자바스크립트의 고유의 개념이 아닙니다.
여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성입니다!

자바스크립트의 고유 개념이 아니다보니 ECMAScript 명세에서도 클로저의 정의를 다루지 않고 있습니다.
이것 때문만은 아니지만, 다양한 문헌에서 제각각 클로저를 다르게 정의하고 있습니다.
또한, 클로저를 설명하는 문장 자체도 이해하기 어려운 단어가 등장하는 경우가 많아, 이를 보고 한번에 클로저가 무엇이다 이해하기는 어렵습니다.

그렇지만, 식견을 넓히기 위해서, 그리고 감을 잡기 위해서 한번 살펴볼까요?

💡
다양한 책들에서의 클로저(Closure) 표현
  • 자신을 내포하는 함수의 컨텍스트에 접근할 수 있는 함수- 더글라스 크록포드, <<자바스크립트 핵심 가이드>>, 한빛미디어(p68)
  • 함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 것- 에단 브라운, <<러닝 자바스크립트>>, 한빛미디어(p196)
  • 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수- 존 레식, <<자바스크립트 닌자 비급>>, 인사이트(p116)
  • 이미 생명 주기상 끝난 외부 함수의 변수를 참조하는 함수- 송형주 고현준, <<인사이드 자바스크립트>>, 한빛미디어(p157)
  • 자유변수가 있는 함수와 자유변수를 알 수 있는 환경의 결합- 에릭 프리먼, <<Head First Javascript Programming>>, 한빛미디어(p534)
  • 로컬 변수를 참조하고 있는 함수 내의 함수- 야마다 요시히로, <<자바스크립트 마스터북>>, 제이펍(p180)
  • 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수- 유인동, <<함수형 자바스크립트 프로그래밍>>, 인사이트(p31)

어떤가요? 좀 와닿으시나요?

문장만 놓고 이해하기는 쉽지 않죠?

이 때문에 본질을 깨닫고 나면 의외로 쉬운 개념인데도, 어딘가 갈증이 해소되지 않은 것처럼 쉽게 이해가 가지 않는 부분이 바로 클로저(Closure)입니다.

저도 스터디하는 책에서 설명하는 것처럼, 클로저의 일반적인 의미를 파악하고, 다양한 사례로 성질을 살펴본 후 마지막에 이를 재조합해서 이해하기 쉬운 문장으로 바꿔보도록 하겠습니다.
그리고 끝에 가서는 클로저를 그림으로 표현한 것을 다시 살펴보면서 이해해보도록 합시다!

MDN의 정의를 통해 살펴보기

💡
MDN(Mozilla Developer Network)에서의 정의
  • "A closure is the combination of a function and the lexical environment within which that function was declared."
  • "클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상"

MDN에 따른 정의는 위와 같습니다.

Lexical Environment 기억나시나요? 저희가 2장에서 실행 컨텍스트(Execution Context)를 배울 때 다루었던 내용입니다.
여기에서 lexical environment라고 함은 실행 컨텍스트의 구성 요소 중 하나인 outerEnvironmentReference에 해당합니다.

LexicalEnvironmentenvironmentRecordouterEnvironmentReference에 의해 변수의 유효범위인 스코프가 결정되고 스코프 체인이 가능해진다는 걸 기억하실겁니다.

어떤 컨텍스트 A에서 선언한 내부함수 B의 실행 컨텍스트가 활성화 된 시점을 생각해봅시다.

LexicalEnvironment 예제
var A = function() {
  var a = 0;
  var B = function() {
    let b = 0;
  };
};

closure_graph_2

위와 같은 코드가 있을 때, 함수 A에서 변수 b에 접근하지 못하지만, 함수 B에서 변수 a에 접근할 수 있습니다.
이는 함수 BouterEnvironmentReference함수 A를 참조하고 있기 떄문입니다.

여기서 combination의 의미를 이해할 수 있습니다.
다만, 내부 함수 함수 B함수 ALexicalEnvironment를 언제나 참조하는 것은 아닙니다.
내부 함수에서 외부 변수를 참조하지 않는다면 combination이라고 할 수 없습니다.

내부함수에서 외부 변수를 참조하는 경우에 한해서만 combination, 즉 '선언될 당시의 LexicalEnviroment와의 상호 관계`가 의미가 있습니다.

closure_graph_1

다시한번 위 그림을 봅시다.

지금까지 파악한 내용에 따르면, 클로저(Closure)"어떤 함수에서 선언한 변수를 참조하는 내부 함수에서만 발생하는 현상"이라고 볼 수 있습니다.

사례를 통해 살펴보기

지금부터는 조금 더 와닿을 수 있도록 사례를 통해 살펴보겠습니다.

일반적인 상황에서의 콜스택 흐름

외부 함수의 변수를 참조하는 내부 함수(1)
var outer = function() {
  var a = 1;
  var inner = function () {
    console.log(++a);
  };
  inner();
};
outer();

inner 함수 내부에서는 a를 선언하지 않았기 떄문에 environmentRecord에서 값을 찾지 못하므로, outerEnvironmentReference에 지정된 상위 컨텍스트인 outerLexicalEnvironment에 접근해서 다시 a를 찾습니다.

outer함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들(a, inner)에 대한 참조를 지웁니다. 이에 따라, 각 주소에 저장되어 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 가비지 컬렉터의 수집 대상이 될 것입니다.

context_empty

VariableEnvrionmentThisBinding은 생략한 콜스택 및 실행 컨텍스트를 도식화한 그림입니다. 일반적인 함수 내부에서의 동작이며, 별 다른 특별한 현상은 보이지 않습니다.

클로저 발생 시의 콜스택 흐름

외부 함수의 변수를 참조하는 내부 함수(1)
var outer = function() {
  var a = 1;
    var inner = function () {
      console.log(++a);
    };
  inner();
};
outer();

앞서 살펴본 코드입니다.
이제부터 이를 수정하면서 살펴봅시다.

inner 함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없습니다.
outer environmentReference에는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조 복사됩니다.
inner 함수outer 함수 내부에서 선언되었으므로, outer 함수LexicalEnvironment가 담길 것입니다.
저희가 2장에서 배웠던 스코프 체이닝(Scope Chaining)에 따라서 outer 함수에서 선언한 변수 a에 접근해서 1만큼 증가시킨 후 그 값인 2를 반환하고, inner 함수의 컨텍스트가 종료됩니다.
10번째 줄도 마찬가지 방식으로 3을 반환합니다.

의문 사항과 가비지 컬렉터의 원리

한가지 의문점이 들지 않으신가요?

🤔

의문 사항

  • inner 함수의 실행 시점에는 outer 함수는 이미 종료된 상태인데 어떻게 outer 함수LexicalEnvironment에 접근할 수 있는 것일까요?

다음을 기억합시다.

💡
가비지 컬렉터(Garbage Collector)의 원리
  1. 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다.

언젠가 inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReferenceouter 함수LexicalEnvrionment를 필요로 하기에 수집 대상에서 제외됩니다.
그 덕분에 inner 함수변수 a에 접근할 수 있는 것입니다.

콜스택 흐름을 그림으로 이해하기

지금까지의 과정을 그림으로 한번 이해해볼까요?

context_empty

정리

앞서서 클로저는 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상이라고 했습니다.
마지막으로 살펴본 것을 떠올리면, 여기서는 outer 함수LexicalEnvironment에 속하는 것 중 변수 a가 대상에서 제외되었습니다.
이처럼 함수의 실행 컨텍스트가 종료된 후 LexicalEnvironment가 가비지 컬렉터의 수집 대상에서 제외되는 경우는 지역변수를 참조하는 내부함수가 외부로 전달된 경우가 유일합니다.

"어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상"이란 "외부 함수의 LexicalEnvironment가 가비지 컬렉팅되지 않는 현상"을 의미합니다.

이를 바탕으로 정의를 고쳐보면 아래와 같습니다.

💡
클로저의 정의
  1. 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상

closure_graph_1

다시한번 처음에 살펴본 도식도를 봅시다.
어때요? 이제 도식도가 이해가 되시나요?

주의사항

마지막으로 한가지 생각해볼게 있습니다.

🤔

생각해봅시다!

  • 외부로의 전달이 과연 return만을 의미할까?

이는 코드로 살펴보면 편하게 이해할 수 있습니다.

setInterval/setTimeout
(function() {
  var a = 0;
  var intervalId = null;
  var inner = function () {
    if (++a >= 10) {
      clearInterval(intervalId);
    }
    console.log(a);
  };
  intervalId = setInterval(inner, 1000);
})();

별도의 외부 객체인 window의 메서드(setTimeout 또는 setInterval)에 전달할 콜백 함수 내부에서 지역변수를 참조합니다.

두 상황 모두 지역변수를 참조하는 내부함수를 외부에 전달했기 때문에 클로저입니다.