끄적끄적

자바스크립트 클로저 응용 및 개념이해 본문

Front-end/Javascript

자바스크립트 클로저 응용 및 개념이해

mashko 2019. 6. 6. 00:07
반응형

자바스크립트에 클로저의 이해 및 구현방법에 대해 알아보도록 합시다.
요즘은 자바스크립트 개발자라면 대부분이 알고 계실 클로저이고, 이것 역시 필수적으로 알아야 할 개념 중에 하나로 꼽히고 있습니다.

클로저
컴퓨터 언어에서 클로저(Closure)는 일급 객체 함수(first-class functions)의 개념을 이용하여 스코프(scope)에 묶인 변수를 바인딩 하기 위한 일종의 기술이다. 기능상으로, 클로저는 함수를 저장한 레코드(record)이며, 스코프(scope)의 인수(Factor)들은 클로저가 만들어질 때 정의(define)되며, 스코프 내의 영역이 소멸(remove)되었어도 그에 대한 접근(access)은 독립된 복사본인 클로저를 통해 이루어질 수 있다.(위키백과)

저 역시 많은 양의 글을 보았지만 위키백과만큼 정확하게 클로저에 대한 개념을 정리해주는 곳이 없는 것 같아서 위키백과의 글을 가져왔습니다. 클로저 코드를 보고 이해하시기전에 클로저란 무엇인지를 파악하셔야 합니다.
스코프가 끝났음에도 불구하고 내부 지역 변수에 접근이 가능한 형태로 캡슐화 은닉화를 구현하는데 사용 할 수 있습니다.
먼저 위에 글에서 일급 객체란? 다른 객체들에 일반적으로 적용 가능한 모든 연산을 지원하는 객체를 가르킵니다.
모든 연산은 보통 함수에 매개변수로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 합니다.

자바스크립트에서 클로저를 구현하는 기본적인 코드를 보고 이해해 봅시다.

function example(name) {
    var sayHi = 'hello';
    return function(message) {
        return sayHi + name + message;
    }
}
var closure = example('Closure');
console.log(closure('Nice')); // 'helloClosureNice'
console.log(closure('?')); // 'helloClosure?'

이처럼 함수의 스코프가 끝난 외부에서 지역 변수에 접근해 이와 연계된 무엇을 할 수 있게 됩니다.
예를 들어보죠.
먼저 비교를 위해 프로토타입으로 구현해 볼까요?

function Example() {
    this._count = 0;
}

Example.prototype.count = function(sum) {
    this._count += sum;
    return this._count;
}

var closure1 = new Example();
var closure2 = new Example();
console.log(closure1.count(1)); // '1'
console.log(closure1.count(1)); // '2'
console.log(closure2.count(1)); // '1'
console.log(closure2.count(1)); // '2'

this를 이용해 컨텍스트에 접근하는 일반적인 객체이고, 이것을 클로저를 통해 만들어 보도록 하죠.

function example() {
    var _count = 0;
    return function(sum) {
        _count += sum;
        return _count;
    }
}
var closure1 = example();
var closure2 = example();
console.log(closure1(1)); // '1'
console.log(closure1(1)); // '2'
console.log(closure2(1)); // '1'
console.log(closure2(1)); // '2'

두가지 코드를 비교해 보면 별로 차이는 없습니다.
하지만 잘 생각해보면 위에 코드는 this를 통해 _count변수에 접근했고, 아래의 코드를 봤을때 클로저를 이용해 구현하게 되면 컨텍스트에 접근할 때 스코프를 이용해 접근하기 때문에 this라는 키워드를 쓰지 않아도 됩니다. 단순한 코드레벨에서의 차이를 보면 스코프를 이용해 접근한 것이라는 차이만 있을 뿐이지만 숫자를 저장하는 _count라는 변수가 있고, count를 변경하는 함수가 있다. 객체의 캡슐화와 은닉화에 부합한다.
이해한 클로저를 통해 응용을 해보도록 합시다. 제일 대표적인 카운팅을 해보죠.

function counter() {
    var _count = 0;
    return {
        increase: function(num) {
            return _count += num;
        },
        decrease: function(num) {
            return _count -= num;
        },
        reset: function() {
            return _count = 0;
        }
    }
}
var counting = counter();
console.log(counting.increase(1)); // 1
console.log(counting.increase(1)); // 2
console.log(counting.decrease(1)); // 1
console.log(counting.decrease(1)); // 0
console.log(counting.increase(1)); // 1
console.log(counting.increase(1)); // 2
console.log(counting.increase(1)); // 3
console.log(counting.reset()); // 0

이 구조는 자바스크립트 싱글톤 객체를 구현할 때 자주 사용하는 모듈 패턴의 전형적인 모습이죠.
우리들이 개발을 하며 무의식중에 많이 사용하는 클로저가 있습니다. 클로저의 대한 개념이 없다면 고개를 갸웃뚱 하실만한 문제죠
반복문 setTimeout클로저입니다.

function count() {
    for (var i = 1; i < 10; i += 1) {
        setTimeout(function() {
            console.log(i);
        }, i*100);
    }
}
count();

기대 값은 무엇이 나올까요? 결과는 10만 9번 출력되게 됩니다.
위와 같은 코드는 많은 개발자들이 1,2,3,4,...9 를 0.1초마다 출력되게 생각 했을텐데 이상한 결과가 나온것이죠.
이유는 for문의 루프는 미리 실행되고 setTimeout()의 내부함수인 익명함수는 외부함수의 변수 i값을 가져와 함수를 실행하게 되는데 미리 실행된 for문 때문에 변수 i의 값이 바뀌게 되고 바뀐 값이 저장되게 되는것이죠. 이 부분의 클로저가 바로 i 입니다.
우리가 풀어오고 지나왔던 글들을 보면 단순히 i 라는 변수는 for문의 ()안에 들어가 있을 뿐 결국은 클로저가 되는 것이죠.
자세히 이해하면 for 문은 미리 실행되어 이미 10의 값을 가지고 있기 때문에 참조값을 가지는 클로저는 10을 출력하게 되는 것이죠.
이를 방지하려면 어떻게 해야 할까요?
첫번째는 즉시 실행 함수를 이용하여 for문이 실행할때마다 i 값을 참조값으로 넣어주는 방식(코드로 이해하죠)

function count() {
    for (var i = 1; i < 10; i += 1) {
        (function(counting) {
            setTimeout(function() {
                console.log(counting);
            }, i*100);
        })(i);
    }
}
count();

1,2,3,4,...9 순서대로 잘 동작하게 됩니다.
두번째는 ES6에서는 함수 스코프 var변수를 블록 스코프 let변수로 변경하는 방식(코드로 이해하죠)

function count() {
    for (let i = 1; i < 10; i += 1) {
        setTimeout(function() {
            console.log(i);
        }, i*100);
    }
}
count();

블록단위의 스코프인 let을 통해 함수 단위의 스코프가 아닌 블록단위의 스코프로 변경이 되면서
정상적인 출력이 되는 것으로 나옵니다.

클로저의 단점
가비지콜렉터에 정리 대상이 되어야 할 것들이 메모리 상에 남아 있게 되므로, 남발 시 비효율적인 메모리 사용을 하게 됩니다.
또한 퍼포먼스 측면에서도 단점이라 할 수도 있을 것 같네요.
클로저는 변수에 접근하려 하면 해당 클로저로 생성한 스코프들을 탐색해서 찾아야 합니다.
그러므로 적당히 적절한 때에 쓰는 것이 제일 바람직한 것 같습니다.

반응형
Comments