[Javascript] 클로저(Closure)와 렉시컬 환경(Lexical environment) 어렵지 않습니다.

클로저는 Javascript 고유의 개념이 아니라 함수를 일급객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 개념입니다. Javascript가 따르고 있는 ECMAscript 명세에는 클로저의 정의를 다루지 않기 때문에 MDN에서 클로저 정의를 살펴보겠습니다.

※ 1급 객체 : 변수에 담을 수 있고 인자로 전달할 수 있고 반환값으로 전달할 수 있으면 1급객체로 취급합니다.

MDN에서는 클로저를 다음과 같이 정의하고 있습니다.

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.

위 정의는 다음과 같이 해석할 수 있습니다.

- 클로저는 '함수'와 '렉시컬 환경'의 조합이다.
- 내부함수가 외부함수의 스코프에 접근할 수 있게한다.

1. 렉시컬 환경(Lexical environment)

MDN정의에 나오는 렉시컬 환경은 무엇일까요? 구성요소를 살펴보겠습니다.

lexical environment = {
    environment record: -,
    outer environment reference: -,
}

렉시컬 환경은 envirionment record와 outer environment reference로 구성되어 있습니다.

  • environment record : 지역(변수, 함수) 식별자 덩어리, 유효범위 내의 식별자가 어떤값과 바인딩 되어 있는지 변수객체를 관리해주는 역할
  • outer environment reference : 자바스크립트는 함수 단위 유효범위를 갖는데, 이 함수들은 중첩될 수 있습니다. 즉 유효범위도 중첩될 수 있다는 뜻입니다. 유효범위가 중첩되었을 때 상위 유효범위로 올라갈 수 있어야 하는데 상위 유효범위에 대한 참조를 할 수 있습니다.

아래 코드를 보며 렉시컬 환경을 더 쉽게 설명하겠습니다.

function sum(x,y) {
    var result = x + y;

    function printResult() {
        console.log("foo: "+ result);
    }

    return printResult;
}

var print = sum(10, 20);
print();

함수가 생성된 시점에 렉시컬 환경이 조성되며 스코프안에 있는 모든 지역 변수들을 environment record 와 outer environment reference가 다음과 같이 관리해주고 있습니다.

LexicalEnvironment = {
    EnvironmentRecord : {
        x: 10,
        y: 20,
        result : undefined,
        printResult : function
    },
    OuterEnvironmentReference :
        globalEC.lexicalEnvironment
}

즉 렉시컬 환경은 코드에서 변수나 함수 등의 식별자를 정의하고 관리해주는 개념으로 생각하면 됩니다. 클로저는 이러한 렉시컬 환경을 기억하고 있다가 자신이 선언됐을 때의 환경 밖에서 호출되어도 그 환경에 접근할 수 있습니다.

※ 참고로 렉시컬 환경은 개념(컨셉?)이라 Javascript 코드에서 실제로 접근할 순 없습니다.

2. 클로저(Closure)란

클로저의 간단한 예제를 살펴보겠습니다.

function outter() {
    var text = "Answer";
    return function () {
        console.log(text);
    };
}

var closure = outter();
closure(); // 'Answer'

outter()는 내부함수를 반환하고 있고, 반환된 내부함수는 외부함수 outter()에서 선언된 text라는 지역 변수를 참조하고 있습니다. 함수는 반환되는 순간 생을 마감합니다. 따라서 closure()을 실행했을 때에 outter()은 이미 죽은 상태입니다. 하지만 closure()를 실행해보면 내부함수가 outter()text를 가지고 와서 실행해주는 것을 볼 수 있습니다. 이는 외부함수 outter()가 소멸되었어도 내부함수의 외부함수 접근이 가능하다는 것 을 보여줍니다. 이는 클로저의 중요한 특징입니다.

※ 내부함수 : (외부)함수 안에서 선언된 함수를 뜻합니다.

다른 예제를 살펴보겠습니다.

function game(title) {
    var gameTitle = title;
    return function() {
        console.log(gameTitle);
    };
}

var overWatch = game('Over Watch');
var lol = game('League Of Legend');

overWatch(); // Over Watch
lol(); // League Of Legend

game의 지역변수인 gameTitle이 동적으로 바뀌는 것 처럼 보이지만 그렇지 않습니다. overWatchlol은 선언 당시의 렉시컬 환경을 각각 저장합니다. 따라서 overWatchgameTitlelolgameTitle은 각각 따로 생성된 녀석들 입니다. 클로저는 자신이 생성된 시점의 환경을 기억하였다가 호출되었을 때 기억해놓은 환경을 가지고와서 수행해줍니다. 이 또한 클로저의 중요한 특징입니다.

이렇게 생성될 때 마다 렉시컬 환경을 기억하면 메모리 차원에서 손해를 볼 수 있습니다. 그럼에도 클로저를 사용하는 이유를 알아보겠습니다.

3. 클로저의 활용

OOP의 대표적인 언어인 Java는 변수/함수의 외부 접근을 방지 하기위해 private으로 변수/함수를 선언합니다. 클로저를 사용하면 이와 유사한 기능을 사용할 수 있습니다.

버튼을 클릭할 때 마다 count가 올라가는 기능을 구현한다고 합시다.

<button onclick="countBtnClick()">Click</button>

전역 변수를 선언하여 count를 올려줄 수 있지만 누구나 접근, 변경할 수 있어 의도치 않게 값이 변경될 수 있습니다.

var count = 0;

function countBtnClick() {
    count += 1;
}

지역 변수를 사용하면 함수를 호출할 떄 마다 count값을 0으로 초기화하기 때문에 이전 상태를 기억하지 못합니다.

function countBtnClick() {
    var count = 0;
    count += 1;
}

클로저를 사용해보겠습니다. 즉시실행함수은 호출후에 바로 소멸하지만 즉시실행함수 내부의 함수가 클로저가 되면서 렉시컬 환경(count)을 기억하게 됩니다. 또 즉시실행함수의 지역변수인 count에 접근할 수 있습니다. count는 외부에서 직접 접근이 불가능하게 되고 private 하게 사용할 수 있습니다.

const countBtnClick = (function() {
    var count = 0;

    return function() {
        count += 1;
    }
})()

※ 너무나 흔한 예제인 반복문 클로저는 블록레벨스코프를 지원하는 let을 사용하면 해결할 수 있기 때문에 스킵하겠습니다.

4. 주의

내가 사용하는게 클로저가 맞는가?

function foo() {
    var color = 'blue';
    function bar() {
        console.log(color);
    }
    bar();
}
foo();

bar함수는 클로저일까요? barfoo안에서 정의되고 실행되었지만, foo밖으로 나오지 않았기 때문에 클로저라고 부를 수 없습니다.

메모리 release

클로저는 각각의 환경을 갖고있기 때문에 메모리를 추가로 소모합니다. 따라서 참조를 제거하여 메모리를 release 시키는 것이 좋습니다.

function game(title) {
    var gameTitle = title;
    return function() {
        console.log(gameTitle);
    };
}

var overWatch = game('Over Watch');
var lol = game('League Of Legend');

overWatch(); // Over Watch
lol(); // League Of Legend

overWatch = null;
lol = null;

Reference


Park Answer

Find answer in the record