클로저

movie·2022년 8월 8일
0
post-thumbnail

클로저란?

1️⃣

함수와 함수가 선언된 어휘적 환경(lexical environment)의 조합 - MDN

??????????????? 진짜 도대체 뭔 소리람 ??????????????????????

  • 함수 : 외부 함수안 내부 함수
  • 함수가 선언된 lexical environment : 내부 함수가 선언됐을 때의 스코프
  • 환경 : 클로저가 생성된 시점의 스코프 내에 있는 모든 지역 변수로 구성

즉, 클로저란 내부함수가 자신이 선언되었을 때의 환경인 스코프를 기억하면서 자신이 선언된 환경 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말한다.

2️⃣

자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부함수 밖에서 내부함수가 호출되더라도 외부함수의 지역변수에 접근할 수 있는데 이러한 함수를 클로저라고 부른다. - poiemaweb

  • 클로저는 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수다.
  • 실행 컨텍스트 관점에서 설명하면, 내부함수가 유효한 상태에서 외부함수가 종료하여 외부함수의 실행 컨텍스트가 반환되어도, 외부함수 실행 컨텍스트 내 활성객체는 내부함수에 의해 참조되는 한 유효하여 내부함수가 스코프 체인을 통해 참조할 수 있는 것


사진 출처 : poiemaweb

☁️ 들어가기 전 선 지식

1️⃣ 어휘적 범위 지정, Lexical Scoping

function outer() {
  var name = 'movie';
  
  function inner() {
   	console.log(name); 
  }
  // inner()는 outer() 내부에 정의된 함수이다. 
  // 즉 outer() 내부에서만 사용가능 하다. 
  // inner()는 자신만의 지역 변수를 가지지 않는다. 
  // 하지만 함수 내부에서 외부 함수의 변수에 접근할 수 있기 때문에 inner()는 outer()의 변수 name에 접근할 수 있다. 
  // outer()는 inner()의 상위 스코프이다. 
  // 만약 inner()가 전역에 선언되었다면 inner()의 상위 스코프는 전역 스코프가 된다. 
  
  inner(); // [log] movie
}

outer();
  • 스코프는 함수를 호출할 때가 아니라 함수를 어디에 선언하였는지에 따라 결정된다. 이를 lexical scoping이라고 한다.
  • 변수가 어디에서 사용 가능한지 알기 위해서는 그 변수가 어디에서 선언되었는지를 고려한다.
  • 중첩된 함수는 외부 스코프에서 선언한 변수에도 접근할 수 있다.

2️⃣ Lexical scope의 실체

function outer() {
  var name = 'movie';
  
  function inner() {
   	console.log(name); 
  }
  
  inner(); // [log] movie
}

outer();
  1. inner()가 호출되면 inner()의 실행 컨텍스트가 EC 스택에 쌓인다.
  2. EC 스택에 쌓이면 변수 객체, 스코프 체인, this에 바인딩할 객체가 결정된다.
  3. 스코프 체인은 '전역 스코프를 가르키는 전역 객체', 'outer() 스코프를 가르키는 함수', 'outer()의 활성 객체', '함수 자신의 스코프를 가르키는 활성 객체'를 순차적으로 바인딩한다.
  4. 스코프 체인이 바인딩한 객체가 lexical scope의 실체이다.

변수 객체 (variable object) = 활성 객체 (activation object)

  • 변수, 매개변수, 인수, 함수 선언에 대한 정보를 가지고 있다.
  • 객체가 사용할 매개변수, 사용자가 정의한 변수 및 객체를 저장한다.

스코프 체인 (scope chain)

사진 출처 : poiemaweb
SC: 스코프 체인, AO: 활성 객체, GO: 전역 객체
일종의 리스트

  • 전역 객체와 중첩된 함수의 스코프의 레퍼런스를 차례대로 저장
  • 해당 전역 또는 함수가 참조할 수 있는 변수, 함수 선언 등의 정보를 담고 있는 전역 객체 또는 확성 객체의 리스트를 가르킨다.

즉, 내부함수가 외부함수의 변수에 접근할 수 있는 이유는 lexical scope의 레퍼런스를 차례대로 저장하고 있는 실행 컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색하기 때문에 가능한 일이다.

3️⃣ 자유 변수 (Free variable)

  • 외부 함수의 변수
  • 클로저에 의해 참조되는 외부 함수의 변수

☁️ 우선 쉽게 알기

1️⃣ lexical environment에 대한 참조 유지

function outer() {
  var name = 'movie';
  
  function inner() {
   	console.log(name); 
  }
  
  return inner();
} // outer() 함수에서 inner() 함수를 반환하고 있다. 
// 스코프의 lexical environment 유지 

var escapedInner = outer(); // return된 inner() 함수가 변수에 저장된다. 

escapedInner(); // [log] movie

escapedInnerouter()가 실행될 때 생성된 inner() 함수의 인스턴스에 대한 참조다.
inner()의 인스턴스는 변수 name이 있는 lexical environment에 대한 참조를 유지한다.
이런 이유로, escapedInner가 호출될 때 name은 사용할 수 있는 상태이다.

  • 몇몇 프로그래밍 언어에서는 함수 안의 지역변수들은 그 함수가 처리되는 동안에만 존재한다. (즉, outer()가 처리되는 동안에만 name이 존재한다.) 그래서 outer()의 실행이 끝나면 name 변수에 접근하지 못할 것이라고 예상하는 것이 일반적이다. 하지만 자바스크립트의 경우에는 다르다!
  • 자바스크립트에서는 함수를 리턴하고 리턴된 함수가 클로저를 형성하기 때문이다.

2️⃣ 서로 다른 맥락의 lexical environment

    function addFunctionFactory(x) {
      var y = 1;
      
      return function(z) {
        y = 100;
        
        return x + y + z; // x:? + y: 100 + z:?
      };
    }

    var add5 = addFunctionFactory(5); // x: 5
    var add10 = addFunctionFactory(10); // x: 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값에 접근하여 값을 계산

addFunctionFactory

  • addFunctionFactory은 함수를 만들어내는 공장이다.
  • addFunctionFactory은 인자를 가진 함수를 리턴한다.

add5add10

  • 둘은 모두 클로저이다.
  • 이들은 같은 함수 본문 정의를 공유하지만 서로 다른 맥락의 lexical environment를 저장한다.
  • 함수 실행시 add5의 lexical environment에서 클로저 내부의 x는 5지만, add10의 lexical environment에서 클로저 내부의 x값은 10이다.

☁️ 클로저를 사용하는 이유 & 활용

  • 클로저는 어떤 데이터(lexical environment)와 그 데이터를 조작하는 함수를 연관 시켜주기 때문에 유용하다. (데이터와, 메소드를 연관시킨다는 점에서 객체지향 프로그래밍과 같은 맥락에 있다.)
  • 상태를 유지한다. 현재 상태를 기억하고, 변경된 최신 상태를 유지한다.
  • 전역변수 사용을 억제한다.
    • 변수는 언제든지 변경될 수 있어 오류의 근본적 원인이 될 수 있다. 가변 데이터를 지양하고 불변성을 지향하는 함수형 프로그래밍에서 side effect를 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.
  • 정보를 은닉할 수 있다. (private 키워드 흉내내기)

☁️ 예시

예시 1 : 오직 하나의 메서드를 가지는 객체를 일반적으로 사용하는 모든 곳에 사용 가능

예시

    function makeSizer(size) {
      return function() {
        document.body.style.fontSize = size + 'px';
      };
    }

    var size12 = makeSizer(12);
    var size14 = makeSizer(14);
    var size16 = makeSizer(16);
  
    document.getElementById('size-12').onclick = size12;
    document.getElementById('size-14').onclick = size14;
    document.getElementById('size-16').onclick = size16;
예시 2 : private 메서드 흉내내기 (모듈 패턴)

예시

private method : 클래스 내부 다른 메서드에서만 해당 메서드를 호출할 수 있다. (자바스크립트에서는 태생적으로 이런 방법을 제공하지 않는다 🧐;)

      var counter = (function() {
      var privateCounter = 0; // 프라이빗 아이템 1️⃣
      function changeBy(val) { // 프라이빗 아이템 2️⃣
        privateCounter += val;
      }
  	  // 1️⃣과 2️⃣는 외부에서 접근될 수 없다. 
      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

private 메서드는

  • 코드에 제한적인 접근만을 허용한다.
  • 전역 네임 스페이스를 관리하는 강력한 방법을 제공해 불필요한 메서드가 공용 인터페이스를 혼란스럽게 하지 않는다.

위의 세가지 퍼블릭 함수는 같은 환경을 공유하는 클로저이다.
자바스크립트의 lexical scope 덕분에 세 함수는 프라이빗 아이템에 접근할 수 있다.

      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 */

위와 같이 코드를 작성하게 된다면 counter1counter2는 서로 독립성을 유지한다.
각 클로저는 그들 고유의 클로저를 통해 프라이빗 아이템의 다른 버전을 참조한다.
각 카운터가 호출될 때마다 하나의 클로저에서 변수값을 변경해도 다른 클로저에는 영향을 주지 않는다.

예시 3 : 현재 상태를 기억하고, 변경된 최신 상태를 유지한다.

예시

    var toggle = (function () {
      var isShow = false;

      // ① 클로저를 반환
      return function () {
        box.style.display = isShow ? 'block' : 'none';
        // ③ 상태 변경
        isShow = !isShow;
      };
    })();

    // ② 이벤트 프로퍼티에 클로저를 할당
    toggleBtn.onclick = toggle;

현재 상태를 기억하고 상태가 변경되어도 최신 상태를 유지해야하는 상황에 유용하다.
만약 클로저가 존재하지 않는다면 이를 위해서 전역 변수를 사용할 수 밖에 없다.
(기억하자 !: 전역 변수는 접근이 쉬워 많은 부작용을 유발하므로 사용을 억제해야한다.)

예시 4 : 전역변수 억제

예시

    // 1️⃣ : 지역변수를 사용할 경우 
    function increase() {
      // 카운트 상태를 유지하기 위한 지역 변수
      var counter = 0;
      return ++counter;
    }

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };
  
    // 2️⃣ : 클로저를 활용할 경우 
    var increase = (function () {
      // 카운트 상태를 유지하기 위한 자유 변수
      var counter = 0;
      // 클로저를 반환
      return function () {
        return ++counter;
      };
    }());

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };

1️⃣ 지역변수를 활용할 경우 전역변수를 사용하지 않기 때문에 의도치 않은 상태 변경은 방지한다.
하지만 increase() 함수가 호출될 때 지역변수가 초기화된다. (변경된 이전 상태를 기억하지 못한다.)

2️⃣ 클로저를 활용할 경우 전역변수를 사용하지 않기 때문에 의도치 않은 상태 변경은 방지하면서,
increase()가 호출될 때마다 counter가 초기화 되지 않기 때문에 이전 상태를 유지할 수 있다.



참고

profile
영화보관소는 영화관 😎

0개의 댓글