[purplepig4657] 자바스크립트의 클로저

ARGOS JavaScript·2021년 10월 24일
0

클로저의 개념

클로저는 '함수'이다. 어떤 함수가 외부 함수의 지역변수에 접근한다면 그 함수는 '클로저'라 한다. 다음 예제를 보자.

function a() {
    var x = 0;
    var b = function() {
        console.log(x);
    }
    return b;
}

var c = a();
c(); // 0

다음 예제에서 함수 a에서 함수 b가 리턴된다. 여기서 b는 외부 함수의 변수 x를 참조한다. 이렇게 a함수 바깥에서 x가 참조될 수 있었고, 이것을 가능하게 하는 개념이 바로 클로저다. 또한 클로저로 참조되는 외부 변수 x를 자유 변수(free variable)이라 한다.

여기서 "a 함수의 실행 컨텍스트는 종료되었을 텐데 어떻게 변수 x를 참조할 수 있을까?"라는 의문이 들 수 있다. 하지만 이 의문은 '스코프 체인' 개념을 알고있다면, 쉽게 해결될 수 있다. a의 실행 컨텍스트가 종료되더라도 a의 변수 객체는 b의 스코프 체인으로 참조되고 있기 때문에 가능한 것이다. 클로저는 이렇게 스코프 체인의 뒷쪽 객체를 참조하는 특성상 성능 저하의 원인이 된다.

클로저의 활용

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

어떠한 함수는 인자를 하나만 받는다고 해보자. 그럼 사용자가 함수를 정의를 해도 한 개의 인자를 받는 함수를 정의할 수 밖에 없다. 하지만 여기서 더 많은 인자를 넘기고 싶다면 어떻게 해야 할까? 여기서 클로저가 사용될 수 있다. 다음 예시를 보자.

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

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

function saySomething(obj, methodName, name) {
    return (function(greeting) {
        return obj[methodName](greeting, name);
    });
}

function newObj(obj, name) {
    obj.func = saySomething(this, "who", name);
    return obj;
}

newObj.prototype.who = function(greeting, name) {
    console.log(greeting + " " + name);
}

var objHello = new HelloFunc();
var obj = new newObj(objHello, "a");
obj.call(); // hello a

이 예제에서 HelloFunc의 prototype의 call 함수는 인자에 greeting만을 받아 함수를 실행한다. 따라서 우린 두 개 이상의 인자를 넘기는 것이 불가능하다. 하지만 클로저를 사용한다면 이것이 가능해 질 수 있다.

이 예제의 saySomething 함수는 함수를 반환하는데 이 반환되는 함수가 클로저이다. 이 클로저는 인자로 greeting을 받고, obj["who"] 함수를 호출한다. 이 과정에서 saySomething의 name변수가 참조되므로 클로저라 할 수 있다. 이러한 과정을 거쳐 obj.call 함수를 호출하면 사실상 newObj.prototype.who 함수가 호출되는 것과 같으므로 인자를 두 개 이상 넣는 것과 같은 효과를 얻을 수 있다.

함수의 캡슐화

다음 두 예제를 비교해보자.

var array = ['a', 'b', 'c'];

function a() {
    return array;
}

var b = a();
console.log(b); // ["a", "b", "c"]
var obj = (function() {
    var array = ['a', 'b', 'c'];

    var a = function() {
        return array;
    }
    
    return a;
})();

var b = obj();
console.log(b); // ["a", "b", "c"]

첫 번째 예시와 두 번째 예시의 출력은 같다. 하지만 두 코드의 차이는 무엇일까?

첫 번째 예시의 문제점은 변수 array가 전역 변수라는 것이다. 이는 많은 문제를 발생시킬 수 있다.

  • 다른 함수에서 쉽게 접근하여 값을 변경 가능.
  • 같은 이름의 변수와 충돌 가능성.

이러한 문제점은 다른 코드와 통합할 때, 혹은 라이브러리 코드를 만들 때 많이 발생한다.

따라서, 변수를 함수 아래로 넣어 외부에 노출되지 않도록 하고, 클로저를 사용하여 자유 변수에 접근하도록 하는 두 번째 예시가 우려하는 문제를 일으킬 가능성이 낮아지는 것이다. 이를 함수의 캡슐화라 한다.

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

클로저의 여러 번 호출로 값이 항상 변할 수 있음을 상기

function a() {
    var b = 0;
    return (function c() {
        b += 1;
        console.log(b);
    });
}

var d = a();
d(); // 1
d(); // 2

이 예시처럼, a 함수의 자유 변수 b 값은 클로저 d에 의해 계속 변경될 수 있다. 이렇듯 자유 변수의 값은 계속 변화할 수 있다는 것을 유의해야 한다.

루프 안에서 클로저를 사용시 유의

function a(num) {
    for(var i = 0; i < num; i++) {
        setTimeout(function() {
            console.log(i);
        }, i * 1000);
    }
}
a(3);
// 3
// 3
// 3

이 예제는 0, 1, 2가 1초 간격으로 출력되는 것을 기대하고 작성하였다. 하지만, 출력은 3만 1초 간격으로 출력되었다.

위 예제에서는 클로저 함수가 자유 변수 i를 참조하고 있다. 일단 setTimeout의 두 번째 인자의 i는 각각 0, 1, 2가 들어갔을 것이다. 하지만 실제로 setTimeout함수에서 호출되는 클로저는 그렇지 않다. 왜냐하면, 클로저를 실행할 때는 이미 i가 0, 1, 2을 지나 3이 된 상태일 것이기 때문이다. 따라서 기대한 것과는 다르게 3만 세 번 출력되었다.

function a(num) {
    for(var i = 0; i < num; i++) {
        (function(num) {
            setTimeout(function() {
                console.log(num);
            }, num * 1000);
        })(i);
    }
}
a(3);
// 0
// 1
// 2

다음 예제는 위의 문제를 해결한 것이다. i를 익명 함수의 인자로 넘겨주었고, 그 익명 함수에 안에서 setTimeout함수가 실행된다. 이렇게 한다면 기대한 결과가 출력되게 된다.

profile
ARGOS JavaScript 정복하기

0개의 댓글