[JS] Closure 클로저 이해

colki·2021년 4월 11일
0

클로저의 정의를 보기 전에 예제를 먼저 보는 편이 이해하기 수월할 것이다.
다음 예제에서 콘솔에는 어떤 값이 출력될까?

function add_maker(a) {
  return function add(b) {
    return a + b;
  };
}

const addThree = add_maker(3); 
const addLucky = addThree(7); 

console.log(addLucky);

🙋‍♂️ Tip 함수를 실행(호출)하면 함수가 생성된다.

실행문 1개당 함수도 1개씩.
Reference는 주소값을 저장해두고 실행될때 주소에 찾아가서 값을 할당(생성)한다.

1) add_maker(3); 을 실행하게 되면 add_maker가 생성되고 실행컨텍스트가 열린다.

2) 이 함수의 매개변수로 받은 3이 함수내에서 변수로 선언된다. (생성)

function add_maker(a) {

  a = 3;
  
  return function add(b) {
    return a + b;
  };
}

3) return function add(b) {};

return문은 새로 만들어서 뱉는 것이기 때문에 여기서도
여기에서 add 함수를 생성해서 리턴하고 있는거다.
그리고 변수 addThree에 할당한다.

4) 이렇게 add함수가 생겨나면서 선언된다. 그리고 add함수는 자신의 주변을 쓱 둘러본다.

나를 둘러싸고 있는 add_maker함수 상위스코프가 있네~
어 내가 가지고 있는 a를 얘가 가지고 있네? a = 3 이구나~

5) addThree(7);실행

함수는 a + b를 연산하기 위해 a와 b를 찾는다.

아 맞다 아까 a = 3이었지! 하면서 a를 줍고,
아 b =7도 생성됐지 여깄네~ 하면서 b도 줍는다.

function add_maker(a) {

  a = 3;
  
  return function add(b) {
    b = 7; 
    return a + b;
  };
}

그렇게 해서 더한 값을 addLucky 변수에 할당한다.
콘솔로 출력하면 10이 나온다.

Closure

어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 앞서 선언했던 a가 사라지지 않는 현상을 말한다.

(사라지지 않는다 = 가비지컬렉터가 수거하지 않는다. *가비지컬렉터: 메모리 청소부)

즉 이미 생명주기가 끝난 상위 컨텍스트의 함수A의 변수에 접근하여 값을 참조하는 것이다.


자바스크립트의 모든 함수는 실행, 선언, 생성되는 순간 자신의 주변 환경을 기억하고
그 정보들은 실행컨텍스트에 담긴다. 이때 수집하는 정보는 아래와 같다.

💡 실행컨텍스트에서 수집하는 정보

  • 자신의 상위스코프 (Lexical Environment)
  • 변수 정보 (호이스팅)
    ㅇ 일반 변수
    ㅇ 매개 변수
    ㅇ 함수 선언문
    ㅇ 선언되어 생성된 함수의 정보
  • this

혹시 자신이 참조해야 할 변수 등이 주변 어디에, 무슨 값으로 있는 지 알아야 사용할 수 있기 때문에 함수를 부르는 위치(실행 위치)는 전혀 중요하지 않게 된다.

🙋‍♀️ Tip 함수는 선언되는 순간 자신의 주변환경을 기억한다 .

함수는 선언된 위치를 기준으로 환경을 기준으로 기억하는거라, 함수선언후 어디에서 언제 실행하는지는 전혀 관계 없다.

또한 자신이 참조해야할 변수가 있다면 변수값의 변화가 있는지 계속 주시한다.
(처음 한 번 하고 끝내는 것이 아니라 지속적으로 모니터링한다.)

function snack () {
  let candyCount = 0;

  function candy () {  // candy는 자신이 참조할 count의 변화를 계속 모니터링한다.
    candyCount++;
    console.log(candyCount);
  }

  return candy;
}

const fat = snack();

fat(); // 1
fat(); // 2
fat(); // 3

1) 변수 fat에는 snack함수의 리턴값 candy, 결국 함수 candy의 주소값이 담긴다.

2) fat(); 함수 실행문이 3개이므로 함수는 3개가 생성된다.

3) 함수 candy가 생성, 선언되었을 때 자신의 상위스코프에 있는 candyCount를 스캔했기 때문에 실행컨텍스트에서 candyCount++ 를 에러없이 수행하고 let candyCount = 1로 재할당하고 콘솔에 1을 출력한다.
그리고 이 값을 똑똑히 기억하고 있는다.

4) 이어서 fat();가 또 실행되면 let candyCount = 1 의 값을 참조해서 함수 candy내부에서 또 1을 더해서 콘솔에는 2가 출력된다. 이렇게 함수가 재차 실행되더라도 candy함수가 주변환경을 계속 인지하고 있기 때문에 candyCount의 값은 증가한다.

이렇게 candyCount 변수는 외부에서는 접근할 수 없는 private한 변수가 된다.


메모리 누수

앞에서 살펴본대로 지속적은 모니터링으로 가비지컬렉팅 당하지 않기 때문에(메모리청소부가 안 치우고 지나감), 클로저는 성능, 메모리 누수 관련 이슈를 가지고 있다.
하지만, 메모리 소모는 클로저의 본질적인 특성이기에 이해하고 활용해야지 지양해야 할 부분이 아니다. 메모리 관리만 잘 해주면 소모됐던 메모리들을 회수할 수 있다.

방법은 간단하다.

💡 클로저가 더 이상 필요하지 않을때
null, undefined 를 할당해서 참조를 끊어주면 된다.

다음은 함수가 종료되는 3가지 방법에서 각각 참조를 끊는 방법을 나타낸 것이다.

  • return에 의한 클로저
  
  var outer = (function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  
  return inner;
})();

console.log(outer()) // 2
console.log(outer()) // 3
  
outer = null; // * outer식별자의 inner 함수의 참조 해제 *
  
console.log(outer()) // Type Error
  • setIntereval (feat. 즉시실행함수) 에 의한 클로저

    참조: 콜백함수 내부에서 지역변수를 참조.
    해제: inner = null inner식별자의 함수 참조를 해제

(function () {
  var a = 0;
  var intervalid = null;
  
  var inner = function () {
    if (++a >= 5) {
      clearInterval(intervalid);
      inner = null; // * inner식별자의 함수 참조를 해제 *
    }
    
    console.log(a);
  }
  
  intervalid = setInterval(inner, 1000); 
})();
  • addEventListener (feat. 즉시실행함수) 에 의한 클로저

    참조: Dom 메서드 addEventListener에 등록할 handler 함수 내부의 지역변수 참조.
    해제: clickHandler = null 식별자의 함수 참조 해제.

  (function () {
  var count = 0;
  var button = document.createElement('button');
  
  button.textContent = 'click';
  
  var clickHandler= function () {
    console.log( ++count, 'times clicked');
    
    if (count <= 5) {
      button.removeEventListener('click', clickHandler);
      clickHandler = null; // *  clickHandler 식별자의 함수 참조 해제 *
    }
  }
  
  button.addEventListener('click', clickHandler);
  document.body.append(button);
})();
profile
매일 성장하는 프론트엔드 개발자

0개의 댓글