개발 이야기

클로저는 도대체 뭘까?

효빈 2024. 8. 29. 21:24

클로저란 무엇일까요?
 
MDN에 클로저가 무엇인지 검색을 해보면, 다음과 같이 설명되고 있습니다.

주변 상태에 대한 참조와 함께 묶인 함수의 조합.
내부 함수에서 외부 함수의 범위에 대한 접근을 제공.

 
위의 설명이 잘 와닿지 않으니, 조금 더 자세하게 설명해 보겠습니다.

실행 컨텍스트에 대해 잘 모르시는 분들은 이전 포스트를 먼저 읽어주세요🧐

실행 컨텍스트 글 바로 보기
 

코드의 실행 과정

클로저는, 자바스크립트 엔진이 아래의 코드를 실행하는 과정을 살펴보면서 이해해 봅시다.

let name = '개구리';

function makeFunc() {
    let name = '고양이';
    function displayName() {
        console.log(name);
    }
    return displayName;
}

let myFunc = makeFunc();
myFunc();

 
먼저 위의 코드를 실행하면, 이렇게 아래의 그림과 같이 전역 실행 컨텍스트가 생성되고, 선언문들을 읽어서 다음과 같이 렉시컬 환경의 환경 레코드에 저장합니다.

 
 
그다음으로는, makeFunc 함수가 호출이 되어, makeFunc 함수 실행 컨텍스트가 실행이 되죠.
아래의 그림으로 나타낼 수 있습니다. 마찬가지로, 아래의 그림처럼 함수 내부의 선언문들도 환경 레코드에 저장해 줍시다.

 
 
그리고, 해당 함수는 displayName 함수를 반환합니다. 그렇기 때문에 myFunc 변수에는 displayName 함수가 저장이 되죠.
displayName 함수가 반환되면, makeFunc 함수 실행 컨텍스트도 종료됩니다. 아래의 그림처럼 말이에요.

 
 
이제 myFunc 변수에 담긴 displayName 함수가 실행됩니다. 
함수가 실행되면 함수 실행 컨텍스트가 생성되고, displayName 함수 내부의 console.log가 실행되는데요,
 
displayName 함수 실행 컨텍스트에는 출력하고자 하는 값인 name 변수가 존재하지 않습니다.
따라서 외부 환경 참조를 통해 상위 렉시컬 환경을 참조해야 하는데요,
 
현재 makeFunc 함수 실행 컨텍스트는 종료된 상황이니, 아래의 그림처럼
이렇게 우선 전역 실행 컨텍스트의 환경을 참조해 보겠습니다.

 
그럼 console.log(name)을 출력하면 '개구리'가 출력이 될 텐데요,
실제로 코드를 실행하면 어떤 값이 출력되는지 아래의 그림을 통해 살펴보겠습니다.

 
'개구리'가 아닌 '고양이'가 출력이 되죠.
 
name 변수를 출력했을 때 '고양이'라는 값이 출력되려면, 앞서 봤던 그림처럼 displayName의 외부환경 참조는, 이미 종료된 makeFunc 함수 환경을 참조해야 합니다.
 
굉장히 이상한 상황이죠.
이러한 현상이 바로 '클로저'와 연관되어 있습니다.
 
좀 더 자세하게 살펴볼까요?
 

렉시컬 스코프, 내부 슬롯

자바스크립트의 식별자들은 자신이 선언된 위치에 따라 스코프를 결정하는 방법인 렉시컬 스코프를 따릅니다.
 
즉, 자신의 스코프를 결정하기 위해 자신이 어디에 선언되어 있는지에 대한 정보를 알고 있어야 합니다.
하지만, 이 실행 컨텍스트에는 식별자의 선언 위치에 대한 정보는 하나도 없습니다.
 
이렇게 함수가 자신이 선언된 위치를 기억하는 공간을 '내부 슬롯'이라고 부릅니다.

 
 
그리고 자바스크립트에서 실행 컨텍스트 내부의 외부 환경 참조는 이 내부 슬롯을 참조해 값을 저장합니다.
 
다시 코드로 돌아와서 살펴보면,
전역 실행 컨텍스트에 있는 makeFunc 함수의 내부 슬롯에는, makeFunc 함수의 상위 스코프인 전역 렉시컬 환경이,
그리고 makeFunc 함수 실행 컨텍스트에 저장된 displayName 함수의 내부 슬롯에는, 상위 스코프인 makeFunc 함수 렉시컬 환경이 저장되어 있습니다.
 
즉, 내부 슬롯에 저장된 랙시컬 환경이 자신에게 접근할 수 있는 범위인 것이죠.
아래의 그림으로 확인해 보면 쉽게 이해할 수 있습니다.

 
그다음으로는 makeFunc 함수 실행 컨텍스트가 종료되었고,
displayName 함수 실행 컨텍스트가 생성됐습니다.
 
아래의 그림처럼 말이에요.

 
그럼, 이 displayName 함수 실행 컨텍스트의 외부 환경 참조는, 전역 실행 컨텍스트의 환경 레코드를 참조하는 것이 아니라,
 
내부 슬롯에 저장되어 있는 자신의 상위 렉시컬 환경을 참조합니다. 이렇게 실행 컨텍스트의 외부 환경 참조는 '내부 슬롯'에 저장되어 있는 값을 참조해 값을 저장합니다.
 
따라서 우리가 출력하고자 했던 name 변수에는 '개구리'라는 값이 아니라, 이렇게 아래의 그림처럼 '고양이'라는 값이 저장되어, 코드 실행 결과 '고양이'가 출력이 되는 것입니다.

 
 
여기서 이 displayName이 바로 클로저입니다.
 

정의

클로저를 한 문장으로 정리해 보겠습니다.
 
displayName 함수처럼 자신의 외부에 있는 makeFunc 함수가 종료되었음에도 불구하고, displayName 함수가 이 외부 함수가 가지고 있는 변수를 참조할 수 있는 상황에서, 내부 함수가 바로 클로저입니다.
 
즉, 내부 함수가 외부 함수보다 오래 유지되어, 외부 함수는 종료되었지만, 내부 함수가 여전히 외부 함수의 스코프에 접근할 수 있는 상황에서 이 내부 함수를 '클로저'라고 부르는 것입니다.