ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JS/클로져] 자바스크립트의 Lexical scoping과 Closure (2)
    Frontend 2019. 6. 10. 00:01

    예전에도 한 번 이 주제에 대해서 글 쓴 적이 있는데, 

    사실 그 때는 완전히 이해가 안 된 상태에서 쓴 글이었다.

    MDN이나 W3Schools에 클로져 관련 글을 읽어봐도 쉽게 이해가 안갔는데

    이번주에 수업을 듣고 처음으로 이해가 갔다.!

    그래서 다시 한 번 개념 정리를 해보려고 한다.

     

     


     

    Lexical Scope

    Closure에 대해서 얘기하기 전에 우리는 Lexical Scope가 무엇인지 정확히 알고 넘어가야한다. 지난 번 글에서도 정리했는 데, Lexical Scoping은 함수를 어디에 선언했는지에 따라서 스코프가 결정되는 것을 말한다. 즉, 함수를 어디에서 호출했느냐는 스코프에 전혀 영향을 끼치지 않는다.

     

    var a = 1;
    
    function foo () {
      var a = 10;
      console.log(a);
    }
    
    function bar () {
      var a = 20;
      foo();
    }
    
    bar();

     

    위 코드를 보면 전역 공간에 a 변수가 선언되고 1이 할당되었다. 이 변수 a는 선언하는 순간  전역 변수가 된다.

     

    그리고 함수 foo를 선언했는데, 함수 foo안에 a 변수를 다시 선언하고 10을 할당했다. foo 함수를 선언하는 즉시, foo 함수 내부의 모든 변수는 foo 함수 스코프에 속하게 된다. foo 함수 내부의 변수 a는 foo 함수 스코프에 속하는 지역 변수가 된다. 

     

    이제 함수 bar를 선언하고 그 안에서 또 다시 a 변수를 선언하고 20을 할당했다. 이 a 변수는 bar 함수 스코프에 속한 지역 변수이다. 이제 bar 함수 내부에서 foo 함수를 실행하는 코드를 추가하였다.

     

    이 상황에서 bar 함수를 실행했을 때, 콘솔 창에는 어떤 숫자가 찍힐 것인가? 바로 10이 출력된다. foo 함수는 bar 함수 내부에서 실행됐지만 Lexical scoping에 의해 함수 스코프는 foo 함수가 선언되었을 때 이미 결정되었다. foo 함수 내부에 변수 a가 있으므로 10을 출력하는 것이다.

     

     

     

    var a = 1;
    
    function foo () {
      console.log(a);
    }
    
    function bar () {
      var a = 20;
      foo();
    }
    
    bar();

     

    그렇다면 만약에 foo 함수 내부에 a 변수가 없다면 콘솔 창에는 어떤 숫자가 출력될까? 이번에는 바로 1이 출력된다. bar 함수 내부에 있는 변수 a는 bar 함수 스코프에 속한 것으로 foo 함수에 어떠한 영향도 끼치지 않는다. foo 함수가 실행되면 먼저 foo 함수 스코프 내에 a 변수가 있는 지 찾고, 변수를 찾지 못하면 상위 스코프에서 찾게 된다. foo 함수의 상위 스코프는 전역이므로 전역 공간에 있는 변수 a를 찾아 1을 출력한다.

     

    이렇게 함수를 어디에 선언했는지에 따라 범위가 결정되는 것을 Lexical Scoping이라고 하며, 반대로 함수가 어디서 호출됐는지에 따라 범위가 결정되는 방식은 Dynamic scoping이라고 한다.

     

     

     


     

    CLOSURE

    A closure is the combination of a function and the lexical environment within which that function was declared.

    (클로저는 함수와 그 함수가 선언되었을 때의 Lexical 환경의 조합. - MDN)

     

    클로저(Closure)란 어떤 함수가 본인이 선언된 주변 환경을 지속적으로 기억하는 것을 의미한다. 여기서 해당 함수가 어디서 실행되었는지는 중요하지 않다. Lexical scoping에 의해 해당 함수가 선언되었을 때의 환경의 영향만 받는다. 

     

    function foo () {
      var a = 2;
    
      function bar () {
        console.log(a);
      }
      bar();
    }
    foo();
    
    console.log(a);

     

    위와 같은 코드가 있을 때, foo 함수를 실행하면 foo 함수 내부에 선언된 bar 함수가 실행되는 데, bar 함수는 본인 스코프 내에서 a 변수를 찾다가, 못찾고 상위 스코프인 foo 스코프 내에서 a 변수를 찾을 것이다. 그래서 2를 출력하고 함수를 종료하게 된다.

     

    foo 함수의 실행이 끝나고나면 더 이상 a 변수에 접근할 수 없다. 위 코드를 실행하면 콘솔 창에는 2a is not defined 에러가 출력될 것이다.

     

     

     

    function say () {
      var a = 2;
      
      function log () {
        console.log(a);
      }
      
      return log;
    }
    
    var sayA = say();
    sayA();

     

    그런데 위의 코드를 살펴보자. say 함수 내부에 변수 a를 선언하고 숫자 2를 할당했다.

    그 다음, log라는 함수를 선언하였는데, log 함수는 콘솔 창에 a를 출력하는 함수이다.

    그리고 say 함수에는 log를 반환하는 리턴문이 있다.

     

    우리는 이제 변수 sayA를 선언한 후, say 함수를 실행하고 반환된 결과값, log 함수를 sayA 변수에 할당했다.

     

    이제 sayA()를 실행하는 것은 log() 함수를 실행하는 것과 같다.

    만약에 자바스크립트에 클로저가 없다면 위 코드를 실행하면 a is not defined 에러가 떠야할 것이다.

     

    그러나 위 코드를 실행하면 2라는 값이 출력된다.

     

    위 코드에서 say 함수 안에 선언한 log 함수를 say 함수가 리턴하고 있고,

    그 리턴된 log 함수를 sayA 변수에 할당하였으므로 say 함수가 종료하고 난 이후에도 함수 내의 환경은 메모리에서 없어지지 않고 살아있다.

     

    즉, say 함수가 종료하고 난 이후에도 say 함수 스코프에 접근할 수가 있는 것이다.

     

     

    이렇게 클로저는 부모 함수의 실행이 끝났더라도 부모 스코프에 접근할 수 있는 함수를 말한다.

    (A closure is a function having access to the parent scope, even after the parent function has closed. - W3Schools)

     

     

     

    function doSometing () {
      var a = 2;
      function something () {
        console.log(a);
      }
    
      foo(something);
    }
    
    function foo (fn) {
      fn();
    }
    doSometing();

     

    위 코드는 뭔가 복잡해보이지만 뜯어보면 위 예제와 비슷한 코드이다.

    something 함수를 foo 함수의 인자로 넘겨주는데, foo 내부에서

    인자를 받아서 실행하고 있으므로 something 함수를 실행하는 것과 같다.

    something 함수는 foo 함수 내부에서 실행되고 있으나 

    함수가 실행되는 위치와는 상관없이 something 함수가 선언되었을 때의 스코프 영향을 받는다.

    그러므로 위 코드는 콘솔 창에 2를 출력한다.

     

     

     

    let foo;
    
    function doSomething () {
      const a = 1;
      
      function something () {
        console.log(a + b);
      }
      
      const b = 2;
      foo = something;
    }
    
    function say () {
      foo();
    }
    
    doSomething();
    say();

     

    이 예제도 뭔가 복잡해보이지만 그림을 그려보면 쉽다.

     

     

    위 코드의 실행순서를 따라가보면 먼저 doSomething() 함수가 실행된다.

    doSomething 함수 안에서 변수 a와 b, something 함수가 선언되었고, foo 함수에 something 함수가 할당되었다.

     

    이제 say() 함수가 실행된다. say 함수는 전역공간에 있는 foo 변수에 할당된 something 함수를 실행하게 된다.

    something 함수는 선언 시점의 본인 스코프와 부모 스코프의 환경을 모두 기억하고 있으므로 a 값에 접근할 수 있다. 

     

    여기서 재밌는 점은 b의 값에도 접근할 수 있다는 것이다. 

    b는 something 함수가 선언된 이후에 선언 및 할당되었다.

     

    그러나 클로저는 선언된 그 시점만 기억하는 것이 아니고 그 시점으로부터 다시 실행되는 그 순간까지 지속적으로 주변 스코프를 기억하고 있다. 그러므로 something 함수를 실행하는 시점은 b가 선언되고 할당된 시점 이후이므로 a와 b 값들을 모두 가져올 수 있다.

     

    그러므로 위 코드는 1 + 2 = 3이란 값을 출력한다.

     

     

     

    function makeAdder (x) {
      return function add (y) {
        return x + y;
      }
    }
    
    var addFive = makeAdder(5);
    var addTen = makeAdder(10);
    
    console.log(addTen(2)); // 12
    console.log(addFive(1)); // 6
    console.log(addTen(22)); // 32

     

    이제 이 코드를 살펴보자.

     

    makeAdder()라는 함수는 add()라는 함수를 리턴하도록 되어있다.

    이제 우리가 addFive라는 변수에 makeAdder(5);를 할당하였다. 그러면 makeAdder()를 실행한 결과값인 add() 함수의 주소가 리턴되어 addFive 변수에 할당되는데, 이 때 인자로 전달된 x의 값, 5가 클로저로 기억된다.

     

    addTen 변수에는 makeAdder(10);이 할당되었으므로 add() 함수의 주소값이 할당되며 인자로 전달된 10이 클로저로 기억된다.

     

    이제 addTen(2)을 실행시키면 add 함수가 비로소 실행된다. add 함수는 비록 선언되었을 당시에는 x의 값이 할당되지 않은 상태였다. 그러나 클로저에 의해 지속적으로 주변 환경을 기억하고 있었고, makeAdder에 10을 인자로 받은 상황을 기억하고 있으므로 x의 값은 10이 된다. 10에 y의 값으로 2가 전달되었으니 두 숫자를 더해 12를 출력한다. 아래 addFive(1)이나 addTen(22)도 같은 이유로 632를 출력한다.

     

     

     

    let foo;
    function doSomething () {
      let a = 1;
    
      function something () {
        a++;
        console.log(a);
      }
      foo = something;
    }
    
    function say () {
      foo();
    }
    doSomething();
    say();
    say();

    이제 이 예제를 보면 클로저가 함수가 선언된 시점의 스코프를 기억하는 것이 아니라 선언되고 난 이후의 환경 변화까지 실시간으로 기억한다는 것을 확실하게 알 수 있게 된다.

     

    위 예제는 something 함수 안에서 a를 1씩 증가시키고 있다.

    이제 say 함수를 여러 번 실행하면 실행한 만큼 a의 값이 증가된다.

     

     

     


     

    Closure를 이용한 카운터

    이러한 클로저의 특성을 이용하여 전역 변수에 a 변수를 선언하지 않고 a의 값을 증가시키는 카운트 함수를 만들 수 있다. 

     

    <button type="button" id="btnPlus">PLUS 1</button>
    <p id="numHere">0</p>
    function counter () {
      let num = 0;
      function increase () {
        num++;
        document.querySelector('#numHere').textContent = num;
      }
      return increase;
    }
    
    var countNum = counter();
    document.querySelector('#btnPlus').addEventListener('click', countNum);

     

    counter 함수 내부에 increase라는 내부 함수를 만들고 리턴시킴으로서 클로저를 만들었다.

    그리고 countNum이란 변수에 increase 함수를 참조시켰다.

     

    이제 Count 버튼을 클릭할 때마다 countNum에 할당된 함수를 실행시키면

    increase 함수가 실행되어 num 변수가 1씩 증가하게 된다.

     

     

    ※ 아래 버튼을 클릭하여 테스트해보세요.

     

    0

     

     

     


     

    정보 은닉 & 캡슐화

    객체지향 프로그래밍 기법의 기본 원칙에는 캡슐화와 정보 은닉이란 것이 있다. 캡슐화(Encapsulation)데이터(속성)와 데이터를 처리하는 함수를 하나로 묶는 것을 의미하며 캡슐화된 객체는 세부 내용이 외부로부터 은폐되어 오류의 파급 효과가 적다. 여기서 정보가 외부로부터 은폐되는 것을 정보 은닉(Information Hiding)이라고 하는 데, 다른 객체에게 자신의 정보를 숨기고 자신의 연산만을 통하여 접근에 허용하는 것을 말한다. 

     

    자바스크립트에서는 클로저를 사용하여 캡슐화와 정보 은닉과 같은 이점을 얻을 수 있다. 아까 위에서 만든 카운트 프로그램을 생각해보자. 만약에 우리가 클로저를 사용하지 않고 위와 같은 기능을 만들려고 했다면 아래 코드와 같이 num 변수를 전역 변수로 만들었을 것이다.

     

    let num = 0;
    function counter () {
      num++;
      document.querySelector('#numHere').textContent = num;
    }
    
    document.querySelector('#btnPlus').addEventListener('click', counter);

     

    이렇게 변수를 전역 공간에 선언하면 다른 모든 함수에서 num 변수에 접근할 수 있게 된다. 그러면 변수의 값이 누군가에 의해 쉽게 변경될 수 있기 때문에 오류 발생이 될 가능성이 커진다. 그러므로 우리는 외부 객체가 특정 객체의 데이터에 직접 접근하여 사용하거나 변경하지 못하게 만들어 프로그램의 안정성을 높이기 위해 클로저를 활용할 수 있다. 

     

    클로저를 사용하면 정보 은닉이란 장점이 있으나 주변 환경을 지속적으로 기억해야하기 때문에 컴퓨터의 메모리가 많이 사용된다는 단점도 있다. 프로그램의 성능에 영향을 미칠 수 있으므로 주의해서 사용해야 한다.

    반응형

    COMMENT