[JS] 클로저와 클로저의 활용

Juno·2020년 11월 18일
5
post-thumbnail

본 포스팅은 자바스크립트 스터디 세미나를 위해 포스팅 하였고,
"코어 자바스크립트" 책을 참고하여 작성되었습니다😊

🔖 들어가기

클로저는 쓰고 안쓰고의 개념이 아닌, 관계 또는 현상(특성) 를 이야기 합니다.

for(var i = 0; i < 100; i++){
  setTimeout(function(){
    console.log(i);
  }, i * 1000);
}

결과를 예상해 보셨나요?

저는 i 가 돌면서 콘솔에 0 부터 99 까지 1초 간격으로 반복되어 출력될 것으로 예상하였습니다.
하지만 생각과는 좀 다르게,
클로저 결과
다음과 같이 동작하게 됩니다. 왜 그럴까요?

 먼저, 자바스크립트는 다음과 같이 동작을 하게 될 것입니다. for문 이 먼저 돌고 그 이후에 콜백함수가 실행 됩니다.

자바스크립트 엔진에서 for문이 먼저 돌고난 후 비동기 콜백함수가 실행되기 때문입니다.

setTimeout(function(){
  console.log(i);
}, 0 * 1000);

setTimeout(function(){
  console.log(i);
}, 1 * 1000);

setTimeout(function(){
  console.log(i);
}, 2 * 1000);

setTimeout(function(){
  console.log(i);
}, 3 * 1000);

// ...

setTimeout(function(){
  console.log(i);
}, 99 * 1000);

이때, 함수 안의 변수 i는 콜백함수가 실행 될때 값이 결정됩니다.( = 실행될 때 값을 찾습니다.)
다음과 같이 반복문이 먼저 돌아서 100개의 setTimeout 함수가 찍히고, i는 100이 되어 있는 상태입니다.
따라서 콘솔에는 1초간격으로 100이 찍히게 되는 것입니다.

이는 클로저 관계로 설계한다면, 다음과 같이 비동기 함수반복문의 설계에서 의도한 대로 동작하게 할 수 있습니다.

for(var i = 0; i < 100; i++){
    function closure(args) {
      setTimeout(function() {
        console.log(args);
      }, i * 1000);
    }
  closure(i);
}

closure
혹시 이해가 되셨나요? 클로저의 정의를 다시 한번 살펴보겠습니다.

클로저란, 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상입니다.

for문 에서 선언한 iclosure 함수가 매개변수로 받았고 콜백함수 이므로 for 문의 실행이 끝난 후에도 closure 함수는 전달받은 i 를 버리지 않고 가지고 있습니다.

function closure(args) {
  setTimeout(function() {
    console.log(args);
  }, 0 * 1000);
 }
closure(0);

function closure(args) {
  setTimeout(function() {
    console.log(args);
  }, 1 * 1000);
 }
closure(1);

function closure(args) {
  setTimeout(function() {
    console.log(args);
  }, 2 * 1000);
 }
closure(2);

...

function closure(args) {
  setTimeout(function() {
    console.log(args);
  }, 99 * 1000);
 }
closure(99);

for문 의 실행 이후를 풀어쓴다면, 다음과 같이 나열할 수 있겠습니다.
이때, 각각의 콜백 함수들은 실행되는 시점의 i 값에 의해 실행된다고 했으니 외부 함수의 실행 컨텍스트는 종료가 되더라도 i 값은 참조복사하여 가지고 있기 떄문에 각각의 i 값으로 실행되어 의도하는 대로 코드를 구현할 수 있습니다.

클로저 활용 사례

  1. 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때
  2. 접근 권한 제어(정보 은닉)
  3. 부분 적용 함수
  4. 커링 함수

하나하나의 코드를 이해하려고 하기 보단, 클로저 가 "이러한 경우들에서 사용되고 있다" 라고만 이해하고 넘어가면 좋겠습니다👍

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

이와 같은 형태의 함수에서 (A)에서는 외부 변수를 사용하고 있으므로 클로저가 없습니다.
하지만, li 태그의 이벤트 리스너로 등록된 콜백함수 (B)는 외부변수인 fruit을 참조하고 있으므로 클로저가 있습니다.

(A)가 forEach 에 의해서 fruits의 원소의 갯수만큼 실행되며 그때마다 새로운 실행 컨텍스트가 활성화 될 것 입니다. 이때 외부변수 fruit을 넘겨 받은 (B)는 (A)의 실행 컨텍스트가 종료 되었더라도 fruit의 값을 기억하고 있다가 콜백함수가 실행될 떄 참조하여 실행됩니다.

클로저 때문에 내부함수에서 사용되는 fruit은 (A)의 종료시에도 (실행 컨텍스트에서 제외되어도) 가비지 컬렉터의 수거 되상이 되지 않아 계속 참조 가능하게 됩니다.

👏  React 에서는 이렇게 사용해요!

const alertFruit = (fruit){
  alert(`your choice is ${fruit}`);
}
...
fruits.forEach((fruit)=>{
	<button onClick={()=>alertFruit(fruit)}>버튼</button>
})

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

정보 은닉 이란 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화 하는 것

 우리가 이미 알고 있는 public  private  protected 이 접근 권한으로 사용되어 정보의 은닉 여부를 결정할 수 있습니다.
자바스크립트는 C++ 에서와 달리 기본적으로 변수 자체에 접근 권한을 직접 부여할 순 없지만,
클로저를 이용하면 함수 차원에서 publicprivate을 구분할 수 있습니다.

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

다음과 같은 예시에서, outer 함수를 종료할 때 inner 함수를 return 함으로써 outer 함수의 지역변수인 a의 값을
외부에서도 읽을 수 있게 됐습니다.

❓ 왜 그럴까요?

outer가 실행 컨텍스트에서 사라지고 원랜 a가 가비지 컬렉텨의 수집 대상이 되어야 하지만 inner함수를 return 하였기 때문에
inner 함수는 a 를 참조복사 하여 가지고 있기 때문입니다.

outer 함수는 외부(전역 스코프)로부터 철저하게 격리된 닫힌 공간입니다.
외부에서는 외부 공간에 노출돼 있는 outer 라는 변수를 통해 outer 함수를 실행할 수 있지만,
outer 함수 내부에는 어떠한 개입도 할 수 없습니다.
따라서, 외부에서는 오직 outer 함수가 return한 정보에만 접근할 수 있습니다.

외부에 제공하고자 하는 정보들을 모아서 return 하고,
내부에서만 사용할 정보들은 return 하지 않는 것으로 접근 권한 제어가 가능합니다.
(return한 변수 : public, 그렇지 않은 변수 : private)

3. 부분 적용 함수

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

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

이와 같이 m개의 인자들만 넘겼다가, 나중에 n-m 개의 인자를 넘기면 원래의 함수 실행 결과를 얻을 수 있도록 하는 함수
(return result를 해주었기 때문에 result가 arguments[i] 를 기억하고 있고, 다시 addPartial을 실행하여 나머지 결과를 얻는다.

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,500));

eventName , timeoutId , func , wait 이 클로저로 처리되었습니다.

미리 일부 인자들을 넘겨두어 기억하게 하고, 추후 필요한 시점에 기억했던 인자들까지 함께 실행합니다. (클로저의 개념)

4. 커링 함수

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

var curry = func => a => b => c => d => e => func(a, b, c, d, e); 

화살표 순서에 따라 함수에 값을 차례로 넘겨주면 마지막에 func가 실행됩니다.

이 커링 함수는 지연 실행 에 주로 사용됩니다!
마지막 인자가 들어오기 전까지 지연시켰다가 실행하는 것이 요긴한 상황일 경우에 사용됩니다.

가장 대표적인 예가 바로 미들웨어 입니다.

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

storenext 의 값이 결정되면, 내부에서 미리 넘겨주어 반환된 함수를 저장시켜놓고, action 에 따라 실행을 달리 하기 위해 action이 들어오기 전 까지 지연시켜 이후엔 action만 받아서 처리할 수 있도록 구현합니다.

정리

클로저란, 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상입니다.

이때, 내부함수를 외부함수로 전달하는 방법에는 함수를 return 하는 경우뿐 아니라 콜백으로 전달하는 경우도 포함됩니다.

클로저는 그 본질이 메모리를 계속 차지하는 개념이므로 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 않도록 관리해줄 필요가 있습니다.

👉 (참고) React.useState()에서 클로저 사용

const useState = (initialVal) => {
  let innerState = initialVal;
  const state = () => innerState;
  const setState = (newVal) => {
    innerState = newVal;
  }
  return [state, setState];
}
profile
사실은 내가 보려고 기록한 것 😆

2개의 댓글

comment-user-thumbnail
2021년 2월 1일

오늘도 글이 머리에 쏙 쏙 들어오네요^^

1개의 답글