코어 자바스크립트 - 05. 클로저

iamsummer__·2021년 1월 12일
0

1️⃣ 클로저란?

MDN에서는 클로저에 대해 "A closure is the combination of a function and the lexical environment within which that function was declared"
직역하면 클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호 관계에 따른 현상이다.
아직까지는 무슨 말인지 잘 이해가 되지 않는다.
예제를 통해 알아보도록 한다.

📚 외부함수의 변수를 참조하는 내부함수

const outer = funtion() {
	let a = 1;
    const inner = function() {
    	console.log(a++);
    }
    inner();
}

outer();

outer함수의 실행 컨텍스트가 종료되면 lexicalEnvironment에 저장된 식별자(a, inner)들에 대한 참조를 지운다.
그러면 각 주소에 저장돼 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 GC 수집 대상이 된다.

📚 외부함수의 변수를 참조하는 내부함수 2

const outer = funtion() {
	let a = 1;
    const inner = function() {
    	console.log(a++);
    }
    return inner();
}

const outer2 = outer();

inner 함수를 실행한 결과를 리턴하고 있으므로 결과적으로 outer함수의 실행 컨텍스틑가 종료된 시점에서는 a변수를 참조하는 대상이 없어집니다.

위의 두 예제는 outer함수의 실행 컨텍스트가 종료되기도 전에 inner함수의 실행 컨텍스트가 종료되어 있으며, 이후에 별도로 inner를 호출할수 없다.

⏰ 그러면 outer 실행 컨텍스트가 종료된 후에도 inner 함수를 호출할수 있게 하려면❓❗️

💡 외부함수의 변수를 참조하는 내부함수 3

const outer = funtion() {
	let a = 1;
    return function() {
    	console.log(a++);
    }
}

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

inner 함수 자체를 반환하였다.
스코프 체이닝에 따라 outer에 선언한 변수 a에 접근하여 1만큼 증가시킨다.

⏰ inner함수의 실행시점에서 outer 함수는 이미 실행이 종료된 상태인데 outer함수의 변수에 어떻게 접근을 할 수 있는가 ❓❗️

GC는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값을 수집대상에서 포함시키지 않는다.
즉, 외부함수인 outer의 실행이 종료되더라도 내부 함수인 inner는 언젠가 outer2를 실행하므로써 호출될 가능성이 열리게 된다.

어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상이다.

다시 클로져에 대해서 정의를 해보면

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

⚠️ 한가지 주의할 점은, 외부로 전달이 곧 return만을 의미하는 것은 아니다.

return없이도 클로저가 발생하는 경우는 setTimeout,setInterval, addEventListener 등 함수 내부에서 지역변수를 참조하는 경우에 발생한다.

(function() {
	let a = 1;
    const inner = function() {
    	a++;
    }
    
    setInterval(inner, 100);
})()


(function() {
	let count = 0;
	$button.addEventListener('click', function() {
    	count++;
    });
})()

2️⃣ 클로저 메모리 관리

클로저는 GC에서 제거가 되지 않는다고 했다.
그러므로 사용한 후에는 식별자에 참조형이 아닌 기본형 테이터 (null)을 할당하면 된다.

const outer = funtion() {
	let a = 1;
    return function() {
    	console.log(a++);
    }
}

const outer2 = outer();
outer2 = null;

3️⃣ 클로저 활용 사례

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

아래 예제는 클로저가 다양한 형태로 변경가능한 경우입니다.

1) 외부변수 참조하는 경우

let fruits = [ 'apple', 'banana', 'strawberry'];



fruits.map(fruit => {
  const li = document.createElement('div');
  li.addEventListener('click', function () {
    console.log(fruit);
  })
})

addEventListener에 넘겨준 콜백함수에서는 fruit라는 외부변수를 참조하고 있습니다.
콜백함수를 함수로 빼보겠습니다.

2) 콜백함수 공통으로 뺀 경우


function call(fruit) {
  console.log(fruit); // fruit: 이벤트 객체가 오게됨
}
fruits.map(fruit => {
  const li = document.createElement('div');
  //li.addEventListener('click', call(fruit));
  li.addEventListener('click', call.bind(null, fruits)); // bind 메서드를 통해서 넘어오는 인자 순서를 바꿔줌
})

call이라는 함수로 분리를 하였는데, 이때 bind메서드를 사용하지 않으면 addEventListener는 콜백함수 호출시 첫번째 인자에 '이벤트 객체'를 주입하기 때문에 문제가 발생할 수 있습니다.
bind를 사용하면 this도 달라지게 됩니다.
아래는 고차함수를 활용하여 보았습니다.

3) 고차함수 활용


function call(fruit) {
  return () => {
    console.log(fruit);
  }
}
fruits.map(fruit => {
  const li = document.createElement('div');
  li.addEventListener('click', call(fruit));
})

고차함수: 함수를 인자로 받거나 함수를 리턴하는 방식

고차함수는 주로 함수형 프로그래밍에서 자주 쓰이는 방식입니다.
call이라는 함수를 호출하면 익명함수를 반환하게 됩니다.
이 익명함수에서는 call이라는 외부함수에 있는 외부변수를 참조하고 있어 fruit값이 제대로 나오게 됩니다.

📌 접근 권한 제어(정보은닉)

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

let test = {
	a: 1,
    b: 2,
    addA: function() {
    	 a++;
    },
    addB: function() {
    	b++;
    }
}

test.a = 100;

이러한 코드는 객체에 직접 접근하여 변수를 바꿀수 있다.
그러므로 변수 은닉화 작업을 위해서는 함수로 만들며 필요한 멤버들만 return해야한다.

function test() {
	let a =  1,
    let b = 2;
    return {
    	 addA: function() {
    	 	a++;
   		 },
        addB: function() {
            b++;
        }
    }
}

test.addA();

이제 직접적으로 a 변수에 접근하여 값을 바꿀 수 없다.
그러나 addA, addB의 메서드에 접근하여 다른 내용으로 덮어씌우는 어뷰징은 발생할 수 있다.
이러한 경우에는 return하기 전에 미리 변경할 수 없게끔 Object.freeze를 사용해야
한다.

📌 부분 적용 함수

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

const add = function() {
	for (let i = 0; i < arguments.length; i++) {
    	console.log(arguments[i]);
    }
}


const addPartial = add.bind(null, 1,2,3);
addPartial(4,5,6); // 1,2,3,4,5,6

💻 실무에서 부분 함수를 적용하기 적합한 예로는 debounce가 있다.
디바운스는 짧은 시간동안에 동일한 이벤트가 많이 발생한 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해서 한번만 처리하는 것이다.

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

const movehandler = function(e) {
	console.log('move')
}

$body.addEventListener('click', debounce('move', movehandler, 500));

클로저로 처리되는 변수에는 eventName, func, wait, timeId가 있다.
최초 event가 발생하면 timeout의 대기열에 wait시간 뒤에 func을 실행할것이라는 내용이 담긴다. 그러나 wait시간 경과하기 전에 다시동일한 event가 발생하면 앞서 저장했던 대기열을 초기화 시키고 다시 대기열을 등록한다.
결국 각 이벤트가 바로 이전 이벤트로부터 wait시간 이내에 발생하는 한 마지막에 발생한 이벤트만이 초기화되지 않고 무사히 실행될 것이다.

📌 커링 함수

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

커링은 한번에 하나의 인자만 전달하는 것을 원칙으로 하며, 마지막 인자가 전달되기 전까지 원본함수가 실행되지 않는다.

const curry = function(func) {

	return function(a) {
    	return function(b) {
        	return func(a,b);
        }
    }
}

let getMax = curry(Math.max)(10);
console.log(getMax(8); // 10
console.log(getMax(20); // 20

필요한 인자 개수만큼 함수를 만들어 계속 리턴하다보며느 인자가 많아질 수록 가독성이 떨어진다.
ES6 화살표 함수를 써서 단 한줄로 표기 가능하다.

const curry = function() {
	return function(a) {
    	return function(b){ 
        	return function(c) {
            	return a+b+c;
            }
        }
    }
}


const curry2 = () => a => b => c => a+b+c

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

⏰ 커링 사용이 유용한 경우 ❓❗️

함수 프로그래밍에서 지연실행이라고 하는 원하는 시점까지 지연시켰다가 실행하는 상황에 요긴하게 사용될 수 있다.
예를 들어 함수의 매개변수가 항상 비슷하고 일부만 바뀌는 경우, 예를 들어보쟈

const getInfo = baseUrl => path => id => fetch(baseUrl + path + '/' + id);


// 이미지 타입별 요청 함수
const getImage = getInfo('https//');
const getIcon = getImage('icon');

// 제품 타입별 요청 함수
const getProduct = getInfo('https');
const getVegetable = getProduct('vegetable');

//실제 요청
const icon1 = getIcon(10);
const icon2 = getIcon(120);

const vegetable1 = getVegetable(550);
const vegetable2 = getVegetable(50);

http요청을 할때이다. 공통적인 요소는 먼저 기억해두고 특정값id만으로 서버요청을 수행하는 함수를 만들어보았다.
개발 효율성이나 가독성 측면에서 좋아졌다.

이러한 이유로 요즘 여러 프레임워크나 라이브러리에서 커링을 많이 사용하고 있다.
대표적인 예로 redux의 미들웨어 logger,thunk 가 있다.

// redux middleware logger
const logger = store => next => action => {
	console.log(action);
    console.log(store.getState());
}

// redux middleware thunk
const thunk = store => next => action => {
	return typeof action === 'function'
    ? action(dispatch, store.getState)
    : next(action);
}

위의 둘 미들웨어는 공통적으로 store, next, action 순서로 인자를 받는다.
이 중 store는 프로젝트 내에서 한번 생성된 이후로 바뀌지 않는 속성이고,
dispatch의 의미를 가지는 next역시 변하지 않지만, action의 경우 매번 달리진다.
그러므로 store,next값이 결정되면 redux내부에서 logger/thunk에 store,next를 미리 넘겨서 반환된 함수를 저장시켜 놓고, 이후에 action만 받아서 처리 가능하다.

📚 정리

📌 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행컨텍스트가 종료된 후에도 해당 변수는 사라지지 않는 현상이다.
📌 내부함수를 외부로 전달하는 방법에는 함수를 return하는 경우 외에도 콜백으로 전달하는 경우도 있다.
📌 클로저는 사용 후에 메모리를 차지하지 않도록 관리해줘야한다.

profile
개발하는 프론트엔드개발자

0개의 댓글