탱구탱구 개발자 일기

자바스크립트 함수에서 클로저는 처음에 이해하기 힘든 개념 중 하나이다.

 

클로저의 정의

 

함수와 그 함수가 선언된 어휘적 환경(렉시컬 환경)의 조합이라고 MDN web 문서에 설명하고 있다.. 무슨 말일까??  

 

먼저 클로저를 이해하기 위해서는 알아야 할 중요한 개념들이 있다. 바로 식별자 결정(identifier resolution), 자유, 속박 변수, 열린, 닫힌 함수, 스코프(scope)이다. 하나씩 천천히 살펴본 후 마지막에 클로저에 대해 이해하도록 하자.

 

식별자 결정

 

자바스크립트에서 식별자를 결정하는 것은 매우 중요하다. 식별자를 결정해야 해당 변수의 유효 범위를 알 수 있기 때문이다. 식별자 결정이란 변수 a가 어디에서 선언된 변수인지를 결정하는 작업을 의미한다. 만약 전역 환경, 전역 환경에서 선언된 함수의 내부 변수, 그 함수에서 선언된 함수에 각각 같은 이름을 가진 변수가 선언되어 있다면?? 우리는 각각의 변수가 어떤 참조값을 갖고 있는지 결정해야 한다. 기본적으로 자바스크립트의 식별자 결정 규칙은 동일한 레벨에서 같은 이름을 갖는 변수들 중 좀 더 안쪽에 선언된 변수를 사용한다는 것이다.

 

스코프

아래와 같은 코드가 있다. 함수 aa()를 호출한 결과는 "ABC"를 출력한다. 왜 그럴까?? 그러기 위해선 스코프에 대해 알아야한다. 스코프는 말 그대로 범위를 말하는데 프로그래밍 언어에서는 보통 유효 범위의 개념으로 많이 쓰인다. 스코프에 대한 설명은 이 글을 참고해보자.

// 어휘적 유효 범위

var a = "A";

function aa() {
    var b = "B"

    function bb() {
        var c = "C"
        console.log(a + b + c);
    }

    bb();
}

aa(); // "ABC"

기본적으로 자바스크립트는 대다수의 언어들과 동일하게 어휘적(lexical) 유효 범위(scope)를 규칙으로 한다. 어휘적 유효 범위는 프로그램이 작성된 문맥(context)에서 결정하는 것을 말하는데 자바스크립트는 함수 레벨과 블록 레벨의 스코프를 가지고 있다. 예제 코드에선 함수 레벨을 따르는 var 변수 키워드로 작성되어 있고 그 규칙에 따라 ABC가 출력된 것이다. 위 예제 코드에서 블록 레벨을 따르는 let 변수 키워드로 선언하더라도 동일한 결과는 출력된다.

 

위 코드에서 말하고 싶은 건 a, b, c 변수를 지칭하는 용어가 다르다는 것이다. 먼저 함수의 인수와 그 함수의 지역 변수속박 변수, 그 외 변수자유 변수라고 부른다. 위 코드에선 변수 c는 속박 변수, a, b는 자유 변수다.

그리고 속박 변수만을 포함하고 있는 함수닫힌 함수, 자유 변수를 가지고 있는 함수열린 함수라고 한다.

 

위에선 aa 함수가 닫힌 함수, bb 열린가 열린 함수이다. 그럼 각각의 변수들의 식별자를 결정하는 과정을 렉시컬 환경 컴포넌트를 통해서 살펴보도록 하겠다. 구체적으로는 렉시컬 환경 컴포넌트 내 외부 렉시컬 환경 참조와 환경 레코드를 사용한다. 렉시컬 환경 컴포넌트에 관한 기본적인 개념은 이 글을 참고해보자.


[속박 변수 c]

 

변수 c는 함수 bb안에서 선언된 지역 변수이므로 해당 함수의 환경 레코드에서 찾을 수 있다. 즉, 함수 bb의 환경 레코드로 식별자 결정을 한다.

함수 bb의 LexicalEnvironmentComponent: {
    //환경 레코드
    EnvironmentRecord : {
        // 선언적 환경 레코드
        DeclarativeEnvironmentRecord : {
            c : "C"
        },
    },
    // 외부 렉시컬 환경 참조
    OuterLexicalEnvironmentReference : 함수 aa의 LexicalEnvironment
},

 

[자유 변수 b]

 

변수 b는 함수 bb의 바깥에서 선언되었고 함수 aa의 지역 변수이다. 자바스크립트 엔진은 함수 bb에서 console에 출력할 b를 참조해야 하는 상황이지만 함수 bb의 환경 레코드에서는 찾을 수 없다. 그러므로 외부 렉시컬 환경 참조를 따라 한 단계 올라가 함수 aa가 속한 실행 문맥의 환경 레코드를 찾게 된다. 그리고 그 안에서 변수 b를 찾을 수 있다. 따라서 함수 aa의 환경 레코드로 식별자 결정을 한다.

함수 bb의 LexicalEnvironmentComponent: {
    //환경 레코드
    EnvironmentRecord : {
        // 선언적 환경 레코드
        DeclarativeEnvironmentRecord : {
            c : "C"
        },
    },
    // 외부 렉시컬 환경 참조
    OuterLexicalEnvironmentReference : 함수 aa의 LexicalEnvironment ->
},

함수 aa의 LexicalEnvironmentComponent: {
    //환경 레코드
    EnvironmentRecord : {
        // 선언적 환경 레코드
        DeclarativeEnvironmentRecord : {
            b : "B"
        },
    },
    // 외부 렉시컬 환경 참조
    OuterLexicalEnvironmentReference : 전역 LexicalEnvironment
}

예제 코드로 설명한다면 다음과 같다.

  1. 함수 aa가 호출될 때 렉시컬 환경 컴포넌트가 생성되고 함수 aa의 환경 레코드에 변수 b가 프로퍼티로 추가된다.
  2. 이후 함수 bb의 선언문이 평가되어 함수 bb 객체가 생성되고 환경 레코드가 생성된다. 이 때 함수 bb의 함수 객체가 함수 aa의 렉시컬 환경을 참조한다.
  3. 따라서 함수 bb에서 함수 aa의 지역 변수인 b를 사용할 수 있다.
  4. 실제로 함수 bb가 호출될 때는 변수 b의 위치를 외부 렉시컬 환경 참조를 따라 올라가 검색할 수 있다.

 

[자유 변수 a]

변수 a는 함수 bb 바깥에서 선언된 자유 변수이고 최상위 레벨인 전역 환경이다. 함수 bb에서 변수 a를 찾으려고 할 때 해당 함수 환경 레코드에는 당연하게도 변수 a에 대한 정보가 들어있지 않다. 그리고 마찬가지로 외부 렉시컬 환경을 따라 올라가 함수 aa의 환경 레코드도 검색하지만 역시 찾을 수 없다. 반복해서 함수 aa의 외부 렉시컬 환경인 전역 환경을 검색한다. 마침내 변수 a 정보를 전역 환경 레코드에서 찾을 수 있다. 즉, 전역 환경 레코드로 식별자 결정을 한다.

함수 aa의 LexicalEnvironmentComponent: {
    //환경 레코드
    EnvironmentRecord : {
        // 선언적 환경 레코드
        DeclarativeEnvironmentRecord : {
            b : "B"
        },
    },
    // 외부 렉시컬 환경 참조
    OuterLexicalEnvironmentReference : 전역 LexicalEnvironment ->
},

전역 LexicalEnvironmentComponent: {
    //환경 레코드
    EnvironmentRecord : {
        // 객체 환경 레코드
        ObjectEnvironmentRecord : {
            bindObject: 전역객체(ex. window)
        }
    },
    // 외부 렉시컬 환경 참조
    OuterLexicalEnvironmentReference : null
}

 

다른 변수와 차이점은 변수 a의 경우 전역 객체이기 때문에 선언적 환경 레코드가 아닌 객체 환경 레코드의 bindObject에 그 객체 전체가 바인딩된다.

 

이렇게 현재의 유효 범위(현재 실행 문맥) 안에 없는 식별자를 찾을 때 외부 렉시컬 환경 참조를 따라 가는 것을 ECMAScript 3에선 스코프 체인 ECMAScript 5부터는 따로 스펙에는 단어가 없지만 편의상 외부 렉시컬 환경 체인 or 유효 범위 체인이라고 부른다.

 

자바스크립트 개발을 할 때 우리가 가장 많이 접하는 오류 중 하나인 참조 오류(Referrence Error)는 이 유효 범위 체인에서 식별자 결정을 하지 못할 때 발생하니 이 개념이 얼마나 중요한지 알 수 있지 않을까???!!


클로저

 

드디어 클로저에 대한 설명이다. 서두에 클로저는 어떤 함수와 그 함수가 선언된 어휘적 환경(렉시컬 환경)의 조합이라고 말했던 것을 기억하는가?? 즉, 클로저는 위에서 입 아프게 설명했던 함수 객체와 그 함수의 렉시컬 환경 컴포넌트라고 말할 수 있다.

 

좀 더 구체적으로는 자기 자신이 정의된 환경(렉시컬 환경)에서 함수 안에 있는 자유 변수식별자 결정을 실행하는 기능을 가진 함수, 그리고 그 기능을 구현한 자료 구조의 모음이다.

 

 앞서 신나게 설명했던 예제는 사실 자바스크립트에서 클로저를 정의한 함수이다.

 

var a = "A";

function aa() {
    var b = "B"

    function bb() {
        var c = "C"
        console.log(a + b + c);
    }

    bb();
}

aa(); // "ABC"

중첩 함수 bb가 정의된 환경은 bb 선언문을 둘러싸고 있는 바깥 영역이다. 이 렉시컬 환경에서 함수 bb에서 참조하는 자유변수인 a와 b의 식별자 결정을 한다.

 

  1. 함수 aa를 호출한다. 함수 aa의 렉시컬 환경 컴포넌트가 생성된다.
  2. 함수 bb 선언문을 평가하는 시점에 함수 bb에 대한 함수 객체를 생성하고 이 함수 객체의 렉시컬 환경 컴포넌트로 함수 bb의 코드, 함수 aa의 렉시컬 컴포넌트 참조, 전역 환경 컴포넌트 참조가 저장된다.
  3. 함수 bb를 호출할 때 위 함수 객체에 저장된 렉시컬 환경 컴포넌트를 생성한다. 그리고 그 렉시컬 환경 컴포넌트에 저장된 외부 렉시컬 환경 참조를 따라 자유 변수 a, b 값을 참조한다.

즉, 함수 bb의 함수 객체 객체가 참조하는 렉시컬 환경 컴포넌트 자유 변수 a, b의 식별자 결정을 위한 자료 구조라고 말할 수 있다. 이것이 바로 클로저다. 이 클로저는 함수 aa가 호출되어 함수 bb 선언문을 평가하는 시점에 생성된다.

 

그리고 함수 bb의 함수 객체가 있는 동안에는 클로저 안의 모든 렉시컬 환경 컴포넌트를 함수 bb 객체가 참조하므로 클로저는 가비지 컬렉션 대상이 되지 않는다. 즉, 메모리에서 지워지지 않는다.

 

또한, 함수 bb는 원래 자유변수 a, b가 포함된 열린 함수였지만 외부 렉시컬 참조를 통해 자유변수 a와 b를 들여왔다는 건 자신의 지역 변수로 등록한 셈이므로 실질적으로 닫힌 함수가 되었다. 이렇게 원래 열려있던 것을 닫히게 하는 의미에서 클로저의 어원이 탄생한 것이다.

이 글을 공유합시다

facebook twitter kakaoTalk kakaostory naver band
loading