on your mark

[Javascript] 클로저(Closure) 본문

WEB/Javascript

[Javascript] 클로저(Closure)

dev-olive 2022. 5. 10. 17:26

클로저

클로저란?

클로저는 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.... ? 이해가 되지 않는다.

흔히 함수 내에서 함수를 정의하고 사용하면 클로저라고 한다. 간단하게 정리하면 함수밖에서 선언된 변수를 함수 내부에서 사용할 때 클로저가 생성된다.

클로저를 이해하기 위해서는 lexical scoping규칙을 알고 있어야 한다. lexical scoping은 함수가 정의된 시점의 스코프 체인을 사용하여 함수가 실행된다는 뜻이다.(호출된 시점이 아님)

var name = "kim";
function outer(){
    var name = "olive";
    function inner(){
        return name;
    }
    return inner;
}
var innerFunc = outer();
var myName = innerFunc();
console.log(`name : ${myName}`); // "name : olive"

outer 함수의 반환으로 인하여 global 스코프 영역으로 inner함수가 꺼내졌지만, 이를 실행하였을 때 inner 함수 내부의 name이 가리키고 있는 것은 outer 함수 스코프 영역의 name("olive")을 참조하고 있다.(global 스코프 영역의 "name"도 아니고, inner함수 스코프 영역의 name(undefined)도 아님)

outer는 이미 호출된 상태이다. 원래 함수와 그 함수 내에 정의된 지역변수는 함수의 실행이 끝나면 사라질 것이라고 생각할 수 있다. 그럼에도 innerFunc을 실행했을 때에 outer 함수의 지역 변수였던 name 변수를 정상적으로 참조하고 있다.

이는 lexical scoping 규칙에 의거한 클로저의 특성인데, 함수는 정의되었을 때의 스코프 체인을 사용해서 실행된다. 즉 inner 함수가 정의된 스코프 체인에서 name 변수는 "olive"로 바인딩 되었고, inner가 어디서 호출되든 상관없이 inner가 실행될 때 이 바인딩은 항상 유효하다.

이렇게 없어진 것 같은 스코프에 대한 참조를 기억하고 있는 것 을 클로저라고 한다.

더 자세한 설명(Click)


자바스크립트 함수가 호출되면, 호출과 관련된 지역 변수를 보관하는 객체가 생성되고, 이 객체는 함수의 스코프 체인에 추가된다. 함수가 리턴되면 객체와 바인딩된 변수는 스코프 체인에서 제거된다.
만약 inner함수가 정의되어 있다면, inner함수에는 스코프 체인에 대한 참조가 있고, 이 스코프 체인은 객체와 바인딩된 변수들을 참조하고 있다.
만약 inner함수 객체가 outer함수 내부에서만 사용된다면, inner함수는 그들이 참조하는 변수들과 함께 가비지 컬렉션이 된다. 하지만 어떤 함수가 inner함수를 정의하고, 그 함수를 반환하거나 어딘가의 프로퍼티로 저장한다면 함수 외부에 inner함수에 대한 참조가 생기게 된다. 이 경우 중첩함수는 가비지 컬렉션이 되지 않고 중첩함수가 참조하는 변수 또는 가비지 컬렉션이 되지 않는다.

위의 예시로 다시 보자면, outer가 실행될 때 지역변수인 name이 바인딩된 객체로 만들어지고 이 객체가 스코프체인에 추가된다. outer가 반환됐을 때 이 객체 또한 사라져야 하지만, outer가 내부에 정의한 inner 함수가 반환되어 변수 innerFunc에 저장되었다. inner 함수에 대한 참조가 남아 있으므로 가비지 컬렉션이 되지 않고, inner 함수가 참조하고 있는 name 또한 가비지 컬렉션이 되지 않는다.

클로저를 통한 은닉화

클로저를 사용하여 외부에서 변수에 직접 접근하는 것을 제한할 수 있다.

function hello(name) {
    this._name = name;
}
hello.property.say = function() {
    console.log('Hello, ' + this._name;)
}
var hello1 = new hello('John');

hello1.say(); // "Hello, John"
hello1._name = 'anonymous';
hello1.say(); // "Hello, anonymous"

hello()로 생성된 객체들은 _name이라는 변수를 가지게 된다. 이 변수를 private하고 쓰고 싶다는 것을 알 수 있지만 외부에서도 쉽게 접근이 가능한 것을 알 수 있다.

function hello(name){
    var _name = name;
    return function(){
        console.log('Hello, ' + _name);
    };
}

var hello1 = hello('John');
var hello2 = hello('Ryan');
var hello3 = hello('Olive');

위와 같이 클로저를 이용하면 외부에서 _name에 접근할 방법이 없다.

반복문 클로저

var i;
for(i=0; i<5; i++){
    setTimout(function() {
        console.log(i);
    }, 100);
}

위 코드의 결과 3초 뒤 5를 다섯 번 로그한다. setTimeout 안에 있는 콜백함수들도 같은 스코프 체인을 공유하며 i의 값을 공유하였기 때문이다.

var i;
for (i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);
        }, 100);
    })(i);
}

같은 함수 내에서 정의된 클로저들은 같은 스코프 체인을 공유하는데, IIFE(즉시 실행 함수 표현 : Immediately Invoked Function Expression)로 감싸져 있어서 공유가 불가능하게 된다. 같은 함수 내부에 정의된 중첩 함수들은 같은 스코프 체인을 공유하는데 여기서는 같은 함수 조건이 충족되지 않는다. 변수 j를 둘러싼 IIFE 함수가 바로 실행되고 사라지기 때문에 루프고 돌 떄마다 매번 다른 함수이므로 j는 공유가 불가능하다.

var i;
for (i = 0; i < 5; i++) {
    function inner(j){
       setTimeout(function() {
            console.log(j);
        }, 100);
    }
    inner(i);
}

IIFE가 아닌 그냥 함수로 감싸고 호출해도 된다. inner함수 또한 호출되고 사라지므로 매번 다른 스코프를 만들어서 j라는 매개 변수를 공유할 수 없게 할 것이다.

Reference

https://developer-alle.tistory.com/369

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

https://yuddomack.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%81%B4%EB%A1%9C%EC%A0%80%EA%B7%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%B4%EB%B3%B4%EC%9E%90