JavaScript의 클로저


JavaScript에서 클로저는 생성 당시의 외부 환경(lexical scope)를 기억하고 있는 함수와 그 외부 환경을 가리킨다. 일반적인 경우 함수가 생성될 때 함께 생성된 함수의 환경은 함수의 실행이 종료되면서 GC에 의해 메모리에서 제거돼야 한다. 하지만 생성된 함수의 레퍼런스를 함수 외부에서 참조하고 있는 경우에는 함수의 외부 환경이 유지되고 이 때 이 함수와 외부 환경을 클로저라 부르는 것이다.

왜 사용하는가?

정보의 은닉

클로저를 사용하는 주된 이유 중 한가지는 정보의 은닉이다. 클로저 내부의 데이터는 클로저 함수 외에는 접근할 수 있는 방법이 없으므로 데이터를 은닉하기에 적합하다.

아래 코드는 클로저를 이용해 카운터를 만든 예시이다.

function Counter() {
    let counter = 0;

    const inc = () => ++counter;
    const dec = () => --counter;

    return {
        inc,
        dec,
    };
}
const counter = Counter();

위 코드는 counter.inc()counter.dec()를 이용해 counter값을 변경할 수 있지만 다른 방법으로 값을 변경할 수 있는 방법이 없다. 하지만 Counter() 함수를 호출할 때마다 inc와 dec 함수를 매번 새로 생성해줘야 하기 때문에 메모리 관점에서 불리한 면이 있다. 따라서 객체 생성이 빈번한 경우에는 es6의 class나 constructor functions을 이용하는 것이 좋다. 심지어 Hash name을 이용하면 외부에서 정보의 은닉도 가능하다.

스코프 생성

클로저가 필요한 또 다른 경우는 임의로 스코프를 만들어줘야 하는 경우이다. 아래는 그에대한 아주아주 유명한 예시이다.

for (var i = 0; i < 10; i++) {  
    setTimeout(() => console.log(i), 100);  
}

위 코드는 언뜻 보면 0.1초마다 0~9까지의 숫자를 출력할 것으로 보인다. 하지만 실제로는 10만 10번 출력하는 것을 볼 수 있다. 첫 번쨰 timeout의 함수가 실행되기도 전에 for문은 실행이 종료되어 i 값이 10이 되어있기 때문이다. 이는 setTimeout 에 인자로 넘겨주는 함수를 새로운 스코프 안에서 실행시켜주면 된다.

for (var i = 0; i < 10; i++) {
    setTimeout(
        (
            (j) => () =>
                console.log(j)
        )(i),
        100 * i
    );
}

하지만 위 경우는 es6에서 블록 스코프가 생긴 이후로 클로저를 쓸 필요가 없어졌다. 단순히 블록 스코프를 갖는 let을 사용하면 원하는대로 동작하는 코드를 작성할 수 있다.

for (let i = 0; i < 10; i++) {
    setTimeout(() => console.log(i), 100 * i);
}

결론

글을 쓰면서 느낀 점은 지금같이 es6가 대세가 된 이후로는 꼭 클로저를 사용해야 하는 경우가 이제는 없다는 것이다. 개인적으로는 위의 Counter 같이 아주 간단한 데이터 + 함수의 object를 생성하는 경우를 제외하고는 클로저를 쓸 일은 없다고 생각한다.

참고 문서

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

https://hyunseob.github.io/2016/08/30/javascript-closure

https://meetup.toast.com/posts/86