JavaScript

실행 컨텍스트와 클로저 개념 정리

준준영 2020. 2. 25. 00:38

실행 컨텍스트와 클로저

이번 장에서는 자바스크립트가 실행될 때 생성되는 하나의 실행 단위인 실행 컨텍스트, 변수의 유효 범위, 그리고 클로저에 대해서 포스팅하겠습니다. 이는 자바스크립트에서 매우 중요한 개념입니다. 이들을 정확히 이해하지 못한다면 다른 사람이 작성한 자바스크립트 코드를 이해하기 어렵고, 활용하기 불편할뿐만 아니라 디버깅도 굉장히 어렵습니다.

여기서 살펴볼 내용은 다음과 같습니다.

  1. 실행 컨텍스트의 개념
  2. 활성 객체와 변수 객체
  3. 스코프 체인
  4. 클로저

실행 컨텍스트의 개념

기존언어를 경험한 독자라면, 콜 스택을 들어보았을 것입니다. 이는 함수를 호출할 때 해당 함수의 호출 정보(C 언어를 예로 들면, 함수의 호출 정보 등으로 함수 내 지역 변수 혹은 인자값 등)가 차곡차곡 쌓여있는 스택을 의미합니다. 가령 C언어의 경우는 함수가 호출될 때마다 해당 함수의 호출 정보가 기존 함수의 호출 정보 위에 스택 형태로 하나씩 쌓입니다. 따라서 C 개발자는 이러한 콜 스택의 호출 정보 등으로 코드의 실행 과정을 추적하여 디버깅과 같은 작업을 수행합니다. 이는 보통의 고급 프로그래밍 언어에서 흔하게 사용하는 방식이고, 자바스크립트 역시 이 범주를 크게 벗어나지 않습니다.

실행 컨텍스트는 실행 가능한 코드가 실행되기 위해 필요한 환경이라고 말할 수 있겠습니다.

실행 컨텍스트는 앞에서 설명한 콜 스택에 들어가는 실행 정보 하나와 비슷합니다. ECMAScript에서는 실행 컨텍스트를 실행 가능한 코드를 형상화하고 구분하는 추상적인 개념으로 기술합니다. 이를 앞서 설명한 콜스택과 연관하여 정의하면, 실행 가능한 자바스크립트 코드 블록이 실행되는 환경이라고 할 수 있고, 이 컨텍스트 안에 실행에 필요한 여러가지 정보를 담고 있습니다. 여기서 말하는 실행 가능한 코드 블록은 대부분의 경우 함수가 됩니다. ECMAScript에는 실행 컨텍스트가 형성되는 경우를 크게 세 가지로 규정하고 있는데 전역 콜, eval() 함수로 실행되는 코드, 함수 안의 코드를 실행한 경우입니다. 대부분 프로그래머는 함수로 실행 컨텍스트를 만듭니다. 그리고 이 코드 블록 안에 변수 및 객체, 실행 가능한 코드가 들어있습니다. 이 코드가 실행되면 실행 컨텍스트가 생성되고, 실행 컨텍스트는 스택 안에 차곡차곡 쌓이고, 제일 위에 위치하는 실행 컨텍스트가 현재 실행되고 있는 컨텍스트입니다. ECMAScript에서는 실행 컨텍스트의 생성을 다음처럼 설명합니다.
현재 실행되는 컨텍스트에서 이 컨텍스트와 관련 없는 실행 코드가 실행되면, 새로운 컨텍스트가 생성되어 스택에 들어가고 그 제어권이 그 컨텍스트로 이동합니다.

역시.. 무슨 말인지 이해가 안갔습니다. 예제로 알아보겠습니다.

console.log("This is global context");

function ExContext1() {
    console.log("This is ExContext1");
};

function ExContext2() {
    ExContext1();
    console.log("This is ExContext2");
};

ExContext2();

Untitled Diagram (1)

위의 그림과 같이 전역 실행 컨텍스트가 가장 먼저 실행됩니다. 여기서 전역 실행 컨텍스트는 사실 별 의미가 없습니다. 가장 먼저 실행되는 실행 컨텍스트일 뿐입니다. 다른 실행 컨텍스트와 다른 점은 이후에 설명하겠습니다. 이 과정에서 새로운 함수 호출이 발생하면 새로운 컨텍스트가 만들어지고 실행되며, 종료되면 반환됩니다. 이와 같은 과정이 반복된 후, 전역 실행 컨텍스트의 실행이 완료되면 모든 실행이 끝납니다.

실행 컨텍스트 생성 과정

이 장에서는 실행 컨텍스트 생성 과정을 설명하고, 활성화 객체와 변수 객체, 스코프 체인에 대한 중요 개념을 살펴보겠습니다.

  • 활성화 객체와 변수 객체
  • 스코프 체인

다음 예제를 실행하면 실행 컨텍스트가 어떻게 만들어질지 생각해봅시다.

function execute(param1, param2) {
    var a = 1, b = 2;

    function func() {
        return a + b; 
    }
    return param1 + param2 + func();
}

execute(3, 4);

자바스크립트 함수를 실행하여 실행 컨텍스트가 생성되면 자바스크립트 엔진은 다음과 같은 일을 정해진 순서대로 실행합니다. 예제에서 execute() 함수를 실행한 경우를 예로 설명하겠습니다.

활성화 객체 생성

실행 컨텍스트가 생성되면 자바스크립트 엔진은 해당 컨텍스트에서 실행에 필요한 여러가지 정보를 담은 객체를 생성하는데, 이를 활성 객체라고 합니다. 이 객체에 앞으로 사용하게 될 매개변수나 사용자가 정의한 변수 및 객체를 저장하고, 새로 만들어진 컨텍스트로 접근 가능하게 되어 있습니다. 이는 엔진 내부에서 접근할 수 있다는 것이지 사용자가 접근할 수 있다는 것은 아닙니다.

argument 객체 생성

다음 단계에서는 arguments 객체를 생성합니다. 이 객체에 대해서는 이전에 포스팅 했던 함수 프로토타입 체이닝 부분에서 설명하였습니다. 앞서 만들어진 활성 객체는 param1과 param2가 들어왔을 경우의 활성 객체의 상태를 표현합니다.

[그림 - 활성 객체]

Untitled Diagram

스코프 정보 생성

현재 컨텍스트의 유효 범위를 나타내는 스코프 정보를 생성합니다. 이 스코프 정보는 현재 실행 중인 실행 컨텍스트 안에서 연결 리스트와 유사한 형식으로 만들어집니다. 현재 컨텍스트에 특정 변수에 접근해야 할 경우, 이 리스트를 활용합니다. 이 리스트로 현재 컨텍스트의 변수뿐 아니라, 상위 실행 컨텍스트의 변수도 접근이 가능합니다. 이 리스트에서 찾지 못한 변수는 결국 정의되지 않는 변수에 접근하는 것으로 판단하여 에러를 검출합니다. 이 리스트를 스코프 체인이라고 하는데, [[scope]] 프로퍼티로 참조합니다. 이것이 왜 리스트의 형태를 띄고 있고, 이 체인이 어떻게 형성되며, 리스트의 구성 요소가 무엇인지는 이후에 좀 더 자세히 살펴보겠습니다. 여기서는 현재 생성된 활성 객체가 스코프 체인의 제일 앞에 추가되며, execute() 함수의 인자나 지역 변수 등에 접근할 수 있다는 것만 알고 넘어갑시다.

Untitled Diagram (1)

변수 생성

현재 실행 컨텍스트 내부에서 사용되는 지역 변수의 생성이 이루어집니다. ECMAScript에서는 생성되는 변수를 저장하는 변수 객체를 언급하는데, 실제적으로 앞서 생성된 활성 객체가 변수 객체로 사용됩니다. 우리가 자바스크립트 관련 문서를 읽다 보면 어떤 곳에서는 활성 객체, 어떤 곳에서는 변수 객체라고 사용되어 혼란스러운 경우가 있는데 두 객체가 같은 객체이므로, 혼동하는 일이 없기를 바랍니다.

변수 객체 안에서 호출된 함수 인자는 각각의 프로퍼티가 만들어지고 그 값이 할당됩니다. 만약 값이 넘겨지지 않았다면 undefined가 할당됩니다. 위의 예제에서 execute() 함수 안에 정의된 변수 a, b와 함수 func가 생성됩니다. 여기서 주의할 점은 이 과정에서 변수나 내부 함수를 단지 메모리에 생성하고, 초기화는 각 변수나 함수에 해당하는 표현식이 실행되기 전까지는 이루어지지 않는다는 점입니다. 따라서 변수 a와 b에는 먼저 undefined가 할당됩니다. 표현식의 실행은 변수 객체 생성이 다 이루어진 후 시작됩니다.

this 바인딩

마지막 단계에서 this 키워드를 사용하는 값이 할당됩니다. 이 값에 어떤 객체가 들어가는지는 이전 포스팅에서 살펴보았으니, 넘어가겠습니다. 여기서 this가 참조하는 객체가 없으면 전역 객체를 참조합니다.

Untitled Diagram

코드 실행

이렇게 하나의 실행 컨텍스트가 생성되고, 변수 객체가 만들어진 후에, 코드에 있는 여러 가지 표현식 실행이 이루어집니다. 이렇게 실행되면서 변수의 초기화 및 연산, 또 다른 함수 실행등이 이루어집니다. 위 그림에서 undefined가 할당된 변수 a와 b에도 이 과정에서 1, 2의 값이 할당됩니다.

참고로 전역 실행 컨텍스트는 일반적인 실행 컨텍스트와는 약간 다른데, arguments 객체가 없으며, 전역 객체 하나만을 포함하는 스코프 체인이 있습니다. ECMAScript에서 언급된 바에 의하면 실행 컨텍스트가 형성되는 세 가지 중 하나로서 전역 코드가 있는데, 이 전역코드가 실행될 때 생성되는 컨텍스트가 전역 실행 컨텍스트입니다. 전역 실행 컨텍스트는 변수를 초기화하고 이것의 내부 함수는 일반적인 탑 레벨의 함수로 선언됩니다. 그리고 전역 실행 컨텍스트의 변수 객체가 전역 객체로 사용됩니다. 즉, 전역 실행 컨텍스트에서는 변수 객체가 곧 전역 객체 입니다. 따라서 전역적으로 선언된 함수와 변수가 전역 객체의 프로퍼티가 됩니다. 전역 실행 컨텍스트 역시, this를 전역 객체의 참조로 사용합니다.

참조 브라우저에서는 최상위 코드가 곧 전역 코드이지만, Node.js에서는 다릅니다.

var a = 10;
b = 15;
console.log(window.a); //출력값 10
console.log(window.b); //출력값 15

브라우저에서 위 코드는 잘 실행됩니다. var a로 정의한 변수가 전역 객체인 window의 한 프로퍼티로 들어갔습니다. 하지만 Node.js에서는 다릅니다.

var a = 10;
b = 15;

console.log(window.a); //출력값 undefined
console.log(window.b); //출력값 15

Node.js에서는 최상위 코드가 브라우저와는 달리 전역 코드가 아닙니다. 따라서 var a로 정의된 변수가 전역 객체에 들어가지 않습니다. Node.js에서는 일반적으로 자바스크립트 파일, 이를테면 filename.js가 하나의 모듈로 동작하고 이 파일의 최상위에 변수를 선언해도 그 모듈의 지역변수가 됩니다. 하지만 var를 사용하지 않을 경우 전역 객체인 global에 들어가고, 이는 전역 객체를 오염시키는 원인이 되므로 주의해야 합니다.

스코프 체인

자바스크립 코드를 이해하려면 스코프 체인의 이해는 필수적입니다. 이를 알아야, 변수에 대한 인식 메커니즘을 알 수 있고, 현재 사용되는 변수가 어디에 선언된 변수인지 정확히 알 수 있기 때문입니다. 앞서 이전에 설명했던 프로토타입 체인과 거의 비슷한 메커니즘이므로 이해하는데 어렵지는 않을 것입니다.

자바스크립트도 다른 언어와 마찬가지로 스코프, 즉 유효 범위가 있습니다. 이 유효 범위 안에서 변수와 함수가 존재합니다. C 코드를 예를 들면, {,}로 묶여 있는 범위 안에 선언된 변수는 블록이 끝나는 순간 사라지므로 밖에서는 접근할 수 없습니다. 특히, 함수의 {,}뿐만 아니라 if, for문의 {,}로 한 블록으로 묶여, 그 안에서 선언된 변수가 밖에서는 접근이 불가능합니다. 하지만 자바스크립트에서는 함수 내의 {,} 블록은, 이를테면 for() {}, if{}와 같은 구문은 유효 범위가 없습니다. 오직 함수만이 유효 범위의 한 단위가 됩니다. 이 유효 범위를 나타내는 스코프가 [[scope]] 프로퍼티로 각 함수 객체 내에서 연결 리스트 형식으로 관리되는데, 이를 스코프 체인이라고 합니다.
스코프 체인은 각 실행 컨텍스트의 변수 객체가 구성 요소인 리스트와 같습니다.
스코프 체인은 의미 그대로 각각의 스코프가 어떻게 연결되었는지 보여주는 것이 스코프
체인입니다.

각각의 함수는 [[scope]] 프로퍼티로 자신이 생성된 실행 컨텍스트의 스코프 체인을 참조합니다. 함수가 실행되는 순간 실행 컨텍스트가 만들어지고, 이 실행 컨텍스트는 실행된 함수의 [[scope]] 프로퍼티를 기반으로 새로운 스코프 체인을 만듭니다. 예제로 살펴보겠습니다.

var var1 = 1;
var var2 = 2;
console.log(var1); // 출력 값 1
console.log(var2); // 출력 값 2

위 예제는 전역 코드입니다. 함수가 선언되지 않아 함수 호출이 없고, 실행 가능한 코드들만 나열되어 있습니다. 이 자바스크립트 코드를 실행하면, 먼저 전역 실행 컨텍스트가 생성되고, 변수 객체가 만들어 집니다. 이 변수 객체의 스코프 체인은 어떻게 될까요? 현재 전역 실행 컨텍스트 단 하나만 실행되고 있어 참조할 상위 실행 컨텍스트가 없습니다. 자신이 최상위에 위치하는 변수 객체인 것입니다. 따라서, 이 변수 객체의 스코프 체인은 자기 자신만을 가집니다. 다시 말해서, 변수 객체의 [[scope]]는 변수 객체 자신을 가르킵니다. 그 후, var1, va2 변수들이 생성되고 변수 객체에 의해 참조됩니다. 실행 컨텍스트 생성과정에서 언급한 대로 이 변수 객체가 곧 전역 객체가 됩니다.

[그림 - 전역 실행 컨텍스트]

Untitled Diagram

함수를 호출한 경우 생성되는 실행 컨텍스트의 스코프 체인

아래 예제에서 함수를 하나 생성해보겠습니다.

var var1 = 1;
var var2 = 2;

function func() {
    var var1 = 10;
    var var2 = 20;
    console.log(var1); // 출력 값 10
    console.log(var2); // 출력 값 20
}
func();

console.log(var1); // 출력 값 1
console.log(var2); // 출력 값 2

이 예제를 실행하면 전역 실행 컨텍스트가 생성되고, func() 함수 객체가 만들어 집니다. 이 함수 객체가 생성될 때, 그 함수 객체의 [[scope]]는 현재 실행되는 컨텍스트의 변수 객체에 있는 [[scope]]를 그대로 가집니다. 따라서, func 함수 객체의 [[scope]]는 전역 변수 객체가 됩니다.

그런 다음에 func() 험수를 실행하였으므로 새로운 컨텍스트가 만들어집니다. 이 컨텍스트를 편의상 func 컨텍스트라고 하겠습니다. func 컨텍스트의 스코프 체인은 실행된 함수의 [[scope]] 프로퍼티를 그대로 복사한 후, 현재 생성된 변수 객체를 복사한 스코프 체인의 맨 앞에 추가합니다. func() 함수 객체의 [[scope]] 프로퍼티가 전역 객체 하나만을 가지고 있었으므로, func 실행 컨텍스트의 스코프 체인은 다음 그림과 같이 [func 변수 객체 - 전역 객체]가 됩니다.

Untitled Diagram (1)

위 그림을 바탕으로 스코프 체인을 간단히 정리하겠습니다.

  • 각 함수 객체는 [[scope]] 프로퍼티로 현재 컨텍스트의 스코프 체인을 참조합니다.
  • 한 함수가 실행되면 새로운 실행 컨텍스트가 만들어지는데, 이 새로운 실행 컨텍스트는 자신이 사용할 스코프 체인을 다음과 같은 방법으로 만듭니다. 현재 실행되는 함수 객체의 [[scope]] 프로퍼티를 복사하고, 새롭게 생성된 변수 객체를 해당 체인의 제일 맨 앞에 추가합니다.
  • 요약하면 스코프 체인은 다음과 같이 표현할 수 있습니다.

스코프 체인 = 현재 실행 컨텍스트의 변수 객체 + 상위 컨텍스트의 스코프 체인

이제, 앞에서 정리한 내용으로 다음 두 예제의 결과를 예상해보겠습니다.

var value = "value1";

function printFunc() {
    var value = "value2";

    function printValue() {
        return value;
    }
    console.log(printValue());
}

printFunc();

앞에 설명을 제대로 이해했다면, 코드가 출력하는 결과값을 쉽게 예측할 수 있습니다. 스코프 체인을 그려보면 다음과 같습니다.

Untitled Diagram

결과 값은 printValue() 함수가 실행될 때 Value 변수를 printFunc 변수 객체에서 찾으므로, 결과값은 value2가 됩니다.

그렇다면 다음 예제도 확인해보겠습니다.

var value = "value1";

function printValue() {
    return value;
}

function printFunc(func) {
    var value = "value2";
    console.log(func());
}

printFunc(printValue);

이 예제는 각 함수 객체가 처음 생성될 당시 실행 컨텍스트가 무엇인지를 생각해야 합니다. 각 함수객체가 처음 생성될 때 [[scope]]는 전역 객체의 [[scope]]를 참조합니다. 따라서 각 함수가 실행될 때 생성되는 실행 컨텍스트의 스코프 체인은 전역 객체와 그 앞에 새롭게 만들어진 변수 객체가 추가됩니다. 그래서 printValue()가 실행될 때의 스코프 체인은 위 예제와는 다르게 만들어집니다. 이것도 그림으로 설명하겠습니다.

Untitled Diagram (1)

printValue 함수는 전역 실행 컨텍스트의 전역 객체에서 함수 객체가 생성되기 때문에 [[scope]] 프로퍼티는 전역 객체를 참조하게 됩니다. 따라서 value 변수를 전역 객체에서 찾으므로 결과 값은 value1이 됩니다.

함수가 정의될 때의 환경을 기준으로 스코프가 설정된다는 것을 꼭 기억해야 합니다.

참고로 크롬 개발자 도구에서 console.dir(함수())를 실행하면 [[scope]] 프로퍼티가 존재합니다. 이것이 바로 스코프 체인입니다. 자기 자신의 스코프를 제외하고, 자신과 가장 가까운 변수 객체의 스코프 순으로 접근하고 있는 것을 확인할 수 있습니다.

image

지금까지 실행 컨텍스트가 만들어지면서 스코프 체인이 어떻게 형성되는지 살펴보았습니다. 이렇게 만들어진 스코프 체인으로 식별자 인식이 이루어집니다. 식별자 인식은 스코프 체인의 첫번째 변수 객체부터 시작합니다. 식별자와 대응되는 이름을 가진 프로퍼티가 있는지를 확인합니다. 함수를 호출할 때 스코프 체인의 가장 앞에 있는 객체가 변수 객체이므로, 이 객체에 있는 공식 인자, 내부 함수, 지역 변수에 대응되는지 먼저 확인합니다. 첫 번째 객체에 대응되는 프로퍼티를 발견하지 못하면, 다음 객체로 이동하여 찾습니다. 이런 식으로 대응되는 이름의 프로퍼티를 찾을 때까지 계속됩니다. 여기서 this는 식별자가 아닌 키워드로 분류되므로, 스코프 체인의 참조 없이 접근할 수 있음을 기억합시다.

클로저

클로저는 자바스크립트의 중요 개념 중 하나로 함수를 일급 객체로 취급하는 함수형 언어에서 사용되는 중요한 특징입니다. 보통 함수가 실행되면 실행 컨텍스트가 생성되고, 다시 반환되면서 함수의 생명주기가 끝나게 됩니다. 하지만 클로저를 사용하면 함수의 생명을 연장 시킬 수 있습니다.

클로저를 간단하게 설명하면 외부함수에서 반환된 내부 함수가 반환 이후에도 생명 주기가 유지되어 외부함수의 변수 객체 안을 탐색 할 수 있는 것을 의미합니다.

아래 예제를 살펴보겠습니다.

function outerFunc() {
    var x = 10;
    var innerFunc = function() { console.log(x); }
    return innerFunc;
}

var inner = outerFunc();
inner();

Untitled Diagram

앞서 배운 그림과 크게 다르지 않습니다. innerFunc()의 [[scope]]은 outerFunc 변수 객체와 전역 객체를 가집니다. 그런데 여기서 잠깐 혼란스러운 부분이 있습니다. 위 예제에서 innerFunc()은 outerFunc()의 실행이 끝난 후 실행됩니다. 그렇다면 outerFunc() 실행 컨텍스트가 사라진 이후에 innerFunc 실행 컨텍스트가 생성되는 것인데, innerFunc()의 스코프 체인은 outerFunc 변수 객체를 여전히 참조할 수 있을까요? 위 예제 결과를 보면 짐작할 수 있지만, outerFunc 실행 컨텍스트는 사라졌지만, outerFunc 변수 객체는 여전히 남아있고, innerFunc의 스코프 체인으로 참조되고 있습니다. 이것이 바로 자바스크립트에서 구현한 클로저라는 개념입니다.

이전 포스팅에서 언급했듯이, 자바스크립트의 함수는 일급 객체로 취급됩니다. 이는 함수를 다른 함수의 인자로 넘길 수 있고, return으로 함수를 통째로 반환할 수도 있음을 의미합니다. 이러한 기능은 앞 예제와 같은 코드가 가능합니다. 여기서 최종 반환되는 함수가 외부 함수의 지역 변수에 접근하고 있다는 것이 중요합니다. 이 지역변수에 접근하려면, 함수가 종료되어 외부 함수의 컨텍스트가 반환되더라도 변수 객체는 반환되는 내부 함수의 스코프 체인에 그대로 남아있어야만 접근할 수 있습니다. 이것이 바로 클로저입니다. 클로저는 이미 많은 곳에서 정의내리고 있지만, 그 정의만 보고는 쉽게 이해하기 어렵습니다. 여기서는 조금 쉽게 풀어서 정의하겠습니다. 이미 생명주기가 끝난 외부함수의 변수를 참조하는 함수를 클로저 함수라고 합니다. 따라서 앞 예제에서 outerFunc에서 선언된 x를 참조하는 innerFunc가 클로저가 됩니다. 그리고 클로저로 참조되는 외부 변수 즉, outerFunc의 x와 같은 변수를 자유 변수라고 합니다. closure라는 이름은 함수가 자유 변수에 대해 닫혀있다는 의미인데, 우리말로 의역하면 자유 변수에 엮여있는 함수라는 표현이 맞을 듯 합니다.

클로저를 이해했다면 다음 예제를 보고 결과값을 예측해봅시다.

function outerFunc(arg1, arg2){
    var local = 8;
    function innerFunc(innerArg){
        console.log((arg1 + arg2)/(innerArg + local));
    }
    return innerFunc;
}

var exam1 = outerFunc(2, 4);
exam(2);

앞 예제에서는 outerFunc() 함수를 호출하고 반환되는 함수 객체인 innerFunc()가 exam1으로 참조됩니다. 이것은 exam1(n)의 형태로 실행될 수 있습니다.

여기서 outerFunc()가 실행되면서 생성되는 변수 객체가 스코프 체인에 들어가게 되고, 이 스코프 체인은 innerFunc의 스코프 체인으로 참조됩니다. 즉, outerFunc() 함수가 종료되었지만, 여전히 내부 함수(innerFunc())의 [[scope]]으로 참조되므로 가비지 컬렉션 대상이 되지 않고, 여전히 접근 가능하게 살아있습니다. 따라서 이후에 exam1(n)을 호출하여도, innerFunc()에서 참조하고자 하는 변수 local에 접근할 수 있습니다. 클로저는 이렇게 만들어집니다. 이 outerFunc 변수 객체의 프로퍼티 값은 여전히 읽기 및 쓰기까지 가능합니다.

클로저의 활용

클로저의 개념을 이해했다면, 이제 이 클로저를 어떻게 활용할 것인지 고민해야 합니다. 앞에서 설명했듯이, 클로저는 성능적인 면과 자원적인 면에서 약간 손해를 볼 수 있으므로 무차별적으로 사용해서는 안됩니다. 사실 클로저를 잘 활용하려면 경험이 가장 중요하게 작용합니다. 한마디로... 계속 클로저에 대해서 공부하고 반복적으로 코딩을 하면... 된다는 말인데..아는만큼 보인다는것 같습니다.

특정 함수에 사용자가 정의한 객체의 메서드 연결하기

function HelloFunc(){
    this.greeting = "hello";
}


HelloFunc.prototype.call = function(func) {
    func ? func(this.greeting) : this.func(this.greeting);
}

var userFunc = function(greeting) {
    console.log(greeting);
}

// 생성자 함수를 이용한 객체 생성
var objHello = new HelloFunc();

objHello.func = userFunc;
objHello.call();

함수 HelloFunc는 greeting 변수가 있고, func 프로퍼티로 참조되는 함수를 call() 함수로 호출합니다. 사용자는 func 프로퍼티에 자신의 정의한 함수를 참조시켜 호출할 수 있습니다. 다만, HelloFunc.prototype.call()을 보면 알 수 있듯이 자신의 지역 변수인 greeting만을 인자로 사용자가 정의한 함수에 넘깁니다. 앞 예제에서 userFunc() 함수를 정의하여 objHello.func()에 참조시킨 뒤, HelloFunc()의 지역 변수인 greeting을 화면에 출력시킵니다. 앞 코드를 실행시키면 다음과 같습니다.

출력결과: hello

함수의 캡슐화

다음과 같은 함수를 작서안다고 가정해봅시다.

I am XXX, I live in XXX. I'am XX years old라는 문장을 출력하는데, XX 부분은 사용자에게 인자로 입력 받아 값을 출력하는 함수 입니다.

가장 먼저 생각할 수 있는 것은 앞 문장 템플릿을 전역 변수에 저장하고, 사용자의 입력을 받은 후, 이 전역 변수에 접근하여 완성된 문장을 출력하는 방식으로 함수를 작성하는 것입니다. 이 방식으로 구현하면 코드는 다음과 같습니다.

var buffAr = [
    'I am',
    '',
    '. I live in ',
    '',
    ' I\'am ',
    '',
    ' years old',
];


function getCompletedStr(name, city, age){
    buffAr[1] = name;
    buffAr[3] = city;
    buffAr[5] = age;

    return buffAr.join('');
}

var str = getCompletedStr('junyoung', 'jungja', 29);
console.log(str);

하지만 위의 예제는 단점이 있습니다. 바로 buffAr이라는 배열은 전역 변수로서, 외부에 노출되어 있다는 점입니다.

이는 다른 함수에서 이 배열에 쉽게 접근하여 값을 바꿀수도 있고, 실수로 같은 이름의 변수를 만들어 버그가 생길 수도 있습니다. 이는 특히 다른 코드와의 통합 혹은 이 코드를 라이브러리로 만들어 버그가 생길 수도 있습니다. 실제로 다른 사람이 사용할 라이브러리로 만들려고 할 때, 까다로운 문제를 발생시킬 가능성이 있습니다. 실제로 다른 사람이 사용할 라이브러리를 만들려고 하는 개발자는 이러한 충돌 가능성이을 충분히 대비해서 라이브러리를 작성해야만 합니다. 앞 예제의 경우, 클로저를 활용하여 buffAr을 추가적인 스코프에 넣고 사용하게 하면, 이 문제를 해결할 수 있습니다.

var getCompletedStr = (function() {

    var buffAr = [
        'I am',
        '',
        '. I live in ',
        '',
        ' I\'am ',
        '',
        ' years old',
    ];

    return (function(name, city, age) {
        buffAr[1] = name;
        buffAr[3] = city;
        buffAr[5] = age;

        return buffAr.join('');
    });
})();

var str = getCompletedStr('zzoon', 'seoul', 16);
console.log(str);

예제에서 가장 먼저 주의해서 봐야 할 점은 변수 getCompletedStr에 익명의 함수를 즉시 실행시켜 반환되는 함수를 할당하는 것입니다. 이 반환되는 함수가 클로저가 되고, 이 클로저는 자유 변수 buffAr을 스코프 체인에서 참조할 수 있습니다.

setTimeout()에 지정되는 함수의 사용자 정의

첫 번째로 소개했던 활용 예제와 유사합니다. setTimeout 함수는 웹 브라우저에서 제공하는 함수인데, 첫 번째 인자로 넘겨지는 함수 실행의 스케쥴링을 할 수 있습니다. 두 번째 인자인 밀리 초 단위 숫자만큼의 시간 간격으로 해당 함수를 호출합니다. setTimeout()으로 자신의 코드를 호출하고 싶다면 첫 번째 인자로 해당 함수 객체의 참조를 넘겨주면 되지만, 이것으로 실제 실행될 때 함수에 인자를 줄 수 없습니다. 그렇다면 자신이 정의한 함수에 인자를 넣어줄 수 있게 하려면 어떻게 해야 할까요? 역시 갓 클로저로 해결할 수 있습니다.

아래 예제 코드를 다시 살펴보겠습니다.

function callLater(obj, a, b) {
    return (function() {
        obj["sum"] = a + b;
            console.log(obj["sum"]);
    });
}

var sumObj = {
    sum : 0
}

var func = callLater(sumObj, 1, 2);
setTimeout(func, 500);

위의 예제를 살펴보면 사용자가 정의한 함수 callLater를 setTimeout 함수로 호출하려면, 변수 func에 함수를 반환받아 setTimeout() 함수의 첫 번째 인자로 넣어주면 됩니다. 반환받는 함수는 당연히 클로저고, 사용자가 원하는 인자에 접근할 수 있습니다.

클로저를 사용할 때 주의사항

지금까지 클로저의 개념과 활용을 알아봤습니다. 앞서 언급한 대로 클로저는 자바스크립트의 강력한 기능이지만, 너무 남발하여 사용하면 안됩니다. 여기서는 클로저에서 사용자가 쉽게 간과할 수 있는 사항을 정리했습니다.

클로저의 프로퍼티값이 쓰기 가능하므로 그 값이 여러 번 호출로 항상 변할 수 있음에 유의해야 합니다.

function outerFunc(argNum) {

    var num = argNum;
    return function(x) {
        num += x;
        console.log('num: ' + num);
    }
}

var exam = outerFunc(40);
exam(5);
exam(-10);

예제에서 보는 바와 같이 exam 값을 호출할 때마다, 자유 변수 num 값은 계속해서 변화하니 주의하기 바랍니다.

하나의 클로저가 여러 함수 객체의 스코프 체인에 들어가 있는 경우도 있습니다.

function func() {
    var x = 1;
    return {
        func1: function() { console.log(++x); },
        func2: function() { console.log(-x); }
    };
};

var exam = func();
exam.func1();
exam.func2();

위 예제에서 반환되는 객체에는 두 개의 함수가 정의되어 있는데, 두 함수 모두 자유 변수 x를 참조합니다. 그리고 각각의 함수가 호출될 때마다 x 값이 변화하므로 유의해야 합니다.

루프 안에서 클로저를 활용할 때는 주의합시다.

이번에 소개할 주의사항은 클로저 설명에서 단골손님처럼 등장하는 예제 코드입니다.

function countSeconds(howMany) {
    for(var i = 1; i <= howMany; i++) {
        setTimeout(function() {
            console.log(i);
        }, 1000);
    }
};

countSeconds(3);

위 에제는 1, 2, 3을 1초 간격으로 출력하는 의도로 만든 에제입니다. 하지만 결과는 4가 연속 3번 1초 간격으로 출력됩니다. 클로저를 잘 이해했다면, 이유를 쉽게 이해할 수 있을 것입니다. setTimeout 함수의 인자로 들어가는 함수는 자유 변수 i를 참조합니다. 하지만 이 함수가 실행되는 시점은 countSeconds() 함수의 실행이 종료된 이후이고, i의 값은 이미 4가 된 상태입니다. 그러므로 setTimeout()로 실행되는 함수는 모두 4를 출력하게 됩니다.

이제 원하는 결과를 얻기 위해 이 코드를 수정하겠습니다. 이를 위해서는 루프 i 값 복사본을 함수에 넘겨줍니다. 이를 위해 즉시 실행 함수를 사용했습니다.

function countSeconds(howMany) {
    for(var i =1; i<= howMany; i++) {
        (function (currentI) {
            setTimeout(function() {
                console.log(currentI);
            }, currentI * 1000);
        }(i));
    }
};

countSecond(i);

즉시 실행 함수를 실행시켜 루프 i 값을 currentI에 복사해서 setTimeout() 들어갈 함수에서 사용하면, 원하는 결과를 얻을 수 있습니다.

지금까지 실행 컨텍스트, 스코프 체인 및 클로저를 알아봤습니다. 우리가 자바스크립트 라이브러리를 만들고자 할 때 이에 대한 지식 없이 만든다면 이름 충돌, 성능 저하, 비효율적인 자원활용 등의 문제가 틀림없이 발생할 것이고, 이는 좋은 라이브러리라고 할수 없습니다. 반드시 자식을 갖춘 후 개발합시다.

인사이드 자바스크립트