본문 바로가기

TIL

JS. Closure

그렇다 클로저다. 언제 한번 정리를 해야겠다고 생각했는데, 어디서 보기만 했지 내가 진정 이해하고 있는지 모르겠어서 다시 정리해보려 한다.  MDN에서 클로저 검색해 보면 렉시컬 스코프부터 알고 가야된다고 한다.

그래서 간단하게 Lexical Scope에 대해 정리하고 가자.

다음은 MDN에 있는 코드다

    var name = "gozilla"
    function init() {
      var name = "Mozilla"; // name은 init에 의해 생성된 지역 변수이다.
      function displayName() { // displayName() 은 내부 함수이며, 클로저다.
        alert(name); // 부모 함수에서 선언된 변수를 사용한다.
      }
      displayName();
    }
    init();

결과는 Mozilla라 나올텐데, displayName의 외부에 있는 name을 참조해서 가져올 것이다. 이건 저번에 실행 컨텍스트가 어떻게 생성되는지 설명한걸 보면 이해가 될거다. scope체인을 따라서 찾다가 부모 함수에 name을 찾아서 출력하는거다.

근데 저기서 displayName이 init밖에 선언 되어 있다면 gozilla가 alert창에 뜰거다.

    var name = "gozilla"
    function init() {
      var name = "Mozilla"; // name은 init에 의해 생성된 지역 변수이다.
     
      displayName();
    }
     function displayName() { // displayName() 은 내부 함수이며, 클로저다.
        alert(name); // 부모 함수에서 선언된 변수를 사용한다.
      }
    init();

렉시컬 스코프는 정적 스코프라고 하는데 어디서 호출하였나가 중요한게 아니라 어디서 선언하였는지에 따라 상위 스코프를 결정하는 것이다.

 

이제 그럼 클로저를 보자

    function makeFunc() {
      var name = "Mozilla";
      function displayName() {
        alert(name);
      }
      return displayName;
    }

    var myFunc = makeFunc();
    //myFunc변수에 displayName을 리턴함
    //유효범위의 어휘적 환경을 유지
    myFunc();
    //리턴된 displayName 함수를 실행(name 변수에 접근)

여기서 결과는 같을거다. 근데 흥미로운 점은 외부함수인 makeFunc로부터 리턴되어 myFunc 변수에 저장되는것이다. 보통은 makeFunc 실행이 끝나면 name 변수에 더이상 접근할 수 없게 될 것으로 예상하는 것이 일반적인데, 우리의 JS는 다르다. 자바스크립트는 함수를 리턴하고, 리턴하는 함수가 클로저를 생성하기 때문이다. 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다. 위의 예시에서는 쉽게 말하면 myFunc가 name이 갖고 있는 값 유지한다는 거다.

다음 예제를 보자

function makeAdder(x) {
      var y = 1;
      return function(z) {
        y = 100;
        return x + y + z;
      };
    }

    var add5 = makeAdder(5);
    var add10 = makeAdder(10);
    //클로저에 x와 y의 환경이 저장됨

    console.log(add5(2));  // 107 (x:5 + y:100 + z:2)
    console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
    //함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산

여기서도 보면 x로 받은 인자값을 기억하는 함수에 다시 2를 인자로 받아서 결과를 내는데, 또한 이미 리턴된 함수에서 외부 값인 y값을 100으로 변경하는 것을 볼 수 있다. 당연히 x값도 변경이 가능하다.

또 다른 예시로 클로저를 이용해서 private method를 흉내낼 수 있다. private method를 간단하게 정리하면 자바같은 언어에 클래스가 있는데 거기 클래스 내부의 다른 메소드에서만 그 메소드들을 호출할 수 있다.

    var counter = (() =>{
      var privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        increment: function() {
          changeBy(1);
        },
        decrement: function() {
          changeBy(-1);
        },
        value: function() {
          return privateCounter;
        }
      };
    })();

    console.log(counter.value()); // logs 0
    counter.increment();
    counter.increment();
    console.log(counter.value()); // logs 2
    counter.decrement();
    console.log(counter.value()); // logs 1

여기서 익명함수를 만들어서 바로 실행시키는데,  함수안의 privateCounter와 changeBy함수는 외부에서 접근할 수 없고 반환되는 3개의 함수에의해서 접근 가능하다.

또한 위의 함수를 다른 변수에 담고 각 Counter를 따로만들어서 호출하면 다른 결과를 낼것이다. 즉 하나의 클로저에서 값을 변경해도 다른 클로저의 값에는 영향을 주지 않는다. 코드는 밑에와 같다.

    var makeCounter = function() {
      var privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        increment: function() {
          changeBy(1);
        },
        decrement: function() {
          changeBy(-1);
        },
        value: function() {
          return privateCounter;
        }
      }
    };

    var counter1 = makeCounter();
    var counter2 = makeCounter();
    alert(counter1.value()); /* 0 */
    counter1.increment();
    counter1.increment();
    alert(counter1.value()); /* 2 */
    counter1.decrement();
    alert(counter1.value()); /* 1 */
    alert(counter2.value()); /* 0 */

setTimeout 예시

이건 저번에 이야기한 자바스크립트 런타임에 대해 보고 오는게 좋다. 

먼저 아래의 코드를 보면 이 코드는 100이 100번 찍힐 것이다. 그 이유는 setTimeout은 비동기 처리되는데 콜백큐에 대기하다가 이벤트 루프애 의해 콜스택으로 올라오는데 그때 이미 i는 100이여서 그렇다.

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

그래서 아래처럼 바꿔야 한다.

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

이렇게 되면 각 i마다 call을 실행 시켜 0부터 잘 호출이 되는 것을 볼 수 있다.

수정 결과

클로저에 대해 문장으로 정리를 조금 하고 마쳐야 겠다.

 

쉽게 이야기 하면 클로저는 클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말한다. 또한 변수의 복사본을 참고하는 것이 아니라 실제 변수에 접근하는 것이다. 찾아보니 이게 가비지 컬렉터의 동작 방식과도 관련이 있다고 하는데, 어떤 값을 참조하는 변수가 하나라도 있으면 수집 대상에 포함 시키지 않는다고 한다. 그래서 가능한거 같다.

 

다른 예시들은 나중에 추가하겠습니다.

 

출처 및 참고:

https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures

 

클로저 - JavaScript | MDN

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.

developer.mozilla.org

https://chrisjune-13837.medium.com/%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%EB%9E%80-d0150952b9df

 

[자바스크립트] 클로저란?

자바스크립트의 정적 유효범위(어휘적 스코프)를 이해하고, 이를 응용한 클로저에 대하여 알아봅니다

chrisjune-13837.medium.com

https://poiemaweb.com/js-closure

 

Closure | PoiemaWeb

클로저(closure)는 자바스크립트에서 중요한 개념 중 하나로 자바스크립트에 관심을 가지고 있다면 한번쯤은 들어보았을 내용이다. execution context에 대한 사전 지식이 있으면 이해하기 어렵지 않

poiemaweb.com

 

'TIL' 카테고리의 다른 글

DOM. 관련 개념 정리  (0) 2022.01.19
JS. this  (0) 2022.01.19
Redux, Redux-toolkit은?  (0) 2022.01.16
JS. 자바스크립트 번들러 그전에 모듈 시스템  (0) 2022.01.15
JS. 코드 스플리팅 그리고 트리 쉐이킹  (0) 2022.01.14