개발공부/자바스크립트

[JavaScript] 스터디 6일차_ 클로저 (1) 개념 및 메모리 관리

햄❤️ 2021. 6. 28. 19:14
728x90

 

1. 클로저의 의미 및 원리 이해

 

 MDN에서 정의하는 클로저는

 "클로저는 함수와 그 함수가 선언될 당시의 Lexical Environment의 상호관계에 따른 현상"

 

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

 

예제

 

 1) let 선언 변수는 호이스팅 되므로 렉시컬 환경에는 올라가지만 초기화는 되지 않는다.

 2) 함수선언은 바로 초기화됨

 3) let amy에서(1번째 줄) 할당이 안되어있기 때문에 undefined임

 4) amy = 29 로 할당됨

 5) plusAge(1)에서 새로운 렉시컬 환경이 생성된다. 현재 내부 렉시컬 환경은 외부 렉시컬 환경을 참조한다.
   (내부 렉시컬 환경에서 amy를 찾을 수 없어 전역 렉시컬 환경에서 찾는다)

  즉, 함수가 생성될 당시의 외부 변수(amy)를 기억하고, 생성 이후에도 계속 접근이 가능하다. 

 


 

책의 예제 코드를 조금 살펴보아야겠다..!

 

1-1. 외부 함수의 변수를 참조하는 내부 함수 - 클로저X

  - inner 함수 내부에서는 a를 선언하지 않았기 때문에 상위 컨텍스트인 outer의 LE에 접근해 a를 참조한다.

  - outer 함수의 실행 컨텍스트가 종료되면, LE에 저장된 식별자(a, inner)에 대한 참조를 지운다.
    참조카운트가 없으므로 GC(가비지컬렉터)의 수집 대상이 된다. 

var outer = function () {
	var a = 1;
    	var inner = function () {
    	console.log(++a);
    };
    inner();
};
outer();

// 2출력

 

1-2. 외부 함수의 변수를 참조하는 내부 함수 - 클로저X

  - return inner(); 에서 함수를 실행한 결과를 리턴하고 있음

  - outer 함수의 실행컨텍스트가 종료되면 a 변수를 참조하는 대상이 없어진다. 즉 inner,a는 GC의 대상이 된다

var outer = function () {
	var a = 1;
    	var inner = function () {
    		return ++a;
    };
    return inner ();
};
var outer2 = outer();
console.log(outer2);

// 2출력

 

1-3. 외부 함수의 변수를 참조하는 내부 함수 - 클로저O

 

  - return inner을 통해 함수 자체를 반환함. outer의 실행 컨텍스트가 종료될 때, 
    outer2의 변수는 outer의 실행 결과인 inner 함수를 참조하게 된다.

  - 즉, outer2를 호출하면 inner함수가 실행된다.

  - inner 함수는 outer함수 내부에서 선언되었으므로, outer 함수의 LE가 담긴다.
    스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근하여 1만큼 증가시킨 2를 반환하며 inner의 실행 컨텍스트가 종료된다.

  - outer2를 호출하면 같은 방식으로 a의 값이 1 증가되어 3이 출력된다. 

  - 🌟가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않기 때문에 outer함수가 종료되어도 outer함수는 inner함수의 변수 참조 때문에 GC의 대상이 되지 않는다.

var outer = function () {
	var a = 1;
    	var inner = function () {
    	return ++a;
    };
    return inner;
};
var outer2 = outer();

console.log(outer2());
// 2 출력

console.log(outer2());
// 3 출력

 

 

2. 클로저와 메모리 관리

 

2-1. 가비지 컬렉션의 메모리 생존 주기

 2-1-1 메모리 생존 주기 

  1) 필요할 때 메모리를 할당한다. - 자동

  2) 할당된 메모리를 사용한다.

  3) 더이상 필요하지 않으면 메모리를 해제한다.  - 자동

 

2-2. 가비지 컬렉션의 개념

 

 - 메모리를 비우는 release에 해당하는 과정으로, 가비지 컬렉팅이라고도 한다.
   JS의 경우 메모리 관리를 자동으로 수행하며, 이 부분이 GC의 역할임

 - 메모리 할당을 추적하고 할당된 메모리 블록이 더 이상 필요하지 않은지를 판단하여 회수하는 것.
   이를 판단하는 기준은 "참조"

 

2-3. 가비지 컬렉션의 2가지 전략

 

 1) Reference Counting  = 참조 카운팅

-> 어떤 다른 오브젝트에서도 참조하지 않는 오브젝트를 더 이상 필요하지 않은 메모리로 판단하여 메모리를 해제한다.

//두 객체는 서로를 순환참조하므로 둘 다 가비지컬렉터의 대상이 되지 않는다.
// 메모리 누수의 예시이다

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o는 o2를 참조한다.
  o2.a = o; // o2는 o를 참조한다.

  return "azerty";
}

f();

 

 -> 💎outer에 null을 할당함으로써 더 이상 참조하는 연결고리가 없어지므로 참조카운트가 0이 된다
      -> GC의 수거 대상이 된다
     보통, null이나 undefined 을 할당하여 참조 카운트를 0으로 만들어 메모리 누수를 방지한다. 

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 함수 참조를 끊음

//null

 

2) Mark-and-sweep = 표시하고 지우기 (마크 스위프)

   -> 더 이상 접근할 수 없는 오브젝트를 더 이상 필요하지 않은 메모리로 판단하여 메모리를 해제한다. 
       객체에 닿을 수 있는지(reachable)를 판단한다. 

   -> 가비지컬렉터에는 힙 외부에서 접근할 수 있는 변수나 오브젝트를 말하는 GC Root가 있다. GC Root에서 시작해 이 Root가 참조하는 모든 오브젝트, 그 오브젝트들이 참조하는 다른 오브젝트를 탐색해 내려가며 마크(Mark)한다.

   -> Mark가 끝나면 가비지 컬렉터는 힙 내부 전체를 돌면서 Mark 되지 않은 메모리들을 해제한다. 이 과정을 Sweep이라고 부른다. 

  -> "참조 받지 않는 객체"는 결국 닿을 수 없는 객체이기 때문에 Reference Counting 보다 효율적이고, 더 많이 사용된다. 

 

2-4. 메모리 누수를 막기 위해 클로저의 메모리 해제

  2-4-1. return 없이도 클로저가 발생하는 경우,
              클로저의 메모리를 해제하기 위해 콜백함수에 null을 할당해서 참조를 끊었다. 

(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);
})();

//1
//2
//3
//4
//5
//6
//7
//8
//9
//10 

 

2-4-2. eventListener에 의한 클로저의 메모리를 해제하기 위해,
            콜백함수(clickHandler)에 null을 할당해서 참조를 끊었다.

(function () {
var count = 0;
    var button = document.createElement('button');
    button.innerText = 'click';
    
    var clickHandler = function(){
    	console.log(++count,'times clicked');
        if(count >= 10){
        	button.removeEventListener('click', clickHandler);
            clickHandler = null; // clickHandler 식별자의 함수 참조를 끊음
        }
    };
    
    button.addEventListener('click', clickHandler);
    document.body.appendChild(button);
})();

 

 

❓클로저 관련 질문

#클로저를 사용할 때 주의해야 할 점?

더보기

클로저를 사용할 때에는 메모리 누수를 조심해야한다. 변수를 참조하는 클로저는 해당 변수값을 계속해서 유지하는 과정에서 메모리 누수가 발생할 수 있다. 메모리를 회수하기 위해 null 혹은 undefinde을 선언하여 사용하지 않는 변수/객체는 메모리 할당을 해제할 수 있다.

 

# 클로저란 무엇인가? 어떻게/왜 사용하나? 

더보기

클로저란 함수와 해당 함수가 선언된 렉시컬 환경의 조합이다. 외부 함수가 반환된 후에도 외부 함수의 변수 범위 체인에 접근할 수 있는 함수이다.

외부에서 직접 접근할 수 없는 private 변수에 접근하여 값을 조작하는 함수를 말한다.

 

728x90