[코어 자바스크립트] 05 클로저

임승민·2022년 12월 27일
0
post-thumbnail

01 클로저의 의미 및 원리 이해

클로저는 JS고유 개념이 아닌 함수형 언어의 보편적 특성이다. 여러 문헌에서 클로저를 다르게 설명한다.

이 책에서 설명하는 클로저는 아래와 같다.

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

왜 그러한지 예시 코드를 통해 그 이유를 알아보자.

// 외부함수의 변수를 참조하는 내부함수(1)
var outer = function() {
	var a = 1;
	var inner = function() {
		console.log(++a)
	};
	inner();
};
outer(); //2

2를 출력하고 함수의 실행 컨텍스트가 종료되면 LE에 저장된 식별자들의 참조를 지운다.
그럼 값들은 참조하는 변수가 없어서 가비지 컬렉터의 수집 대상이 된다.

// 외부함수의 변수를 참조하는 내부함수(2)
var outer = function() {
	var a = 1;
	var inner = function() {
		return ++a;
	};
	return inner();
};
var outer2 = outer();
console.log(outer2); //2

이번 코드는 inner함수의 실행결과를 반환했다. 이는 outer함수 실행 컨텍스트가 종료되면 a변수를 참조하는 대상이 사라진다. (1)과 같은 결과다.

(1), (2)는 outer 실행 컨텍스트 종료 전 inner 실행 컨텍스트가 종료됐고 이후에 inner를 호출할 수 없다.

그럼 outer 실행 컨텍스트 종료 후 inner함수를 호출할 수 있게 해보자

// 외부함수의 변수를 참조하는 내부함수(3)
var outer = function() {
	var a = 1;
	var inner = function() {
		return ++a;
	};
	return inner;
};
var outer2 = outer();
console.log(outer2()); //2
console.log(outer2()); //3

이번엔 inner함수 자체를 반환했다. outer 실행 컨텍스트 종료 시 outer2 변수는 outer 실행 결과인 inner 함수를 참조한다. 따라서 outer2를 호출하면 inner가 실행된다.

가비지 컬렉터는 특정 값을 참조하는 변수가 있다면 그 값을 수집 대상에서 제외한다. 그래서 inner 실행 시 outer 실행 컨텍스트는 종료됐음에도 outer의 LE에 접근할 수 있다.

return 말고도 window의 메서드, DOM의 메서드도 외부로 전달이 가능하다.

// setInterval/ setTimeout
(function() {
	var a = 0;
	var intervalId = null;
	var inner = function() {
		if(++a>=10) clearInterval(intervalId)
		console.log(a);
	}
	intervalId = setInterval(inner, 1000);
})();
// eventListener
(function() {
	var a = 0;
	var button = document.creatElement('button');
	button.addEventListener('click', function(){
			console.log(++a);
	});
	document.body.appendChild(button);
})();

두 경우, 지역변수를 참조하는 내부함수를 외부로 전달했기에 클로저다.


02 클로저와 메모리 관리

  • 메모리 누수: 개발자 의도와 달리 특정 값의 참조카운트가 0이 되지않아 GC의 수거 대상이 되지않는 것
    • 하지만 반대로 개발자의 의도대로 참조카운트를 0이 되지않게 했다면 누수라고 할 수 없다.
  • 과거에는 의도치 않는 누수가 발생했다.(순환 참조, 인터넷 익스플로러의 이벤트 핸들러 등)
    하지만 대부분 최근 JS에서는 발생하지 않는다. 따라서 ‘메모리 소모’에 대한 관리법만 알면된다.

관리방법

  1. 클로저는 필요에 의해 의도적으로 함수의 지역변수, 메모리를 소모 함으로써 발생한다.
  2. 그럼 필요성이 사라진 이후에 메모리를 소모하지 않게한다.
  3. 참조 카운트를 0으로 하면 GC가 수거해 소모된 메모리가 회수된다.

참조 카운트를 0으로 만드는 방법

식별자에 참조형이 아닌 기본형 데이터를 할당한다. (보통 null, undefined)

// return에 의한 클로저의 메모리 해제
var outer = function() {
	var a = 1;
	var inner = function() {
		return ++a;
	};
	return inner;
};
var outer2 = outer();
console.log(outer2()); //2
console.log(outer2()); //3
outer2 = null;     // outer2 식별자의 inner함수 참조를 끊음
// setInterval에 의한 클로저의 메모리 해제
(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);
})();
// eventListener에 의한 클로저의 메모리 해제
(function() {
	var a = 0;
	var button = document.creatElement('button');
	
	var clickHandler = function(){
		console.log(++a);
		if(a>=10){
			button.removeEventListener('click', clickHandler);
			clickHandler = null; // clickHandler 식별자의 함수 참조를 끊음
		}
	};
	button.addEventListener('click', clickHandler);
	document.body.appendChild(button);
})();

03 클로저 활용 사례

5-3-1 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때

이벤트 리스너에 관한 예시이다.

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

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

document.body.appendChild($ul);

콜백함수 B에는 외부 변수(fruit)를 참조하고 있어 클로저가 있다.

B함수가 참조할 변수 fruit은 A가 종료된 후 GC대상에 제외되어 계속 참조가 가능할 것이다.

허나 B함수가 콜백함수에만 사용되지 않는다면 반복을 줄이기 위해 외부로 분리하는게 낫다.

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

document.body.appendChild($ul);
alertFruit(fruits[1]);

콜백 함수를 꺼내 alertFruit변수에 담으면 직접 실행할 수 있다. 마지막 줄에서 banana가 잘 출력된다.

하지만 click 시에는 [object MouseEvent]라는 값이 출력된다.

왜냐하면

  • 콜백 함수 인자의 제어권을 addEventListener가 가지고 있다.
  • addEventListener가 콜백 함수를 호출할 때 첫번째 인자로 event객체를 넘긴다.
    • 이는 bind를 이용하면 해결이 가능하다.
...
fruits.forEach(function (fruit) {
	var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruit.bind(null, fruit));
    $li.appendChild($li);
});
...

하지만 bind를 사용하면 몇가지 변경사항이 있다.

  • 이벤트 객체가 인자로 넘어오는 순서가 바뀐다.
  • 함수 내부의 this가 원래의 그것과 달라진다.

이러한 변경사항과 이슈를 해결하려면 고차함수를 이용해야 한다.

고차함수: 함수를 인자로 받거나 함수를 반환하는 함수

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

위처럼 고차함수를 이용하면 click시 함수의 실행 컨텍스트가 열리면서 인자로 넘어온 fruit을 outerEnvironmentReference에 의해 참조한다.
따라서 alertFruitBuilder함수에서 반환된 함수에는 클로저가 있다.

5-3-2 접근 권한 제어(정보 은닉)

💡 정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념중 하나이다.

접근권한 종류

  • public
  • private
  • protected

JS는 기본적으로 변수에 접근 권한을 부여할 수 없다.
하지만 return을 활용하면 외부 스코프에서 함수 내부 변수 중 일부 변수에 대한 접근 권한을 부여할 수 있다.

클로저를 이용한 접근권한 제어 방법

  • 함수에서 지역변수 및 내부함수 등을 생성한다.
  • 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터(대상이 여러개면 객체/배열, 하나면 함수)를 return한다.
    → return한 변수는 공개 맴버, 아닌 변수는 비공개 맴버

제어를 해도 메서드를 덮어씌우는 어뷰징이 가능하다. 따라서 return전 조치해야 한다.

var creatCar = function(){
...
	var publicMembers = {...};
...
	Object.freeze(publicMembers);
	return publicMembers;
};

Object.freeze() 메서드는 객체를 동결시켜 수정할 수 없다.

5-3-3 부분 적용 함수

💡 부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억 시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다.

bind를 활용한 부분 적용 함수

var add = function () {
	var result = 0;
	for (var i = 0l i < arguments.length; i++) {
		result += arguments[i];
	}
	return result;
};
var addPartial = add.bind(null, 1, 2, 3, 4, 5);
console.log(addPartial(5, 6, 7, 8, 9, 10));  // 55

이 함수는 this를 사용하지 않아 bind 만으로도 문제 없이 구현됐다. 하지만 this를 변경할 수 없어 메서드에서 사용할 수 없다.

부분 적용 함수 - 디바운스

디바운스는 짧은 시간 같은 이벤트가 다량 발생할 때 전부 처리하지 않고 처음 혹은 마지막에 발생한 이벤트만 처리한다.
성능 최적화에 도움을 주는 기능으로 scroll, wheel, mousemove, resize 등에 적용하기 좋다.

var debounce = (eventName, func, wait) => {
	var timeoutId = null;
  return function(event) {
    var self = this; 
     	console.log(eventName, 'event 발생');
     	clearTimeout(timeoutId);
     	timeoutId = setTimeout(func.bind(self, event), wait);
   };
};

var moveHandler = (e) => {
	console.log('move event 처리');
};
var wheelHandler = (e) => {
	console.log('wheel evnet 처리');
}

document.body.addEventListener('mousemove',debounce('move', moveHandler, 500));
document.body.addEventListener('mousewheel',debounce('wheel', moveHandler, 700));

이벤트가 이전 이벤트로부터 wait 시간 이내에 발생하는 한 마지막에 발생한 이벤트만이 초기화되지 않고 실행 될 것이다.

위 디바운스 함수에서 클로저로 처리되는 변수 eventName, func, wait, timeoutId

5-3-4 커링 함수

💡 커링함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말한다.

부분 적용 함수와의 차이점

  • 커링은 한번에 하나의 인자 전달만을 원칙으로 한다.
  • 마지막 인자가 올 때까지 원본 함수는 실행되지 않는다.
//커링 함수 예제
var curry3 = function (func){
	return function (a){
		return function (b){
			return func(a,b);
		};
	};
};
var getMaxWhith10 = curry3(Math.max)(10);
console.log(getMaxWhith10(8)); //10
console.log(getMaxWhith10(20)); //20

커링 함수는 필요한 상황에 만들어 쓰기 편하다.
인자 개수 만큼 함수를 만들어 계속 반환하다 마지막에 다 조합한 최종 결과를 반환하면 된다.

인자가 많을수록 가독성이 떨어진다. 하지만 ES6의 화살표 함수를 이용하면 한줄로 작성할 수 있다.

단순 간결화를 넘어 가독성도 향상된다.

var curry3 = func => a => b => func(a,b);

인자들은 쌓였다가 마지막 호출로 실행 컨텍스트가 종료되고 전부 GC의 수거 대상이 된다.

유용한 경우

💡 지연실행: 필요한 정보만 받아 전달하고 필요한 정보가 들어오면 전달하는 식으로 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 것.

커링 함수는 이 지연실행에 유용하다. 아래는 커링 함수가 적합한 경우이다.

  • 원하는 시점까지 지연하다 실행해야 하는 경우.
  • 프로젝트에서 자주 쓰이는 함수의 매개변수가 항상 비슷하고 일부만 바뀌는 경우.

0개의 댓글