JavaScript:

[JavaScript] 클로저(closure)

칠일오.

⚠️ 모던 자바스크립트 딥 다이브를 읽고 작성한 글입니다.

 

클로저를 설명하기에 앞서 명확하게 기억해야 할 것이 있다. 바로 자바스크립트는 함수를 어디에 정의했는지에 따라 상위 스코프를 결정한다는 것이다. (이를 렉시컬 스코프라고 한다.) 즉, 함수를 어디서 호출하는지는 함수의 상위 스코프 결정에 어떠한 영향도 주지 않는다는 말이기도 하다. 

👀 짚고 넘어가기
렉시컬 환경은 자신의 '외부 렉시컬 환경에 대한 참조'를 통해 상위 렉시컬 환경과 연결되는 것이고, 이를 스코프 체인이라 한다. 그리고 상위 렉시컬 환경이 곧 상위 스코프이다. 이때 함수는 자신의 내부 슬롯 [[Environment]]에 상위 스코프의 참조를 저장한다.

 

클로저란?

서론이 길었는데 그럼 클로저는 도대체 무엇일까? 우선 예제부터 살펴보자.

const x = 1;

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

const innerFunc = outer();
innerFunc(); // 10

실행 순서는 x가 전역 변수로 선언과 할당이 이루어지고, outer 함수도 전역 함수로서 선언이 되고, innerFunc 변수에 할당되어 있는 outer 함수가 실행이 될 것이다. outer 함수는 중첩 함수 inner 함수를 반환하고 생명 주기를 마감한다. 즉, outer 함수는 실행 컨텍스트 스택에서 제거된다. 그렇다면 왜 결과가 10이 나올 수 있는 것일까?

 

이것이 바로 클로저이다. 클로저란, 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이는 짚고 넘어가기에서 봤듯이 [[Environment]]에 상위 스코프를 저장하기 때문에 언제든 참조할 수 있으며 바인딩된 값을 변경할 수 있다. 즉, outer 함수는 실행이 종료되었지만 outer 함수의 렉시컬 환경은 남아있다. inner 함수의 [[Environment]]에 의해 참조되고 있고, inner 함수는 전역 변수 innerFunc에 의해 참조가 이루어지고 있기 때문에 가비지 컬렉션 대상이 되지 않기 때문이다.

 

그럼 이렇게도 생각할 수 있을 것이다. 중첩 함수면 모두 클로저인가? 

function outer() {
	const x = 1;
    const y = 2;
    
    const inner = function() {
    	const z = 3;
        console.log(z)
    }
    return inner;
}

const innerFunc = outer();
innerFunc(); // 3

위 예제를 보면 inner 함수가 외부 함수의 어떤 것도 참조하고 있지 않다. 이럴 경우 모던 브라우저는 메모리를 하지 않기 위해 최적화를 통해 상위 스코프를 기억하지 않는다. 따라서 inner 함수는 클로저가 아니다. 반대로 중첩 함수가 외부 함수를 참조하고 있더라도 외부 함수보다 일찍 소멸된다면 클로저라고 보기 어렵다. 다시 정리해 보자.

  • 중첩 함수가 상위 스코프의 식별자를 참조하고 있어야 한다.
  • 중첩 함수가 외부 함수보다 더 오래 유지되어야 한다.

이 두 가지 조건에 모두 부합해야 클로저라고 할 수 있다.

그럼 만약 상위 스코프의 여러 식별자 중 하나의 값만 참조한다면 참조되지 않는 값들은 어떻게 될까?

예상대로 모던 브라우저는 클로저가 참조하고 있는 식별자만을 기억한다. 이때 클로저에 의해 참조되는 상위 스코프의 변수를 '자유 변수'라고 부르며, '함수가 자유 변수에 의해 닫혀있다(closed)'라는 의미에서 클로저(closure)라고 작명한 것이다..!

즉, 클로저란 자유 변수에 묶여있는 함수라고도 정의할 수 있다.

 

클로저의 활용

이제 클로저를 어떻게 활용하는지 알아보자. 

클로저는 상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.

이해가 안 될 땐 예제를 보자.

const increase = function() {
	let num = 0;
    return ++num;
}

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

increase 함수만이 변경하도록 코드가 짜여있지만 막상 실행하면 console이 모두 개별적으로 실행되어 1만 출력하는 걸 확인할 수 있다. 그렇다면 console이 순차적으로 실행될 때 increase의 값도 이전 상태를 유지하려면 어떻게 해야 할까? 그렇다 이럴 때 클로저를 사용하여 해결할 수 있다.

const increase = (function() {
	let num = 0;
    return function() {
    	return ++num;
    }
}());

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

increase 함수 내부에 있는 클로저는 즉시 실행 함수의 렉시컬 환경을 상위 스코프로 기억한다. 이 말은 곧 num을 언제 어디서 호출하든 참조하고 변경할 수 있다는 것이다.