Currying

커링(Currying)은 함수형 프로그래밍에서 유래한 중요한 개념으로, 여러 개의 인자를 받는 함수를 단일 인자를 받는 일련의 함수들로 변환하는 기법이다.
이 기법은 수학자이자 논리학자인 하스켈 커리(Haskell Curry)의 이름을 따서 명명되었다.
커링은 자바스크립트의 함수형 프로그래밍 패러다임에서 특히 유용하며, 함수 합성과 부분 적용을 가능하게 하는 강력한 도구이다.

커링은 자바스크립트에서 함수형 프로그래밍을 구현하는 데 중요한 기법 중 하나이다.
이 기법은 코드의 재사용성과 모듈성을 높이고, 함수 조합을 용이하게 하며, 복잡한 로직을 더 작고 관리하기 쉬운 단위로 분해하는 데 도움이 된다.

커링의 주요 이점:

  1. 함수 재사용성 증가: 기본 함수로부터 특화된 함수를 쉽게 생성
  2. 코드 가독성 향상: 복잡한 로직을 더 작고 명확한 단계로 분해
  3. 함수 조합 용이: 파이프라인과 체이닝 패턴을 더 쉽게 구현
  4. 지연 평가: 모든 인자가 제공될 때까지 함수 실행을 지연

반면, 커링은 처음 접하는 개발자에게는 익숙하지 않을 수 있으며, 디버깅이 복잡해질 수 있고, 코드 흐름을 추적하기 어려울 수 있다는 단점도 있다.

커링은 단순히 이론적인 개념이 아니라 실제 애플리케이션 개발에서 유용하게 활용될 수 있는 실용적인 패턴이다.
함수형 프로그래밍의 개념을 이해하고 적용하면, 코드의 품질과 유지보수성을 크게 향상시킬 수 있다.

자바스크립트의 유연한 특성 덕분에 커링은 다양한 방식으로 구현하고 활용할 수 있으며, Lodash나 Ramda와 같은 라이브러리를 통해 더 쉽게 접근할 수도 있다.

커링의 기본 개념

커링은 여러 매개변수를 받는 함수를 단일 매개변수를 받는 함수들의 체인으로 변환하는 과정이다.
각 함수는 하나의 인자를 받고, 다음 함수를 반환한다. 마지막 함수가 최종 결과를 반환한다.

기본적인 예시를 통해 살펴보자:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 일반 함수 (커링되지 않은 함수)
function add(a, b, c) {
  return a + b + c;
}

// 커링된 함수
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}

// 사용 방법
console.log(add(1, 2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6

커링된 함수 curriedAdd는 첫 번째 인자 a를 받고, 두 번째 인자 b를 받는 함수를 반환한다. 그리고 그 함수는 세 번째 인자 c를 받아 최종 결과를 계산하는 함수를 반환한다.

화살표 함수를 이용한 커링

ES6의 화살표 함수를 사용하면 커링 함수를 더 간결하게 작성할 수 있다:

1
2
3
4
// 화살표 함수로 작성한 커링 함수
const curriedAdd = a => b => c => a + b + c;

console.log(curriedAdd(1)(2)(3)); // 6

이 방식은 함수 본문이 단순할 때 특히 유용하며, 코드의 가독성을 높일 수 있다.

부분 적용(Partial Application)과의 차이점

커링과 부분 적용은 종종 혼동되는 개념이다. 두 기법 모두 함수의 인자를 미리 채우는 기법이지만, 작동 방식에 차이가 있다:

  • 커링: 함수를 여러 개의 단일 인자 함수로 분해한다. 각 함수는 정확히 하나의 인자만 받는다.
  • 부분 적용: 함수의 일부 인자를 미리 고정한 새로운 함수를 생성한다. 남은 인자는 한 번에 전달될 수 있다.

부분 적용의 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function partial(fn, ...fixedArgs) {
  return function(...remainingArgs) {
    return fn(...fixedArgs, ...remainingArgs);
  };
}

function add(a, b, c) {
  return a + b + c;
}

const add5and10 = partial(add, 5, 10);
console.log(add5and10(3)); // 18 (5 + 10 + 3)

여기서 partial 함수는 add 함수의 첫 두 인자를 고정하고, 나머지 인자를 나중에 전달받는 새로운 함수를 생성한다.

커링 유틸리티 함수 만들기

일반 함수를 커링 함수로 변환하는 유틸리티 함수를 만들 수 있습니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...moreArgs) {
        return curried.apply(this, args.concat(moreArgs));
      };
    }
  };
}

// 사용 예시
function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6

curry 함수는 인자의 수가 원본 함수의 매개변수 수(fn.length)와 같거나 많으면 바로 함수를 실행합니다. 그렇지 않으면 더 많은 인자를 기다리는 새로운 함수를 반환합니다. 이 방식을 통해 원하는 대로 인자를 부분적으로 적용할 수 있습니다.

커링의 실제 활용 사례

함수 재사용 및 특수화

커링을 통해 기본 함수를 재사용하여 특수한 용도의 함수를 쉽게 생성할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 일반 로깅 함수
const log = level => message => {
  console.log(`[${level}] ${message}`);
};

// 특수화된 로깅 함수
const error = log('ERROR');
const info = log('INFO');
const debug = log('DEBUG');

error('서버 연결 실패'); // [ERROR] 서버 연결 실패
info('사용자 로그인 성공'); // [INFO] 사용자 로그인 성공
debug('현재 메모리 사용량: 80%'); // [DEBUG] 현재 메모리 사용량: 80%

이 예시에서 log 함수는 커링을 활용해 다양한 로그 레벨에 맞는 특수화된 함수를 생성한다.

이벤트 처리 및 이벤트 리스너

이벤트 처리 함수를 커링하면 재사용 가능한 이벤트 핸들러를 만들 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 이벤트 핸들러 생성 함수
const handleEvent = eventType => element => callback => {
  element.addEventListener(eventType, callback);
  return {
    remove: () => element.removeEventListener(eventType, callback)
  };
};

// 특수화된 이벤트 핸들러
const handleClick = handleEvent('click');
const handleMouseover = handleEvent('mouseover');

// 사용 예시
const button = document.querySelector('#submit-button');
const clickHandler = handleClick(button)(e => {
  console.log('버튼이 클릭되었습니다', e);
});

// 나중에 이벤트 리스너 제거
// clickHandler.remove();

함수 조합(Function Composition)

커링은 함수 조합을 더 쉽게 만들어 함수형 파이프라인을 구성하는 데 도움이 된다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 함수 조합 유틸리티
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

// 단일 작업 함수들
const add10 = x => x + 10;
const multiply2 = x => x * 2;
const subtract5 = x => x - 5;

// 함수 조합으로 새로운 함수 생성
const transformValue = compose(subtract5, multiply2, add10);

console.log(transformValue(5)); // 25 ((5 + 10) * 2 - 5)

이 패턴은 데이터 변환 파이프라인을 만들 때 특히 유용하다.

데이터 필터링 및 변환

배열 처리 함수를 커링하여 재사용 가능한 필터와 변환 함수를 만들 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 필터 함수
const filter = predicate => array => array.filter(predicate);

// 맵 함수
const map = transformer => array => array.map(transformer);

// 술어 및 변환기
const isEven = x => x % 2 === 0;
const isPositive = x => x > 0;
const square = x => x * x;
const double = x => x * 2;

// 재사용 가능한 필터 및 변환기
const filterEven = filter(isEven);
const filterPositive = filter(isPositive);
const mapSquare = map(square);
const mapDouble = map(double);

// 데이터 처리
const numbers = [-2, -1, 0, 1, 2, 3, 4, 5];
const result = mapSquare(filterPositive(numbers));
console.log(result); // [1, 4, 9, 16, 25]

// 함수 조합으로 처리 파이프라인 생성
const processNumbers = compose(mapSquare, filterPositive);
console.log(processNumbers(numbers)); // [1, 4, 9, 16, 25]

커링과 클로저의 관계

커링은 클로저에 크게 의존한다.
클로저는 함수가 생성될 때의 환경을 기억하는 메커니즘으로, 함수가 자신이 생성된 범위 밖에서 실행되더라도 해당 환경의 변수에 접근할 수 있게 한다.

커링된 함수에서 각 단계의 함수는 이전 단계에서 받은 인자를 클로저를 통해 “기억"한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function curriedAdd(a) {
  // 여기서 a 값을 "기억"하는 클로저 생성
  return function(b) {
    // 여기서 a와 b 값을 "기억"하는 클로저 생성
    return function(c) {
      // 클로저를 통해 a, b, c 모두 접근 가능
      return a + b + c;
    };
  };
}

const step1 = curriedAdd(5); // a = 5를 기억
const step2 = step1(10);     // a = 5, b = 10을 기억
const result = step2(15);    // a = 5, b = 10, c = 15로 계산
console.log(result); // 30

클로저 덕분에 각 함수는 이전에 받은 인자들의 값을 유지할 수 있으며, 이는 커링이 작동하는 핵심 메커니즘이다.

커링의 장단점

장점

  1. 함수 재사용성: 기본 함수로부터 특수화된 함수를 쉽게 생성할 수 있다.
  2. 부분 적용: 일부 인자만 미리 적용하여 새로운 함수를 만들 수 있다.
  3. 가독성: 복잡한 함수 호출을 더 작고 관리하기 쉬운 단계로 나눌 수 있다.
  4. 조합 용이성: 함수 조합과 파이프라인 구성이 쉬워진다.
  5. 지연 평가: 모든 인자가 제공될 때까지 함수 실행을 지연시킬 수 있다.

단점

  1. 디버깅 어려움: 중첩된 함수 호출은 디버깅을 어렵게 만들 수 있다.
  2. 복잡성 증가: 커링 패턴에 익숙하지 않은 개발자에게는 코드가 더 복잡해 보일 수 있다.
  3. 성능 오버헤드: 여러 함수 호출과 클로저 생성으로 인한 미세한 성능 오버헤드가 있을 수 있다.
  4. 인자 순서 중요성: 커링은 인자 순서에 의존하므로, 인자 순서가 잘못되면 코드를 재구성해야 할 수 있다.

실전에서의 커링: 함수형 라이브러리

실제로 많은 함수형 프로그래밍 라이브러리는 커링을 지원한다.
예를 들어, Lodash의 _.curry 메소드는 함수를 커링된 버전으로 변환한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const _ = require('lodash');

function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = _.curry(add);

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

Ramda와 같은 라이브러리는 처음부터 모든 함수가 자동으로 커링된다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const R = require('ramda');

const add = R.add;
const multiply = R.multiply;

// 부분 적용
const add5 = add(5);
const multiply10 = multiply(10);

console.log(add5(10)); // 15
console.log(multiply10(3)); // 30

// 함수 조합
const transform = R.pipe(
  add(5),
  multiply(2),
  R.subtract(R.__, 3) // R.__는 플레이스홀더
);

console.log(transform(10)); // ((10 + 5) * 2 - 3) = 27

함수형 프로그래밍에서의 커링 활용

함수형 프로그래밍에서 커링은 다음과 같은 핵심 패턴에 활용된다:

데이터 후처리(Data-last) 패턴

함수형 프로그래밍에서는 데이터를 함수의 마지막 인자로 받는 패턴이 일반적.
이를 통해 데이터 처리 함수를 쉽게 체이닝할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 데이터 후처리 패턴의 함수들
const map = fn => array => array.map(fn);
const filter = predicate => array => array.filter(predicate);
const reduce = (fn, initial) => array => array.reduce(fn, initial);

// 사용 예시
const numbers = [1, 2, 3, 4, 5];
const double = x => x * 2;
const isEven = x => x % 2 === 0;
const sum = (acc, val) => acc + val;

const sumOfDoubledEvens = reduce(sum, 0)(
  map(double)(
    filter(isEven)(numbers)
  )
);

console.log(sumOfDoubledEvens); // 12 (2*2 + 4*2)

포인트-프리(Point-free) 스타일

커링을 사용하면 함수를 인자 없이 참조만으로 조합할 수 있는 “포인트-프리” 스타일 프로그래밍이 가능하다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// compose 함수 정의
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
// pipe 함수 정의 (왼쪽에서 오른쪽으로 적용)
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

// 기본 함수들
const add = a => b => b + a;
const multiply = a => b => b * a;
const subtract = a => b => b - a;

// 포인트-프리 스타일로 함수 조합
const calculate = pipe(
  add(5),       // 먼저 5를 더하고
  multiply(2),  // 그 결과에 2를 곱하고
  subtract(3)   // 마지막으로 3을 뺀다
);

console.log(calculate(10)); // ((10 + 5) * 2 - 3) = 27

이 스타일에서는 중간 변수나 명시적인 데이터 참조 없이 함수만으로 로직을 표현한다.

고급 커링 패턴

1. 플레이스홀더와 부분 적용

일부 라이브러리는 인자 순서를 유연하게 처리할 수 있는 플레이스홀더 기능을 제공한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Ramda 스타일의 플레이스홀더
const _ = {}; // 플레이스홀더 심볼

function advancedCurry(fn) {
  const arity = fn.length;
  
  return function curried(...args) {
    // 플레이스홀더가 아닌 인자만 계산
    const realArgs = args.filter(arg => arg !== _);
    
    // 플레이스홀더를 포함하지 않은 충분한 인자가 있거나,
    // 모든 인자(플레이스홀더 포함)가 제공된 경우
    if (realArgs.length >= arity || args.length >= arity) {
      // 인자 배열에서 플레이스홀더 처리
      const finalArgs = args.map(arg => arg === _ ? undefined : arg);
      return fn.apply(this, finalArgs);
    }
    
    // 그렇지 않으면 더 많은 인자를 기다리는 함수 반환
    return function(...moreArgs) {
      // 새 인자로 플레이스홀더 채우기
      const newArgs = args.map(arg => 
        arg === _ && moreArgs.length ? moreArgs.shift() : arg
      ).concat(moreArgs);
      
      return curried.apply(this, newArgs);
    };
  };
}

// 사용 예시
function divide(a, b, c) {
  return (a / b) / c;
}

const curriedDivide = advancedCurry(divide);

console.log(curriedDivide(10, 2, 2)); // 2.5
console.log(curriedDivide(10)(2)(2)); // 2.5
console.log(curriedDivide(_, 2, 2)(10)); // 2.5
console.log(curriedDivide(10, _, 2)(2)); // 2.5

멀티 커링(Multi-currying)

여러 인자를 그룹으로 처리하는 커링 방식:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function multiCurry(fn, numArgs) {
  return function curried(...args) {
    if (args.length >= numArgs) {
      return fn.apply(this, args);
    }
    
    return function(...moreArgs) {
      return curried.apply(this, args.concat(moreArgs));
    };
  };
}

// 사용 예시
function multiply4(a, b, c, d) {
  return a * b * c * d;
}

// 2개씩 인자 그룹화
const curriedMultiply = multiCurry(multiply4, 2);

console.log(curriedMultiply(2, 3)(4, 5)); // 120

동적 커링

함수의 매개변수 개수에 따라 동적으로 커링을 적용하는 패턴:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function dynamicCurry(fn) {
  function collectArgs(prevArgs) {
    return function(...currentArgs) {
      const args = [...prevArgs, ...currentArgs];
      
      if (fn.length === 0 || args.length >= fn.length) {
        return fn.apply(this, args);
      }
      
      return collectArgs(args);
    };
  }
  
  return collectArgs([]);
}

// 다양한 매개변수를 갖는 함수
function sum2(a, b) { return a + b; }
function sum3(a, b, c) { return a + b + c; }
function sumAll(...args) { return args.reduce((a, b) => a + b, 0); }

const curriedSum2 = dynamicCurry(sum2);
const curriedSum3 = dynamicCurry(sum3);
const curriedSumAll = dynamicCurry(sumAll);

console.log(curriedSum2(1)(2)); // 3
console.log(curriedSum3(1)(2)(3)); // 6
console.log(curriedSumAll(1)(2)(3)(4)); // 10

실제 프로젝트에서의 커링 사용 예시

이벤트 처리 시스템

웹 애플리케이션에서 이벤트 처리 시스템을 만들 때:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 이벤트 처리 시스템
const handleEvent = eventType => element => handler => {
  const wrappedHandler = event => {
    console.log(`${eventType} 이벤트 발생:`, event.target);
    handler(event);
  };
  
  element.addEventListener(eventType, wrappedHandler);
  
  // 이벤트 리스너 제거 함수 반환
  return () => element.removeEventListener(eventType, wrappedHandler);
};

// 특수화된 이벤트 핸들러
const handleClick = handleEvent('click');
const handleSubmit = handleEvent('submit');
const handleChange = handleEvent('change');

// 사용 예시
const button = document.querySelector('#save-button');
const form = document.querySelector('#user-form');
const input = document.querySelector('#username-input');

const removeClickListener = handleClick(button)(e => {
  console.log('저장 버튼 클릭됨');
});

const removeSubmitListener = handleSubmit(form)(e => {
  e.preventDefault();
  console.log('폼 제출됨');
});

const removeChangeListener = handleChange(input)(e => {
  console.log('입력값 변경:', e.target.value);
});

// 나중에 이벤트 리스너 제거
// removeClickListener();

API 요청 함수

다양한 API 요청을 처리하는 함수를 커링을 통해 구성:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 기본 fetch 래퍼
const fetchAPI = baseURL => endpoint => method => headers => body => {
  const url = `${baseURL}${endpoint}`;
  const options = {
    method,
    headers: {
      'Content-Type': 'application/json',
      ...headers
    },
    ...(body && { body: JSON.stringify(body) })
  };
  
  return fetch(url, options).then(response => {
    if (!response.ok) {
      throw new Error(`HTTP 오류: ${response.status}`);
    }
    return response.json();
  });
};

// API 클라이언트 구성
const api = fetchAPI('https://api.example.com');

// 엔드포인트별 함수
const usersAPI = api('/users');
const productsAPI = api('/products');

// 메서드별 함수
const getUsers = usersAPI('GET')({});
const createUser = usersAPI('POST')({});
const getProducts = productsAPI('GET')({});

// 사용 예시
getUsers()
  .then(users => console.log('사용자 목록:', users))
  .catch(error => console.error('오류:', error));

createUser({ name: '홍길동', email: 'hong@example.com' })
  .then(newUser => console.log('새 사용자:', newUser))
  .catch(error => console.error('오류:', error));

클래스 기반 코드에서의 커링 활용

객체지향 프로그래밍에서도 커링을 유용하게 활용할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class EventBus {
  constructor() {
    this.events = {};
  }

  // 이벤트 구독 메서드 (커링 스타일)
  on(eventName) {
    return (handler) => {
      if (!this.events[eventName]) {
        this.events[eventName] = [];
      }
      this.events[eventName].push(handler);
      
      // 구독 취소 함수 반환
      return () => {
        this.events[eventName] = this.events[eventName].filter(h => h !== handler);
      };
    };
  }

  // 이벤트 발행 메서드 (커링 스타일)
  emit(eventName) {
    return (data) => {
      const handlers = this.events[eventName] || [];
      handlers.forEach(handler => handler(data));
    };
  }
}

// 사용 예시
const bus = new EventBus();

const onUserLogin = bus.on('userLogin');
const onUserLogout = bus.on('userLogout');

const emitUserLogin = bus.emit('userLogin');
const emitUserLogout = bus.emit('userLogout');

// 이벤트 구독
const unsubscribe1 = onUserLogin(user => {
  console.log(`${user.name}님이 로그인했습니다`);
});

const unsubscribe2 = onUserLogin(user => {
  console.log(`로그인 시간: ${new Date().toLocaleTimeString()}`);
});

onUserLogout(user => {
  console.log(`${user.name}님이 로그아웃했습니다`);
});

// 이벤트 발행
emitUserLogin({ id: 1, name: '홍길동' });
// 출력:
// 홍길동님이 로그인했습니다
// 로그인 시간: 12:34:56

// 첫 번째 구독 취소
unsubscribe1();

// 다시 이벤트 발행 (첫 번째 핸들러는 호출되지 않음)
emitUserLogin({ id: 2, name: '김철수' });
// 출력:
// 로그인 시간: 12:35:01

emitUserLogout({ id: 1, name: '홍길동' });
// 출력:
// 홍길동님이 로그아웃했습니다

함수형 컴포넌트 설계

커링을 활용하여 재사용성이 높은 UI 컴포넌트를 설계할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 기본 버튼 컴포넌트 생성 함수
const createButton = theme => size => onClick => text => {
  const button = document.createElement('button');
  
  // 테마 적용
  button.classList.add(`btn-${theme}`);
  
  // 크기 적용
  button.classList.add(`btn-${size}`);
  
  // 텍스트 설정
  button.textContent = text;
  
  // 이벤트 핸들러 등록
  button.addEventListener('click', onClick);
  
  return button;
};

// 특정 테마의 버튼 생성자
const primaryButton = createButton('primary');
const secondaryButton = createButton('secondary');
const dangerButton = createButton('danger');

// 특정 크기의 버튼 생성자
const primaryLargeButton = primaryButton('large');
const primarySmallButton = primaryButton('small');
const dangerSmallButton = dangerButton('small');

// 실제 사용
const saveButton = primaryLargeButton(
  () => console.log('저장 버튼 클릭됨')
)('저장');

const cancelButton = secondaryButton('medium')(
  () => console.log('취소 버튼 클릭됨')
)('취소');

const deleteButton = dangerSmallButton(
  () => {
    if (confirm('정말 삭제하시겠습니까?')) {
      console.log('삭제 실행');
    }
  }
)('삭제');

// DOM에 추가
document.body.appendChild(saveButton);
document.body.appendChild(cancelButton);
document.body.appendChild(deleteButton);

커링과 함수 메모이제이션

커링과 메모이제이션(이전 호출 결과를 캐싱하는 기법)을 결합하여 성능을 최적화할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 메모이제이션 함수
function memoize(fn) {
  const cache = new Map();
  
  return function(args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// 커링과 메모이제이션 조합
function curryAndMemoize(fn) {
  const memoized = memoize(fn);
  
  return function curried(args) {
    if (args.length >= fn.length) {
      return memoized.apply(this, args);
    }
    
    return function(moreArgs) {
      return curried.apply(this, args.concat(moreArgs));
    };
  };
}

// 사용 예시: 피보나치 수열
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const efficientFib = curryAndMemoize(fibonacci);

console.time('첫 번째 호출');
console.log(efficientFib(40)); // 102334155
console.timeEnd('첫 번째 호출');

console.time('두 번째 호출');
console.log(efficientFib(40)); // 102334155 (캐시에서 반환)
console.timeEnd('두 번째 호출');

타입스크립트에서의 커링

타입스크립트를 사용하는 환경에서도 타입 안전성을 유지하면서 커링을 활용할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 기본 커링 함수
function curry<A, B, C, R>(
  fn: (a: A, b: B, c: C) => R
): (a: A) => (b: B) => (c: C) => R {
  return (a: A) => (b: B) => (c: C) => fn(a, b, c);
}

// 커링된 함수의 타입 정의
type CurriedFunction3<A, B, C, R> = 
  (a: A) => (b: B) => (c: C) => R;

// 함수 정의와 커링
function add(a: number, b: number, c: number): number {
  return a + b + c;
}

const curriedAdd: CurriedFunction3<number, number, number, number> = curry(add);

// 사용
const result = curriedAdd(1)(2)(3); // 6

// 부분 적용 함수의 타입 정의
type PartiallyApplied2<A, B, C, R> = 
  (b: B, c: C) => R;

function partial<A, B, C, R>(
  fn: (a: A, b: B, c: C) => R,
  a: A
): PartiallyApplied2<A, B, C, R> {
  return (b: B, c: C) => fn(a, b, c);
}

// 부분 적용 사용
const addFrom10 = partial(add, 10);
const result2 = addFrom10(5, 3); // 18

비동기 작업에서의 커링

비동기 프로그래밍에서도 커링은 유용하게 활용될 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 비동기 작업을 위한 커링된 함수
const fetchResource = baseURL => resourceType => id => {
  const url = `${baseURL}/${resourceType}/${id}`;
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP 오류: ${response.status}`);
      }
      return response.json();
    });
};

// API 기본 URL 설정
const fetchFromAPI = fetchResource('https://api.example.com');

// 특정 리소스 유형에 대한 함수
const fetchUser = fetchFromAPI('users');
const fetchProduct = fetchFromAPI('products');
const fetchOrder = fetchFromAPI('orders');

// 사용 예시
async function loadUserData() {
  try {
    const user = await fetchUser(123);
    console.log('사용자 정보:', user);
    
    // 사용자의 주문 가져오기
    const orders = await Promise.all(
      user.orderIds.map(orderId => fetchOrder(orderId))
    );
    
    console.log('주문 내역:', orders);
  } catch (error) {
    console.error('데이터 로드 실패:', error);
  }
}

loadUserData();

참고 및 출처