6월 21, 2016

[번역] Understanding JavaScript Closures

아래는 Understanding JavaScript Closures 라는 아티클에 대한 번역입니다.
원문 링크는 여기를 참조해주세요.




Understanding JavaScript Closures


자바스크립트에서, 클로져는 컨텍스트 내의 변수들이 참조로 바인딩되는 함수입니다.


function getMeAClosure() {
    var canYouSeeMe = "here I am";
    return (function theClosure() {
        return {canYouSeeIt: canYouSeeMe ? "yes!": "no"};
    });
}
var closure = getMeAClosure();
closure().canYouSeeIt; //"yes!"

모든 javscript 함수들은 생성시 클로져를 형성합니다.
이 글에서는 그 이유를 설명하고 클로져가 형성되는 과정을 살펴보겠습니다.
그 다음 몇가지 일반적인 클로져에 대한 오해를 다루고, 실용적인 예제로 마무리 짓겠습니다. 그에 앞서 짧은 요약을 하자면:
클로져는 어휘적 범위(Lexical Scope)와 변수 환경(VariableEnvironment)을 갖습니다.

어휘적 범위(Lexical Scope)

본래 어휘적인(lexical)이라는 말은 단어나 일반적인 언어에서 쓰이는 용어입니다.
그래서 함수의 '어휘적 범위'(Lexical Scope)란 함수가 기술된 물리적인 코드상의 위치에 의해 정적으로 정의되는 범위를 말합니다.

아래 예제를 참고:

var x = "global";
function outer() {
    var y = "outer";   
    function inner() {
        var x = "inner";   
    }
}

함수 inner 는 함수 outer 가 물리적으로 감싸고 있고 그 밖에는 global context가 감싸고 있습니다.
우리는 아래와 같은 lexical 계층 구조를 형성했습니다.

global
    outer
         inner

어떤 함수든지 간에 outer lexical scope 는 그것의 lexical 계층 구조의 조상에 의해 정해집니다.
따라서, inner 함수의 outer lexical scope 는 전역 객체와 outer 함수를 포함하게 됩니다.

변수 환경(VariableEnvironment)

전역 객체는 실행 컨텍스트와 관련이 있습니다.
또한 함수의 모든 호출은 새로운 실행 컨텍스트를 생성하고 그 안으로 진입합니다.
실행 컨텍스트는 정적 lexical scope 에 동적으로 대응됩니다.
각 실행 컨텍스트는 컨텍스트 안에서 선언된 변수들의 저장소인 변수 환경을 정의합니다.

[EcmaScript3 에서, VariableEnvironment 는 활성객체(ActivationObject)로 알려졌었다. - 저자가 이전 글에서 사용하던 용어]

의사코드로 변수 환경을 표현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//variableEnvironment: {x: undefined, etc.};
var x = "global";
//variableEnvironment: {x: "global", etc.};
function outer() {
    //variableEnvironment: {y: undefined};
    var y = "outer";
    //variableEnvironment: {y: "outer"};
    function inner() {
        //variableEnvironment: {x: undefined};
        var x = "inner";   
        //variableEnvironment: {x: "inner"};
    }
}


하지만, 이는 전체적인 그림의 일부만을 보여줄 뿐입니다.
각각의 VariableEnvironment 는 해당 lexical scope의 VariableEnvironment 를 물려받습니다.


[[scope]] 속성

실행 컨텍스트가 코드 안에서 함수의 정의를 만나게 되면,
현재의 변수 환경을 가리키는 [[scope]] 라는 이름의 프로퍼티를 가진 새로운 함수 객체가 생성됩니다.
모든 함수는 [[scope]] 속성을 가지고, 함수가 호출되면 [[scope]] 속성의 값은 변수 환경의
외부 어휘 환경 레퍼런스 프러퍼티(outer lexical environment reference 또는 outerLex)에 할당됩니다.
이러한 방식으로, 각 변수환경은 어휘적 부모의 변수환경으로부터 상속받게 됩니다.
이 스코프 체이닝은 전역 객체로부터 시작하여 어휘적 계층의 깊이만큼 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//VariableEnvironment: {x: undefined, etc.};
var x = "global";
//VariableEnvironment: {x: "global", etc.};
function outer() {
    //VariableEnvironment: {y: undefined, outerLex: {x: "global", etc.}};
    var y = "outer";   
    //VariableEnvironment: {y: "outer", outerLex: {x: "global", etc.}};
    function inner() {
        //VariableEnvironment: {x: undefined, outerLex: {y: "outer", outerLex: {x:"global", etc.}};
        var x = "inner";   
        //VariableEnvironment: {x: "inner", outerLex: {y: "outer", outerLex: {x:"global", etc.}};
    }
}

[[scope]] 는 중첩된 variableEnvironments 간의 다리 역할을 수행하며
내부 VariableEnvironments 에 저장된 외부 변수를 통해 이 프로세스를 가능하게 합니다.
[[scope]] 는 또한 클로져를 가능하게 하는데, 만약 [[scope]]가 없다면, 외부 함수의 변수들은 참조되지 않을 것(dereferenced)이고, 외부 함수가 리턴되면 가비지 콜렉터에 의해 제거될 것이기 때문입니다.

결국은, 클로져는 단지 어휘적 스코핑(Lexical scoping)의 피할 수 없는 부작용인 것입니다.

클로져에 대한 오해

이제 클로져가 어떻게 돌아가는지 대충 이해했으니, 클로져와 관련된 요상한 루머를 다뤄보겠습니다.

1. 클로져는 내부 함수가 리턴된 후에만 생성된다.

함수가 생성 될 때, 외부 렉시컬 스코프의 변수를 가리키는 scope 프로퍼티가 할당 되고,
이것이 가비지 콜렉트되는 것을 막습니다.
그러므로 클로져는 함수 생성과 동시에 형성되는 것입니다.
따라서 클로져가 되기 전에 반드시 함수가 리턴되어야 한다는 법은 없습니다.
아래는 함수의 리턴없이 동작하는 클로져입니다.

var callLater = function(fn, args, context) {
    setTimeout(function(){fn.apply(context, args)}, 2000);
}
callLater(alert,['hello']);

2. 외부 변수의 값은 클로져에 복사되거나 내장된다.

앞에서 봤듯이, 클로져는 '값'이 아닌 '변수'를 참조합니다.

//Bad Example
//Create an array of functions that add 1,2 and 3 respectively
var createAdders = function() {
    var fns = [];
    for (var i=1; i<4; i++) {
        fns[i] = (function(n) {
            return i+n;
        });
    }
    return fns;
}
var adders = createAdders();
adders[1](7); //11 ??
adders[2](7); //11 ??
adders[3](7); //11 ??

세 개의 adder 함수는 모두 같은 i 변수를 가리키고 있습니다. 그리고 어떤 함수가 호출되든, i 의 값은 4 입니다.

해답 중 하나는 각 인자를 즉시 실행 함수로 전달하는 것입니다.
모든 함수 호출은 각자의 유일한 실행 컨텍스트를 발생시키기 때문에, 연속적인 함수 호출로 인자 변수의 유일성을 보장할 수 있습니다.



//Good Example
//Create an array of functions that add 1,2 and 3 respectively
var createAdders = function() {
    var fns = [];
    for (var i=1; i<4; i++) {
        (function(i) {
            fns[i] = (function(n) {
                return i+n;
            });
        })(i)   
    }
    return fns;
}
var adders = createAdders();
adders[1](7); //8 (-:
adders[2](7); //9 (-:
adders[3](7); //10 (-:

3. 클로져는 오로지 내부 함수에 대해서만 적용된다.

 [[scope]] 프로퍼티가 오로지 전역적으로 언제든지 접근 가능한 전역 스코프를 참조하므로, 외부 함수에 의해 생성된 클로져는 특별히 흥미로울 것이 없다고 생각할 수도 있습니다.(여기서 외부 함수란 window 객체 바로 하위의 전역 함수를 말하는 것입니다.)
그럼에도 불구하고 클로져의 형성 과정은 모든 함수가 동일하고, 모든 함수는 클로져를 형성한다는 점을 아는 것이 중요합니다.

4. 클로져는 익명 함수에만 적용된다.

이전 글에서 너무 많이 거론했으므로 생략합니다.

5. 클로져는 메모리릭의 주범이다.

클로져 그 자체는 순환 참조를 만들지 않습니다. 우리의 원래 예제에서,
inner 함수는 scope 속성을 통해 외부 변수를 참조하는 것이지,
참조 변수나 함수 outer가 inner 함수 혹은 지역 변수를 참조하지 않습니다.

IE의 구버전들은 메모리 릭으로 악명이 높고 주로 클로져를 비난의 대상으로 삼았습니다.
일반적인 원인은 함수에 의해 참조되는 DOM 요소이며, 같은 돔 요소가 같은 렉시컬 스코프 안의 다른 객체를 함수로 참조하기 떄문입니다.
IE 6~8 버전들은 대부분 이러한 순환 참조 문제가 있습니다.

마무리

프로그래밍 용어에서, 클로져는 우아함과 세련됨의 높이를 표현합니다.
클로져는 코드를 간결하고, 읽기 쉽고, 아름답게 만들고 기능적인 재사용을 촉진합니다.
클로져의 동작과정과 사용하는 이유를 알면 클로져를 사용하는데 있어 불확실성을 제거할 수 있습니다.
나는 이 문서가 그런 측면에서 도움되기를 바랍니다. 댓글이나 질문, 의견 등 모두 환영합니다.

댓글 없음:

댓글 쓰기

댓글