[JS]클로저 활용예시 (부분적용함수, 커링함수)

3.3 부분 적용 함수

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

🤪뭔소리지...?
this를 바인딩해야 하는 점을 제외하면 bind메서드의 실행 결과가 바로 부분 적용 함수이다.this를 사용하지 않는다면 bind 메서드만으로 문제없이 구현되겠지만 this의 값을 변경(null)할 수 밖에 없기 때문에 메서드에서는 사용할 수 없다. bind와 다르게 this에 관여하지 않는 부분 적용 함수가 있다면 더 좋을 것이다.

  • 부분 적용 함수 구현1)
const partial = function(){
  const originalPartialArgs = arguments;
  const func = originalPartialArgs[0];
  if(typeof func !== 'function'){
     throw new Error('첫 번째 인자가 함수가 아닙니다.');
  }
  return function(){
  //외부함수의 areguments를 함수 빼고 두 번째 인자부터 배열로 만듬
    //첫 번째 인자 배열
    const partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
    //두 번째 인자 배열
    const restArgs =  Array.prototype.slice.call(arguments);//현재 함수의 arguments
    return func.apply(this, partialArgs.comcat(restArgs)); // 인자를 합쳐서 func 실행, 여러 개의 인자들을 하나의 배열로 보냄(apply) func에서는 배열 자체가 아니라 배열 속 원소들을 인자로 받음
  };
};

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

const addPartial = partial(add, 1,2,3,4,5);
console.log(addPartial(6,7,8,9,10));

const dog = {
	name: '멍멍이',
  	greet: partial(function(prefix, suffix) {
    	return prefix + this.name + suffix;
    }, '왈왈, ')
}

dog.greet('입니다!'); //왈왈, 강아지 입니다!

첫 번째 인자로 원본 함수, 두 번째 인자 이후부터는 미리 적용할 인자들을 전달하고, 반환할 함수(부분 적용 함수)에서는 다시 나머지 인자들을 받아 이들을 모아(concat) 원본 함수를 호출(apply)한다. 실행 시점의 this를 그대로 반영함으로써 this에는 아무런 영향을 주지 않게 된다.

다만 반드시 인자를 앞에서부터 차례로 전달할 수 밖에 없다. 인자들을 원하는 위치에 미리 넣어놓고 나중에 빈 자리에 인자를 채워넣어 실행할 수 있도록 코드를 수정해보자.

  • 부분 적용 함수 구현 2)
Object.defineProperty(window, '_', {window 객체에 _속성을 만들어 값을 넣고, 속성 
 	value: 'EMPTY_SPACE',
                                    writable: false,
                                    configurable: false,
                                    enumerable: false
                                   });

const partial12 = functino(){
  const originalPartialArgs = arguments;
  const func = originalPartialArgs[0];
  if(typeof func !== 'function'){
     throw new Error('첫 번째 인자가 함수가 아닙니다.');
  }
  return function(){
    const partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
    const restArgs =  Array.prototype.slice.call(arguments);
    //📍추가된 부분, 비어있으면(_) restArgs 인자들 차례대로 끼워 넣기
    for (var i = 0; i < partialArgs.length; i++){
      	if(partialArgs[i] === _) {
          partialArgs[i] = restArgs.shift();
        }
    }//여기까지 추가된 부분
    return func.apply(this, partialArgs.concat(restArgs));
  };
};

const addPartial2(add, 1,2,_,4,5,_,_,8,9);
console.log(addPartial(3,6,7,10)); //55

'비워놓음'을 표시하기 위해 미리 전역객체에 ''라는 프로퍼티를 준비하면서 삭제 변경 등의 접근에 대한 방어 차원에서 여러 가지 프로퍼티 속성을 설정했다. 추가된 부분을 보면 예시(1)과 차이가 있다. 처음에 넘겨준 인자들 중 ''로 비워놓은 공간마다 나중에 넘어온 인자들이 차례대로 끼워 들어가도록 구현했다.

  • 부분 적용 함수3) 디바운스
    디바운스는 짧은 시간 동안 동일한 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것으로, 프론트엔드 성능 최적화에 큰 도움을 주는 기능 중 하나이다. scroll, wheel, mousemove, resize 등에 적용하기 좋다.

최소한의 기능으로 구현해본다면,

const debounce = function(eventName, func, wait){
  let timeoutId = null;
  
  return function (event) {
    const self = this;//이벤트 발생한 DOM요소(body)
    console.log(eventName, 'event 발생');
    clearTimeout(timeoutId);
    💡timeoutId = setTimeout(func.bind(self, event), wait);//bind해주지 않으면, setTimeout의 객체인 window가 this로 바인딩 된다.
  };
};

const moveHandler = function (e) {
  console.log('move event 처리');
  console.dir(e); //MouseEvent 객체
};

const wheelHandler = function (e) {
  console.log('wheel event 처리');

//이벤트가 발생하면 debounce 함수가 실행되고, 내부함수를 리턴, 반환된 내부함수가 이벤트 핸들러가 된다. 
document.body.addEventListener('mouse', debounce('move', moveHandler, 500));
document.body.addEventListener('mouse', debounce('move', moveHandler, 700));  

최초 이벤트가 발생하면 💡표시 코드에 의해 timeout의 대기열에 'wait 시간 뒤에 func를 실행할 것'이라는 내용이 담긴다. 그런데 wait 시간이 경과하기 이전에 다시 동일한 event가 발생하면 이번에는 7번째 줄에 의해 앞서 저장했던 대기열을 초기화하고, 다시 8번째 줄에서 새로운 대기열을 등록한다. 결국 각 이벤트가 바로 이전 이벤트로부터 wait 시간 이내에 발생하는 한 마지막에 발생한 이벤트만이 초기화되지 않고 무사히 실행될 것이다.

콘솔

이렇게 실제 이벤트 발생(wait시간 내)은 여러번이지만 이벤트 발생에 따른 콜백함수 실행은 한 번씩 된다.

3.4 커링함수(currying function)

커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출 될 수 있게 체인 형태로 구성한 것을 말한다. 부분 적용 함수와 기본적인 맥락은 일치하지만 몇 가지 차이가 있다.

  • 부분적용함수
    -여러 개의 인자를 전달 가능
    -실행결과 재 실행시, 원본 함수 무조건 실행
  • 커링함수
    -한 번에 하나의 인자만 전달하는 것이 원칙
    -중간 과정상의 함수를 실행한 결과는 그 다음 인자를 받기 위해 대기
    -마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않음

부분 적용 함수와 달리 커링 함수는 필요한 상황에 직접 만들어 쓰기 용이하다. 필요한 인자 개수만큼 함수를 만들어 계속 리턴해주다가 마지막에 짠! 하고 조합해서 리턴해주기 때문이다.

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

  • 커링함수1)
const curry3 = function(func) {
  return function(a) {
    return function(b){
      return func(a,b);
    }
  }
};

const getMaxWith10 = curry3(Math.max)(10);
console.log(getMaxWith10(8)); //10
console.log(getMaxWith10(25)); //25

const getMinWith10 = curry3(Math.min)(10);
onsole.log(getMinWith10(8)); //8
console.log(getMinWith10(25)); //10

getMaxWith10 는 function(b) { return Math.max(10, b) } 과 같다.

10과 비교해서 큰 수를 찾는 것을 여러 번 해야 한다면 커링함수를 이용해서 getMaxWith10으로 간단하게 마지막 인수 하나만 추가해 원하는 결과를 얻을 수 있다.

  • 커링함수2)
const curry5 = function(func) {
  return function(a) {
    return function(b) {
      return function(c) {
   		return function(d) {
      	  return function(e) {
      return func(a,b,c,d,e);
 	   		};
  		};
 	};
  };
};
};

const getMax = curry5(Math.max);
console.log(getMax(1)(11)(7)(3)(5));//11

//ES6 화살표함수
const curry5_ = func => a => b => c => d =>e => func(a,b,c,d,e);

인자가 많아질수록 가독성이 떨어진다는 단점이 있다. ES6의 화살표 함수(curry5_)를 사용하면 한 줄에 표기할 수 있다. 그리고 커링 함수를 이해하기 훨씬 수월하다. 화살표 순서에 따라 함수에 값을 차례로 넘겨주면 마지막에 func가 호출될 거라는 흐름이 한눈에 파악된다.

이 커링 함수가 유용한 경우가 있다. 당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 하면 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 셈이 된다. 이를 함수형 프로그래밍에서는 지연실행(lazy execution)이라고 칭한다. 원하는 시점까지 지연시켰다가 실행시키는 것이 요긴한 상황이라면 커링을 쓰기에 적합하다. 혹은 프로젝트 내에서 자주 쓰이는 함수의 매개변수가 항상 비슷하고 일부만 바뀌는 경우에도 유용하다.

  • 커링함수3)
//커링함수
const getInformation = baseUrl => path => id => fetch(baseUrl + path + '/' +id);

//url 전달
const ImgUrl = 'http://imageAddress.com/';
const getImg = getInformation(ImgUrl);

//path전달
const getEmoticon = getImg('emoticon');
const getIcon = getImg('icon');

//실제요청 (마지막 id만전달) -> 원본함수실행
const emoticon1 = getEmoticon(100);
const emoticon2 = getEmoticon(102);
const icon1 = getIcon(205);
const icon2 = getIcon(234);
const icon3 = getIcon(265);

fetch 함수는 url을 받아 해당 url에 HTTP 요청을 한다. 보통 REST API를 이용할 경우 baseUrl은 몇 개로 고정되지만 path나 id값은 매우 많을 수 있다. 이런 상황에서 서버에 정보를 요청할 필요가 있을 때마다 매번 baseUrl부터 전부 기입해주기보다는 공통적인 요소는 먼저 기억시켜두고 특정한 값(id)만으로 서버 요청을 수행하는 함수를 만들어두는 편이 개발 효율성이나 가독성 측면에서 더 좋을 것이다.

최근의 여러 프레임워크나 라이브러리 등에서 커링을 광범위하게 사용하고 있다. Flux 아키텍처의 구현체 중 하나인 Redux의 미들웨어를 예로 들면 다음과 같다.

  • Redux의 미들웨어의 커링 함수
const loger = store => next => action => {
  console.log('dispatching', action);
  console.log('next state', store.getState());
  return next(action);
}

//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 값이 결정되면 logger, thunk에 store, next를 미리 넘겨서 반환된 함수를 저장시켜놓고, 이후에 action만 받아서 처리할 수 있게끔 한 것이다.

🎈면접질문

Q1. 커링으로 동작하는 코드를 간단하게 보여주세요 : )

[참고한자료]
정재남, 『코어자바스크립트』, 위키북스(2019)
https://codingsalon.tistory.com/27?category=900712 [코딩쌀롱]

좋은 웹페이지 즐겨찾기