JavaScript

7.4 클로저

문정훈 2022. 1. 30. 01:52

1. 예시를 통해 클로저 정의하기 

const x = 1;

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

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

위 예시를 평가와 실행 과정으로 생각해보면 위 코드의 평가 과정에서는 

x는 일시적 사각지대 상태가 되며 outer 라는 key의 값으로 함수 객체가 생성되어 저장된다. 이때 outer의 상위 스코프가 [[Environment]]의 값에 할당된다. 

그리고 innerFunc은 일시적 사각지대 상태가 된다.

이제 '실행' 과정을 보자

런타임이 진행되면 x =1; 이 할당되고 innerFunc = outer(); 이 부분이 실행된다. 

outer() 함수가 호출 되었으므로 outer 함수의 '평가' -> '실행' 과정이 진행된다. outer 함수의 실행 과정이 모두 마친 뒤 inner 변수의 값으로 할당된 함수를 리턴하여 innerFunc가 가지게 된다. 

그리고 innerFunc(); 문이 실행된다. 

여기서 클로저를 모른다면 의문을 가질 수 있다.

interFunc 에는 outer의 실행 결과로 outer의 내부 함수가 리턴되고 outer 함수가 종료되면 메모리에서 outer 함수가 사라질 것이고 그럼 outer 내부에 선언된 inner 역시 메모리에서 사라지므로 innerFunc(); 실행은 안되는 것이 아니냐? 생각할 수 있다. 

이제 클로저에 대해 정리해보자.

 

※ 클로저의 이론적 정의 

위 예제와 같이 외부 함수(outer) 보다 중첩함수(inner)가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이런 중첩함수(inner)를 클로저라고 한다. 

 

위 코드를 다시 정리해보면 위 소스코드의 평가 과정에서 outer 함수의 상위 스코프가 결정되고 outer() 호출문이 실행되면서 inner 함수의 상위 스코프가 outer 함수로 결정된다. 이 상위 스코프는 [[Environment]] 내부 슬롯의 값이다. 

그리고 innerFunc는 중첩 함수인 inner 함수를 가리킨다. 

 

innerFunc는 inner를 가리키고 inner의 [[Environment]]의 내부 슬롯 값으로 outer를 가리키므로 innerFunc(); 가 호출 되는 시점에 inner함수는 메모리에 온전히 살아있게 되며 inner 함수에서 참조하고 있는 outer의 값 역시 메모리에 살아있게 된다. 이때 중요한 것은 outer 함수의 모든 필드가 메모리에 살아있는 것이 아니라 inner가 참조하고 있는 outer의 필드만 메모리에 살아있게 된다. (메모리의 효율성을 위해..)

우선 여기까지 정의하고 더욱 자세한 이야기는 아래에서 해보자.

정리하면, 누군가에 의해 참조되고 있다면 가비지 컬렉션의 대상이 되지 않는다.

가비지 컬렉터는 누군가가 참조하고 있는 메모리의 공간을 함부로 해제하지 않는다. 

 

 

2. 클로저 정의 심화

위 1번 에서 클로저란 무엇인지 예시를 통해 1차적으로 정의했다. 

클로저의 이론적 정의를 더욱 자세히 알아볼 필요가 있다. 

이론적인 클로저실제 클로저로 간주하는 것과 구별을 명확히하여 이해해야한다. 

 

이론적 클로저의 정의는 외부 함수보다 중첩 함수가 더 오래 유지되어 중첩함수가 이미 생명주기가 종료된 외부 함수의 변수를 참조할 수 있으며 그러한 중첩 함수를 클로저라 하였다.

이 이론적인 클로저의 정의만 본담녀 자바스크립트의 모든 함수는 자신의 상위 스코프를 기억하므로 이론적으로 모든 함수는 클로저가 된다.

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

아래 2가지 경우는 클로저가 아닌 예시가 된다. 

 

 

경우1)  

const x = 1;

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

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

위 예시를 보면 중첩 함수에서 외부함수의 어떠한 변수(필드)도 사용하지 않는다.

이런 경우라면 중첩 함수가 사용하지도 않는 외부 함수를 기억하는 것은 메모리 낭비가 된다.

따라서 js에서는 이처럼 중첩 함수에서 외부 함수의 필드를 사용하지 않는다면 외부 함수를 메모리에서 제거한다. 

innerFunc에 의해 중첩 함수가 참조되므로 중첩 함수는 살아있게 되고 외부 함수는 아무도 참조를 하지 않기 때문에 가비지 컬렉터의 대상이 되는 것이다. 

 

 

경우2)

const x = 1;

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

 

위 경우는 중첩 함수가 외부 함수보다 생명 주기가 짧다. 즉 외부 함수가 소멸하기 전에 중첩함수가 먼저 소멸된다. 이 경우 역시 중첩 함수를 클로저라고 하지 않는다. 


 

클로저란 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 주첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이다. 

 

 

3. 클로저 마무리

일반적으로 클로저라고 하는 것을 마지막으로 정리해보면 

가비지 컬렉터는 누군가가 참조하고 있는 메모리의 공간을 함부로 해제하지 않는다. 하지만 아무도 참조하고 있지 않는 대상에 대해서는 메모리에서 제거된다. (함수이던 변수이던)

 

가비지 컬렉터의 내용을 잘 적용시키며 클로저를 이해하면 이해가 쉬워진다. 

중첩함수가 외부 함수보다 생명주기가 길며 중첩 함수에서 외부 함수의 필드를 참조한다면

중첩함수의 [[Environment]]의 값으로 외부 함수를 기억하기 때문에 외부 함수가 생명주기가 끝난다고 해서 메모리에서 제거 되지 않는다.

하지만 중첩 함수에서 사용하는 외부 함수의 필드 값만 제거 되지 않는것이며 중첩 함수에서 사용하지 않는 외부 함수의 필드 값은 메모리에서 제거 된다. 

(외부 함수를 기억하는 것이 아닌 중첩함수에서 사용하는 외부 함수의 필드만을 기억하는 것이다.)