상세 컨텐츠

본문 제목

JavaScript | 실행 컨텍스트 (3) - 클로저

프로그래밍 언어/JavaScript

by hyuga_ 2024. 3. 29. 16:24

본문

인터넷에서 클로저의 정의를 둘러보다보면 잘 이해가 가지 않는다. 굉장히 모호하게 서술되어있기 때문이다. 개념 자체가 모호해서 그런 것일 거다. 내가 책에서 이해한 방식대로 한번 정리하는 것이 필요할 것 같다. 클로저를 활용하는 방식은 다양한데, 솔직히 말하면 내가 아직 거기까지 다루기엔 시기상조인 것 같다. 그래서 이번에는 클로저가 어떤 개념인지를 명확히 이해하는 것에만 초점을 맞추려고 한다. 

 


클로저(Closure)란?

클로저에 대한 서술은 저자마다 상이한데, 코어 자바스크립트 저자에 따르면 그나마 다음 문장들이 잘 정의한 것에 가깝다고 한다.

  • 클로저는 함수를 선언할 때 만들어지는, 유효범위가 사라진 후에도 호출할 수 있는 함수 - <자바스크립트 닌자 비급>, p116
  • 클로저는 이미 생명 주기상 끝난 외부 함수의 변수를 참조하는 함수 - <인사이드 자바스크립트>, p157
  • 클로저는 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수 - <함수형 자바스크립트 프로그래밍>, p31

MDN에서는 클로저를 '함수와 그 함수가 선언될 당시의 렉시컬 환경의 상호관계에 따른 현상'이라고 서술하고 있다. 상호관계라는 말이 성립하려면, 해당 함수가 외부 컨텍스트, 즉 렉시컬 환경의 변수를 사용하고 있다는 말일 것이다.

 

 

선정리 후설명

클로저는 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다. 이는 LexicalEnvironment(내부함수가 참조해야 할 변수들이 포함된)이 GC에 의해 사라지지 않음으로써, 내부함수 실행의 일관성을 보장하는데 도움이 된다. 내부함수를 외부로 전달하는 방법에는 함수를 return하는 경우뿐 아니라, 콜백으로 전달하는 경우도 포함된다. 

 

GC를 공부하다보면, 메모리를 관리하기 위해 클로저를 잘 관리하라는 내용이 있다. 클로저는 그 본질이 메모리를 차지하는 개념(GC가 청소하지 않도록 하는)이므로 더이상 사용하지 않게 된 클로저에 대해서는 null 값을 할당하는 식으로 해제하는 습관을 들여놓는 것이 중요하다. 

 

 

 

클로저 현상 이해하기

코드 예시를 통해 클로저를 관찰해보자. 해당 예시는 우테코 테코톡 영상을 참고했다.

 

const x = 1;

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

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

 

위 코드를 보고 콘솔에 무엇이 출력될지 생각해보자.

 

클로저를 모른다면 result() 에서 1을 출력할 거라고 생각되지만, 실제로는 10을 출력한다. '아니.. inner를 return 할 때에는 이미 outer 함수의 생명주기가 끝나고 x = 10 이라는 변수도 사라진 거 아닌가?!' 라고 생각될 수도 있다. 그러나 이것이 클로저 ... 내부 함수인 inner의 실행이 예측 가능하고 일관적이기 위한 조치라고 생각하면 된다. 

 

이처럼 이미 생명주기가 끝난 상위 스코프를 참조하는 상황에서, inner 함수를 흔히 클로저 라고 부른다.

(클로저는 함수라기보다는 현상이라고 표현하는 게 더 알맞다고 생각한다)

 

출처: 우테코 테코톡

 

 

구조를 간략 묘사하면 위와 같은데, outer 함수의 생명주기가 끝남에 따라 실행 컨텍스트 스택에서 pop되지만, outer 함수의 렉시컬 환경까지는 사라지지 않는다. return으로 외부로 전달한 inner 함수의 실행 컨텍스트가 outer의 렉시컬 환경을 참조하게 되며, outer의 렉시컬 환경은 참조되고 있으므로 GC에 의해 mark 되지 않는다.

 

inner 함수는 실행 컨텍스트 내부적으로(outerEnvironmentReference에서) 상위 스코프 참조를 유지하고 있기 때문에 이를 참조하여 생명주기가 끝난 외부함수의 변수에 접근이 가능하다.

 

클로저는 본인의 상위 스코프에서 참조하는 식별자만을 기억한다. 클로저에 의해 참조된 변수를 자유변수(Free variable)라고 한다. (위 예시에서 x = 10)

 

 

자유변수는 계속 유지되어 참조할 수 있다. 아래 코드를 보자. 

function makeCounter() {
    let count = 0; // `makeCounter`의 지역 변수 `count`

    return function() {
        return count++; // 내부 함수에서 외부 함수의 변수 `count`에 접근
    };
}

const counter = makeCounter();
console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2

 

위 예시에서 makeCounter 함수는 내부 함수를 반환한다. 이 내부 함수는 makeCounter 함수의 지역 변수 count에 접근할 수 있는 클로저를 형성한다. 이렇게 반환된 함수를 통해 count 변수는 makeCounter 함수가 실행을 마친 후에도 계속해서 접근할 수 있게 되며, 각 함수 호출 사이에 상태를 유지할 수 있다.

 

 

 

 

 

관련글 더보기