ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 클로저
    자바스크립트 2021. 9. 4. 17:00

     

    여러 블로그들과 책을 통해 클로저와 관련 개념들을 정리했습니다. 오류가 있을 수 있습니다.

    문제 예시

    var arr = [];
    for (var i = 0; i < 3; i++) {
      arr[i] = function () {
        return i;
      };
    }
    for (var j = 0; j < arr.length; j++) {
      console.log(arr[j]());
    }
    
    // 결과값 3 3 3

    코드 결과값이 0 1 2 로 나올 것이라 생각할 수 있지만 사실 위 코드를 실행하면 결과값이 3 3 3으로 나옵니다.

    어떻게 이런 결과가 나왔는지 이해하기 위해 몇 가지 개념들을 알아보겠습니다.

     

    실행 컨텍스트 & 렉시컬 환경 & 렉시컬 스코프

    var one = 1;
    
    function firstFunc() {
      var two = 2;
      function secondFunc() {
        var three = 3;
        console.log(one + two + three);
      }
      secondFunc();
    }
    firstFunc();

     

    실행컨텍스트와 실행컨텍스트 스택

    함수 내 함수들이 많은 위의 예시는 어떻게 코드가 진행이 될까요?

     

    우선  자바스크립트는 전역 코드부터 평가합니다. 평가를 통해 변수 선언문과 함수 선언문을 실행하고 선언한 변수와 함수를 실행 컨텍스트가 관리하는 스코프에 등록을 합니다. 

     

    그 다음, 평가를 마친 코드를 실행합니다. 변수에 값을 할당하고 함수를 호출합니다. 함수를 호출하면 현재 스코프에서의 실행을 일시정지하고 해당 함수의 코드를 평가하고 실행합니다.

     

    함수 호출이 완료되면 호출 이전으로 돌아가 일시정지한 코드부터 실행을 합니다.

     

    이처럼 함수 실행 순서, 변수 식별자 관리, 스코프 관리 정리등의 복잡한 메커니즘을 실행컨텍스트를 통해 실행 및 관리합니다.

     

    그 중 전역코드 실행 중에 firstFunc 함수 호출. firstFunc 함수 실행 중에 secondFunc 함수 호출 및 실행. secondFunc 함수가 종료되면 firstFunc 함수 실행 후 종료. firstFunc함수 종료되면 전역코드 다시 실행 및 종료 등의 순서를 관리하는 곳을 실행컨텍스트 스택이라고 부릅니다.

     

    렉시컬 환경

    실행컨텍스트는 어디서 식별자 및 식별자 값 및 스코프 정보를 저장할까요?

     

    위의 설명처럼 자바스크립트는 전역코드부터 시작해서 함수를 호출해야 해당 함수를 평가하고 실행을 합니다. 코드 평가 및 실행을 하며, 실행컨텍스트는 렉시컬 환경이란 곳에 식별자와 해당 식별자의 값, 그리고 상위 스코프를 기억합니다. 렉시컬 환경은 두가지 컴포넌트로 구성됩니다.

    • 환경 레코드
    • 외부 렉시컬 환경에 대한 참조

     

    환경 레코드에는 현재 위치하는 스코프에 식별자 및 식별자의 값을 저장합니다.

    외부 렉시컬 환경에 대한 참조는 상위 스코프를 참조할 수 있도록 합니다.

     

    렉시컬 스코프

    상위 스코프는 어떻게 결정될까요?

     

    위의 설명을 봤을 때 상위 스코프는 어디서 누군가한테 호출했는지에 따라 결정할 것 같습니다만 아닙니다.

    자바스크립트는 상위 스코프를 함수를 어디서 정의 했는지에 따라 상위 스코프를 결정합니다. 이를 렉시컬 스코프라고 부릅니다. 

     

    var x = "global";
    
    function foo() {
      var x = "local";
      bar();
    }
    
    function bar() {
      console.log(x);
    }
    
    foo(); // global
    bar(); // global

     

    foo() 실행을 하면 bar함수는 상위 스코프를 자신을 불러낸 foo 함수가 아닌 작성 된 코드 위치 상 자신의 상위 스코프를 상위 스코프로 가리킵니다. 그렇기 때문에 foo 함수의 지역 변수인 var x="local"이 아닌 자신의 상위 스코프의 전역 변수 var x="global"을 참조합니다.

     

    클로저

    mdn에서 클로저를 어떻게 정의하는지 보겠습니다.

    A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

     

    클로저란 함수와 해당 함수의 주변을 둘러쌓는 참조값들(렉시컬 환경)과의 결합이다. 즉, 클로저를 통해 내부 함수는 바깥 함수가 갖고 있는 값들에 접근이 가능하다. 자바스크립트에서 클로저는 함수 생성 때 매번 생성된다. 

     

    클로저란 함수와 해당 함수의 렉시컬 환경과의 결합입니다. 실행컨텍스트, 렉시컬 환경, 렉시컬 스코프의 개념을 공부하고 클로저의 정의를 보니 클로저란 당연한 것이고 모든 함수는 클로저라고 볼 수 있습니다. 그러나 일반적으로 우리는 모든 함수에 클로저라고 하지 않습니다. 함수와 함수의 렉시컬 환경과의 결합이 평소에 비해 특별해 보일 때 클로저라고 부릅니다.

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

    mdn에서 볼 수 있는 클로저의 예시입니다. 위 코드를 세단계로 나누어 보겠습니다.

     

    1. makeFunc 함수 실행

    2. makeFunc 함수 종료

    3. myFunc 함수 실행

     

    1. makeFunc 함수를 실행하면 실행 컨텍스트는 해당 함수의 렉시컬 환경을 저장 후 displayName 함수를 반환합니다. 

    2. makeFunc 함수가 종료 되었으니 makeFunc 함수는 실행 컨텍스트의 스택에서 삭제가 됩니다.

    3. myFunc 함수를 실행을 하니 삭제된 줄 알았던 makeFunc의 환경레코드에 접근하여 var name="Mozilla"를 불러온다.

     

    생명주기가 끝난 상위 함수의 렉시컬 환경에 접근한 현상을 보고 우리는 클로저라고 부릅니다. 

     

    사실 makeFunc의 실행 컨텍스트가 실행 컨텍스트에서 삭제가 되어도 렉시컬 환경이 소멸하지는 않습니다. 그 이유는 myFunc에 의해 참조되고 있기 때문에 아직까지는 삭제 대상이 아니기 때문입니다. 

     

    클로저의 활용

    실행 컨텍스트의 스택에서 삭제가 되어 직접적으로 접근하기 어렵지만 렉시컬 환경이 살아있기에 렉시컬 스코프 상 내부 함수를 활용하여 간접적으로 변수 값에 접근이 가능합니다. 이런 특성을 활용하여 자바 등의 언어의 프라이빗 메서드를 비슷하게 흉내낼 수 있습니다.

     

    var myFunc = makeFunc();
    myFunc();
    
    var counter = (function () {
      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

     

    counter.value(), counter.increment(), counter.decrement()를 실행하면 상위 스코프의 렉시컬 환경의 함수 및 변수 값에 접근하는 것을 알 수 있습니다.

    문제 예시

    var arr = [];
    for (var i = 0; i < 3; i++) {
      arr[i] = function () {
        return i;
      };
    }
    for (var j = 0; j < arr.length; j++) {
      console.log(arr[j]());
    }
    
    // 결과값 3 3 3

     

    다시 첫번째 예시 코드로 돌아오겠습니다. 

     

    코드 결과값이 0 1 2 로 나올 것이라 생각할 수 있지만 사실 위 코드를 실행하면 결과값이 3 3 3으로 나옵니다. 그 이유는 첫번째 for문을 돌며 아래와 같이 결과값을 생성하기 때문입니다.

    var i = 3;
    arr[0] = function () {
      return i;
    };
    arr[1] = function () {
      return i;
    };
    arr[2] = function () {
      return i;
    };

     

    결과적으로 arr[0](), arr[1](), arr[2]() 함수는 해당 함수에 지역변수 i값을 갖고 있지 않아 상위 스코프의 i값을 참조할 수밖에 없습니다. 

    console.log(arr[0]()); // 3
    console.log(arr[1]()); // 3
    console.log(arr[2]()); // 3

     

    방법 1

     

    결과값을 0 1 2 로 나오기 위해서는 어떤 방안들이 있을까요?

    위의 문제는 클로저를 통해 해결할 수 있습니다.

    var arr = [];
    for (var i = 0; i < 5; i++) {
      arr[i] = (function (i) {
        return function () {
          return i;
        };
      })(i);
    }
    for (var j = 0; j < arr.length; j++) {
      console.log(arr[j]());
    }

     

    for문을 아까처럼 굳이 풀어쓰자면 아래와 같습니다.

    var i = 3;
    
    arr[0] = function (i = 0) {
      return function () {
        return i;
      };
    };
    
    arr[1] = function (i = 1) {
      return function () {
        return i;
      };
    };
    
    arr[2] = function (i = 2) {
      return function () {
        return i;
      };
    };

     

    익명함수를 한번 더 덮엇 매개변수 i를  함수의 지역 변수로 사용할 수 있도록 하는 방법입니다.

    console.log(arr[0]()); // 0
    console.log(arr[1]()); // 1
    console.log(arr[2]()); // 2

     

    원하는 대로 결과값을 반환했습니다.

     

    방법2

    var arr = [];
    for (let i = 0; i < 3; i++) {
      arr[i] = function () {
        return i;
      };
    }
    for (var j = 0; j < arr.length; j++) {
      console.log(arr[j]());
    }
    
    // 결과 값 0 1 2

     

    let과 var의 차이점은 무엇이기에 서로 다른 결과값을 반환하느 것일까요?

     

    var로 선언한 변수는 함수 레벨 스코프 특성을 지닙니다. 함수의 코드블록만을 지역 스코프로 바라봅니다. 따라서 var로 변수를 선언한 위의 for문의 변수는 지역 스코프가 아닌 전역 스코프 특성을 지닙니다.

     

    반면 let은 블록 레벨 스코프 특성을 지니기 때문에 함수, if문, for문, while문 등을 지역 스코프로 바라봅니다.

     

    이 때문에 둘의 렉시컬 환경은 서로 다르게 생성됩니다. 

     

    위의 예시에서 var로 선언한 경우 전역 렉시컬 환경에서 var x의 값을 업데이트 하는 형식으로 진행됩니다. 결국에 x=3의 참조값이 남습니다.

     

    let으로 선언한 경우 for문이 반복될 때마다 독립적인 렉시컬 환경을 생성하고 해당 식별자의 값을 유지합니다. 그 결과 변화한 let 변수마다 고유한 참조값이 남을 수가 있습니다.

     

    참고자료

     

    자바스크립트의 스코프와 클로저 : NHN Cloud Meetup

    자바스크립트의 스코프와 클로저

    meetup.toast.com

     

    클로저(Closures) 무엇인가? :: 마이구미

    이 글은 클로저(Closures) 에 대해 다룬다. 자바스크립트 개발 면접이라면, 필수적으로 물을 정도로 중요하다. 참고보다 번역에 가까운 글이 되었다. 참고 링크 - https://medium.com/dailyjs/i-never-understo

    mygumi.tistory.com

     

    클로저 - JavaScript | MDN

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

    developer.mozilla.org

     

    자바스크립트 클로저(Closure)

    (2020.01.29) 스코프 관점에서 볼 수 있도록 새로 작성했습니다. 자바스크립트에는 클로저(Closure)라는 개념이 존재합니다. 프로토타입 기반의 언어인 자바스크립트는 클로저를 통해서 클래스 기

    yuddomack.tistory.com

     

    모던 자바스크립트 Deep Dive - YES24

    『모던 자바스크립트 Deep Dive』에서는 자바스크립트를 둘러싼 기본 개념을 정확하고 구체적으로 설명하고, 자바스크립트 코드의 동작 원리를 집요하게 파헤친다. 따라서 여러분이 작성한 코드

    www.yes24.com

     

    '자바스크립트' 카테고리의 다른 글

    [넘블] DataFetching 모듈 설계하기  (0) 2022.06.30
    프로토타입 체인 & toString()  (0) 2021.08.31
Designed by Tistory.