Core js - 클로저

heyhey·2023년 11월 13일
0
post-thumbnail

1. 클로저의 의미 & 원리

클로저는 함수형 프로그래밍 언어에서 등장하는 특성입니다. JS 만의 특징은 아닙니다. 그래서 많은 정의들이 있는데 컨텍스트를 통해 알아보겠습니다.

A에서 선언한 내부함수 B의 실행 컨텍스트가 활성화되는 시점에는 B의 outerEnvironmentReference가 참조하는 대상인 A의 LexicalEnvironment에도 접근이 가능합니다.
=> B에서는 A에서 선언한 변수에 접근이 가능합니다.

여기서 내부 함수에서 외부변수를 참조하는 경우에 한해서만 combination이 됩니다. 즉 선언될 당시의 LexicalEnvironment 와의 상호관계가 의미 있습니다.

지금까지는 '어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상'이라고 요약이 됩니다.

예시를 통해 외부 함수의 변수를 참조하는 내부 함수를 확인하겠습니다.

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

Outer 함수에서 변수 a를 선언했고, inner (내부함수)에서는 a의 값을 증가 시키고 출력합니다. inner에서는 EnvRecord 에서 값을 찾지 못하기 때문에 상위 컨텍스트의 Outer의 LexicalEnv를 접근해 a를 찾습니다.

Outer 함수의 실행 컨텍스트가 종료되면 LexicalEnv에 저장된 식별자들 (a,inner)를 지우게 됩니다. => 가비지 콜렉터

식별자에게 변수명을 줘보도록 하겠습니다.

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

여기서는 Inner 함수를 실행한 결과를 리턴하고 있기 때문에 outer 함수의 실행 컨텍스트가 종료된 시점에서는 a 변수를 참조하는 대상이 없어집니다.
앞에서와 마찬가지로 inner 변수의 값들은 가비지 콜렉터로 소멸합니다.

이번에는 실행컨텍스트가 종료된 이후에서 Inner 함수를 호출할 수 있게 만들어 보겠습니다.

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 함수를 참조하게 될 것입니다.
inner 함수의 실행 컨텍스트의 envRecord 에는 수집할 정보가 없습니다. outerEnv 에는 inner 함수가 선언된 위치의 LexicalEnv가 참조복사 됩니다.
inner 함수는 Outer 함수 내부에서 선언됐기 때문에 outer 함수의 LexicalEnv가 담깁니다.

inner 함수의 실행 시점에는 outer 함수는 이미 실행이 종료된 상태인데 Outer 함수의 LexicalEnv에 어떻게 접근할까요?

가비지 컬렉터의 동작 방식 때문입니다. 어떤 값을 참조하는 변수가 있기 때문에 수집 대상이 되지 않습니다.

inner 함수의 실행컨텍스트가 활성화 되면 outerEnvReference가 outer 함수의 LexicalEnv를 필요로 할 것이기 때문에 수집되지 않습니다.

클로저는 어떤 함수에서 선언한 변수를 참조하는 내부 함수에서만 발생하는 현상입니다.
== 외부 함수에서 LexicalEnv가 가비지 컬렉팅 되지 않는 현상

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

return 없이

return 없이도 클로저가 발생할 수 있습니다. 지역변수를 참조하는 내부함수를 전달하면 사라지지 않습니다.
(addEventListener, setInterval ... )(window,global)

2. 클로저와 메모리 관리

클로저는 객체지향과 함수형을 모두 아우르는 중요한 개념입니다. 클로저의 특성상 메모리 소모는 불가피합니다. 이런 메모리 특성을 잘 이해하고 사용한다면 메모리 누수를 막을 수 있을 것입니다.

메모리 관리 방법은 생각보다 간단합니다.
클로저는 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생합니다.

필요성이 사라지면, 더는 메모리를 사용하지 않게끔만 설정해주면 됩니다.
=> 참조 카운트를 0으로 만들어주면 GC가 수거

어떻게 참조 카운트를 0으로 할 것인가 ?
식별자에 참조형이 아닌 기본형 데이터 (null & undefined) 를 할당하면 됩니다.

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

3 클로저 활용 사례

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

이벤트 리스너 의 예시가 대표적으로 사용합니다.
클로저의 '외부 데이터'에 주목하며 흐름을 따라가겠습니다.

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

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

li를 클릭하면 해당 리스너에 기억된 콜백 함수를 실행하게 하였습니다.

B에서 외부 변수를 참조하고 있으므로 클로저가 있습니다.

fruits 의 개수만큼 A가 실행될 것이고, 그 때마다 새로운 실행 컨텍스트가 활성화됩니다.
위의 함수가 실행될 때는 B의 outerEnv 가 A의 lexicalEnv 를 참조하게 됩니다.
따라서 B 함수가 참조할 예정인 변수 fruit 에 대해서는 A가 종료된 후에도 유지됩니다.

B 함수를 따로 분리해 정리하겠습니다.

var alertFruit = function(fruit){
	alert (fruit)
}
fruits.forEach(function(fruit){ // [A]
	var $li = document.createElement('li')
	$li.innerText = fruit;
	$li.addEventListener('click',alertFruit)	
	$ul.appendChild($li)
})

이렇게 작성하게 되면, li 를 클릭하게 될 시 [object Mouse Event] 라는 값이 출력됩니다.
콜백 함수의 인자에 대한 제어권이 addEventListener 가 가진 상태이며,
addEventListener 는 콜백 함수를 호출할 때 첫번째 인자에 '이벤트 객체'를 주입하기 때문입니다.

이 문제를 해결하기 위해 bind 메서드를 활용해보겠습니다.

$li.addEventListener('click',alertFruit.bind(null,fruit))

이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점과 함수 내부에서 this가 원래의 그것과 달라진다는 점이 문제로 생기게 됩니다.
함수형 프로그래밍에서 자주 사용하는 고차함수를 활용해 해결해 보겠습니다.

var alertFrutiBuilder = function(fruit){
	return function(){
		alert(fruit)
	}
}
...
$li.addEventListener('click',alertFruitBuilder(fruit))

alertFrutiBuilder 함수는 익명의 함수를 반환합니다. 이 함수의 실행 결과가 다시 함수가 되기 때문에 이렇게 반환된 함수를 리스너에 콜백 함수로 전달할 수 있습니다.

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

정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화하여 모듈간의 결합도를 낮추고 유연성을 높입니다. 보통의 언어는 private, public, protected 세가지의 종류가 있습니다.

js 는 기본적으로 변수 자체에 이런 접근 권한을 직접 부여하도록 설계가 되어 있지 않습니다. 이를 클로저를 이용해 값을 구별할 수 있습니다.

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

outer 함수를 종료할 때 inner 함수를 반환함으로써 outer 함수의 지역변수인 a의 값을 외부에서도 읽을 수 있게 되었습니다. 이처럼 클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 중 변수에 대한 접근 권한을 부여할 수 있습니다.

outer 함수는 외부로부터 격리된 공간입니다. 외부에서는 outer 변수를 통해 실행할 수는 있지만, 내부에 개입은 할 수 없습니다.

  • 내부에서 사용할 정보는 return 하지 않는다 => private
  • return 한 변수들은 공개 => public

클로저를 활용해 접근권한 제어하는 방법
1. 함수에서 지역변수 및 내부 함수 생성
2. 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터를 return
return 된 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 됩니다.

3-3. 부분 적용 함수

n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가 나중에 n-m 개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 알 수 있는 함수입니다.
this 를 binding 해야 하는 점을 제외하면 앞서 살펴본 bind 메서드의 실행 결과가 바로 부분 적용 함수입니다.

var debounce = function(eventTime,func,wait){
	var timeOutId = null;
	return function(event){
		var self = this;
		console.log(eventTime,'event!')
		clearTimeout(timeoutId)
		timeoutId = setTimeout(func.bind(self,event),wait)
	}
}
document.body.addEventListener('mousemove',debounce('move',()=>{},500))

커링 함수

커링 함수란 여러개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠 순차적으로 호출할 수 있게 체인 형태로 구성한 것입니다. 커링은 한번에 하나의 인자만 전달하는 것이 원칙입니다. 그리고 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않습니다.

var curry3 = function(func){
	return function(a){
		return function(b){
			return func(a,b)
		}
	}
}
const getMaxWith10 = curry3(Math.max)(10)
const getMinWith10 = curry3(Math.min)(10)
getMaxWith10(8) // 10
getMaxWith10(20) // 20

커링 함수는 필요한 상황에 직접 만들어 쓰기 용이합니다. 필요한 인자 개수만큼 함수를 만들어 계속 return 해주다가 마지막에 조합해서 리턴해주면 됩니다.

가독성이 떨어지기 때문에 화살표 함수로 적는다면 간단해집니다.

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

각 단계에서 받은 인자들을 모두 마지막 단계에서 참조할 것이므로 GC 되지 않고 메모리에 쌓이게 됩니다. 마지막 호출로 실행 컨텍스트가 종료된 후에야 비로서 GC의 수거 대상이 됩니다.

이 커링함수는 당장 필요한 정보를 받아서 전달하는 식으로 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 셈이 됩니다. 이를 '지연실행'이라고 합니다.
원하는 시점까지 지연시켰다가 실행하는 것이 커링을 쓰기에 적합한 경우입니다.
혹은 자주 쓰이는 함수의 매개변수가 항상 비슷하고 일부만 바뀌는 경우에도 적절하게 사용될 것입니다.
Redux에서도 위와 마찬가지로 미들웨어에서 커링을 사용한다고 합니다.

profile
주경야독

0개의 댓글