[TIL] JavaScript의 클로저

August 03, 2021

자바스크립트의 클로저(closure) 개념을 제대로 이해하기 위해서는 먼저 자바스크립트의 스코프, 실행 컨텍스트, 프로토타입, 가비지 컬렉션 등에 대한 이해가 선행되면 좋을 것 같다. 각각의 자세한 내용은 따로 다루기로 하고 이 글에서는 클로저에 집중하기로 한다.

클로저

MDN은 Closure를 다음과 같이 정의한다.

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

함수와 렉시컬 환경의 “조합”이라는 건 무슨 의미며, 중첩 함수가 외부함수의 스코프에 접근할 수 있는 건 당연한 말 아닌가? 클로저의 원리와 쓰임새에 대해서 알아보며 의문을 해소해보자.

중첩 함수의 렉시컬 환경 (+ 가비지 컬렉션)

클로저에 대해서 찾다 보면 “이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다”는 말을 가장 먼저 발견할 수 있다. 클로저를 설명하는 대표적인 특징 중 하나이기 때문일 것 같은데 처음에는 그게 어떻게 가능한 건지 전혀 감이 오질 않았다. 그러나 자바스크립트의 렉시컬 환경에 대해서 알고나니 비로소 이해할 수 있었다.

스코프 글에서 자바스크립트의 모든 식별자는 자신이 선언된 위치에 의해 스코프가 결정된다고 했다. 중첩 함수는 외부 함수의 몸체 안에서 정의된 함수를 뜻한다. 따라서 중첩 함수의 렉시컬 환경은 상위 스코프인 외부 함수의 몸체가 되는 것이다.

여기까지는 익숙한 내용이다. 그런데 외부 함수의 생명 주기가 끝나 소멸된다면, 그 함수의 몸체 내부를 참조하는 중첩 함수의 스코프도 함께 소멸되는 것이 아닌가? 그러면 참조할 외부 함수의 변수도 없는 것 아닌가? 라는 의문이 든다.

이 부분이 클로저가 이상한(?) 이유였는데 이 의문을 해소하기 위해서는 자바스크립트의 내부 슬롯, 그 중에서도 [[Environment]] 내부 슬롯에 대해 알 필요가 있었다.

(자세한 내용은 프로토타입과 프로퍼티 어트리뷰트에서 다루는 것으로 하고) 간단하게 말하자면 자바스크립트의 객체가 생성될 때 엔진의 내부 로직에 의해 객체의 내부 깊~~은 곳에 여러가지 프로퍼티가 함께 생기는데, 함수의 내부 슬롯 중 [[Environment]]라는 곳에는 함수의 렉시컬 환경이 저장된다는 내용이다. 즉, 함수는 자신이 태어난 환경을 몸 속 깊은 곳에서 기억하고 있는 것이다. 따라서 중첩 함수가 평가되어 객체가 생성될 때, 중첩 함수가 정의된 외부 함수 렉시컬 환경이 중첩 함수의 내부 슬롯에 저장되게 된다.

여기까지는 사실..

위에서 “중첩 함수가 외부함수의 스코프에 접근할 수 있는 건 당연한 말 아닌가?”라는 질문에 대해 어떻게 그것이 가능한 건지에 대해 좀 더 자세하게 살펴본 내용에 해당된다. 동시에 클로저의 정의에 대입하면 “자바스크립트의 모든 함수는 클로저”임을 뜻하는 내용이기도 했다.

하지만 일반적으로 모든 함수를 클로저라고 부르지는 않는다. 모든 함수가 외부 함수의 렉시컬 환경을 기억하기 때문에 외부 함수가 소멸해도 렉시컬 환경이 여전히 메모리 공간에 남아있다면, 메모리 공간은 죽은 함수들의 정보들로 가득찰 것이다. 만약 중첩 함수가 외부 함수의 어떤 요소도 참조하지 않아서 필요가 없는 경우에도 말이다.

따라서 외부 함수의 변수가 중첩 함수에 의해 참조되지 않는 경우, 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않는다고 한다. 이를 통해 메모리 낭비를 방지할 수 있는 것이다.

외부 함수를 참조하고 있는 중첩 함수에 한해서, 생명 주기가 종료된 외부 함수의 식별자를 참조할 수 있다는 특수한 기능을 중점으로 클로저를 인식하면 될 것 같다.

다시 돌아와서,

클로저에 대한 진짜 궁금증은 생명 주기가 끝난 외부 함수 즉, 실행 컨텍스트가 실행 컨텍스트에서 제거된 외부함수의 스코프가 어떻게 아직도 메모리에 존재하고 있느냐일 것이다.

그 해답은 자바스크립트의 가비지 컬렉션 알고리즘에 있었다. 가비지 컬렉션 글에서 자바스크립트의 가비지 컬렉터는 ‘도달 가능한’ 객체는 메모리에서 해제하지 않는다고 했었다.

여기서 외부 함수의 렉시컬 환경은 중첩 함수의 내부 슬롯 [[Environment]]에 의해 참조되어 도달 가능한 상태이다. 따라서 함수는 소멸하였어도 그 렉시컬 환경은 여전히 메모리 공간에 남아있어 중첩 함수에 의해 참조될 수 있는 것이다. 함수는 죽었지만 렉시컬 환경은 죽지 않았다. 다른 함수의 마음 속에 살아있었기 때문이다..⭐

이것이 중첩 함수가 정의된 스코프 밖에서 실행될 때에도 기억하고 있는 스코프의 변수들에 접근할 수 있는 이유다.

상태 은닉

죽은 줄로만 알았던 외부 함수의 변수들이 사실은 메모리 공간에 살아있었다는 충격적인 소식을 들었다. 그럼 살아있으니 그 값들을 가지고 뭔가를 할 수 있지 않을까? 할 수 있다. 그래서 가능한 것이 의도치 않은 변경을 방지하기 위해 상태를 안전하게 은닉하도록 돕는 클로저 활용법이다.

let cnt = 0;

const plus = function () {
    cnt++;
    return cnt;
}

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

위와 같이 전역 변수 cnt의 값을 1씩 더해주는 함수가 있다고 하자.

만약 코드가 아주 복잡해져서 cnt 변수를 실수로 조작한다면, 기대했던 것과 다른 값을 얻게 될 위험이 있다. 전역 변수의 위험성이다.

let cnt = 0;

const plus = function () {
    cnt++;
    return cnt;
}

console.log(plus());	// 1
console.log(plus());	// 2
console.log(plus());	// 3
cnt = 100;
console.log(plus());	// 101

이와 같은 경우를 방지하기 위해 클로저를 유용하게 사용할 수 있다. 외부 함수 내부에 cnt 변수를 선언하고 중첩 함수로 참조하여 관리한다면, 중첩 함수를 이용하지 않고서는 외부에서 해당 변수에 접근할 방법이 없기 때문이다.

function closure () {
    let cnt = 0;
    const plus = function () {
        cnt++;
        console.log(cnt);
    }
    return { plus };
}

const hiddenCnt = closure();
console.log(hiddenCnt.plus);	// 1
console.log(hiddenCnt.plus);	// 2
console.log(hiddenCnt.plus);	// 3
conole.log(cnt);	// Uncaught ReferenceError: cnt is not defined

이제 cnt 변수에 접근하여 값을 조작할 수 있는 방법은 소멸된 외부 함수에서 정의된 plus 함수를 사용하는 방법뿐이다. 클로저를 활용해 cnt 변수를 은닉하여 의도치 않은 값의 변경은 차단했지만, 특정 함수만을 사용하여 여전히 원하는 연산을 수행할 수 있게 되었다.

결론 및 느낀점

클로저는 죽어버린 함수를 기적처럼 소생시키는 마법사는 아니었다. 클로저를 사용한다면 결국 메모리 어딘가에 해당 값들이 죽지 않고 여전히 살아남아 있다는 뜻이니까.

따라서 클로저를 가장 효율적으로 활용할 수 있는 적재적소를 판단하는 능력이 필요할 것 같다.

파편적으로만 학습했던 자바스크립트의 개념들은 결국 모두가 긴밀하게 연결되어 있었다. 당장 이해가 되지 않는 내용이 있다면, 연관된 주변 개념들 혹은 굵직한 자바스크립트의 동작 원리를 다시 한 번 확실하게 이해하고 돌아와서 완벽하게 이해할 필요가 있다.

참고

MDN Web Docs | 클로저
변수의 유효범위와 클로저
모던 자바스크립트 Deep Dive | 위키북스 | 이웅모


Profile picture

42KIM A person trying to create something.
👉Github