제육's 휘발성 코딩
article thumbnail
반응형

클로저 개요

클로저(Closure)는 여러 함수형 프로그래밍에서 등장하는 특성으로, 문헌 별로 다르게 정의되고 있습니다. 
MDN (Mozila Developer Network)에서는 클로저에 대해 "함수와 그 함수가 선언될 당시의 lexical environment의 상호 관계에 따른 현상"이라고 소개합니다. 

LexicalEnvironment는 자바스크립트에서 실행 컨텍스트를 생성 및 실행하는 과정에 사용되는 컴포넌트로, environmentRecord, outerEnvironmentReference, thisBinding 등의 작업을 통해 변수의 유효범위인 스코프가 결정되고, 스코프 체인이 가능해집니다. 

이 중 outerEnvironmentReference는 현재 컴포넌트에서 변수를 찾지 못하면 외부 환경에서 변수를 찾는 역할을 하는데, 예를 들어 컨텍스트 A에서 선언한 내부함수 B의 실행 컨텍스트가 활성화된 시점에는 B의 outerEnvironmentReference를 통해 참조하는 대상인 A의 LexicalEnvironment에도 접근이 가능해집니다. (A에서 B에 선언된 변수는 접근할 수 없다)

MDN에서 설명한 클로저의 상호 관계에 따른 현상은 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상 정도로 판단할 수 있습니다. 
 

클로저 이해하기 (외부함수 vs 내부함수)

외부함수에서 변수를 선언하고 내부함수에서 해당 변수를 참조하는 형태를 만들어 봅시다. 
 

let outer = function() {
    let a = 1;
    let inner = function() {
        return ++a;
    };
    return inner; // 함수의 실행 결과가 아닌 함수 반환
};

let outer2 = outer();
console.log(outer2());      // 2
console.log(outer2());      // 3

함수의 실행 결과가 아닌 함수를 반환하면, outer 함수의 실행 컨텍스트가 종료될 때 outer2 변수는 outer의 실행 결과인 inner 함수를 참조하게 됩니다.
 

콜스택 과정

전역 실행 컨텍스트가 생성되면서 environmentRecore 컴포넌트에는 outer와 outer2가 호이스팅 됩니다.

outer 함수 종료 후에 outer 실행 컨텍스트엔 environmenrRecord에 a의 값이 남아 있게 됩니다. 그 이유는 inner함수는 outer2를 통해 참조 중 (실행할 가능성 존재)이기 때문에 GC대상이 되지 않기 때문입니다.
따라서 outer 내부에 정의한 변수를 참조하는 내부 함수를 외부로 전달했을 때 outer의 실행 컨텍스트가 종료되어도 a가 사라지지 않게 됩니다.
 

클로저와 메모리 관리 

클로저는 필요에 의해 함수가 종료되더라도 지역변수를 사용할 수 있도록 만들어줍니다.

따라서 필요성이 사라진 시점에는 더는 메모리를 소모하지 않도록 GC의 수거 대상이 되도록 참조 카운트를 0으로 만들어줘야 합니다. 참조 카운트를 0으로 만들기 위해 null이나 undefined를 할당하면 됩니다. 
 

let outer = function() {
    let a = 1;
    let inner = function() {
        return ++a;
    };
    return inner; // 함수의 실행 결과가 아닌 함수 반환
};

let outer2 = outer();
console.log(outer2());      // 2
console.log(outer2());      // 3
outer = null;               // outer 식별자의 inner 함수 참조를 끊음

이전 코드에서 마지막에 outer = null을 선언하여 GC의 수거 대상이 되도록 만들 수 있습니다.
 

(function() {
    var a = 0; 
    var intervalId = null; 
    var inner = function() {
      if (++a >= 10) { 
        clearInterval(intervalId); 
        inner = null;       // inner 식별자의 함수 참조를 끊음
      }
      console.log(a);
    };
    intervalId = setInterval(inner, 1000);
  })();

setInterval은 외부 객체인 Window 객체의 메서드로 전달할 콜백 함수 내부에서 지역변수를 참조합니다. 지역 변수를 참조하는 내부 함수를 외부에 전달했기 때문에 return 없이도 클로저가 발생하며, inner 함수를 null로 선언하여 GC의 대상이  되도록 만들 수 있습니다.
 

클로저 예제 

대표적인 콜백 함수 중 하나인 이벤트 리스터를 통해 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때의 상황을 살펴볼게요. 

let fruits = ['apple', 'banana', 'peach'];
let $ul = document.createElement('ul');

fruits.forEach(function (fruit){
    let $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', function () {
        alert('your choice is ' + fruit);
    });
    $ul.appendChild($li);
});

document.body.appendChild($ul);

위의 코드에서 addEventListener에 넘겨준 콜백 함수는 fruit이라는 외부 변수를 참조하고 있으므로 클로저가 존재하고 있습니다.

addEventListener 함수가 콜백 함수에 국한되지 않는다면 반복을 줄이기 위해 외부로 분리하는 것이 좋습니다. 
 

let alertFruit = function (fruit) {
    alert('your choice is ' + fruit);
}
fruits.forEach(function (fruit){
    let $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruit.bind(null, fruit));
    $ul.appendChild($li);
});

alert 부분을 alertFruit 함수를 통해 외부로 분리해서 사용하는 방법입니다.

addEventListener 메서드에는 bind 메서드를 활용했습니다. 그렇지 않으면 이벤트 객체를 주입하여 li 클릭 시 [object MouseEvent] alert가 출력됩니다. 
 

let alertFruitBuilder = function (fruit) {
    return function() {
        alert('your choice is ' + fruit);
    }
}
fruits.forEach(function (fruit){
    let $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruitBuilder(fruit));
    $ul.appendChild($li);
});

bind 메서드를 사용하게 되면 첫 번째 인자에 null을 넣어 해결하였지만, 원래의 this를 유지하지 못하고 있습니다. 이런 변경사항이 발생하지 않게 구현하기 위해선 함수를 리턴하는 고차함수를 활용해야 합니다.
 

 

 

[JavaScript] - public, private 접근 제한자 (feat. 코어 자바스크립트)

자바스크립트에서 public, private ? 자바스크립트는 변수에 접근 제한자 (public, private, protected 등)을 직접 부여하도록 설계되어 있지 않고, 클로저를 이용하여 함수 차원에서 public한 값과 private한 값

sasca37.tistory.com

 

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^