Currying vs. Partial Application

자바스크립트의 함수형 프로그래밍에서 가장 중요한 개념 중 두 가지는 커링(Currying)과 부분 적용(Partial Application)이다. 이 두 기법은 콜백 함수를 다루는 강력한 패턴으로, 코드의 재사용성과 모듈성을 크게 향상시킨다.

커링과 부분 적용은 자바스크립트의 콜백 함수를 더 효과적으로 다루기 위한 강력한 기법이다.
두 패턴 모두 함수의 재사용성을 높이고 코드를 더 모듈화하는 데 도움이 된다.

두 기법의 핵심 차이는 인자 처리 방식과 최종 함수의 구조에 있다.
커링은 항상 단일 인자 함수의 체인을 만들고, 부분 적용은 일부 인자를 고정한 새로운 함수를 만든다.

프로젝트의 요구 사항과 프로그래밍 스타일에 따라 적절한 기법을 선택하거나, 두 기법을 결합하여 사용하면 더 유연하고 재사용 가능한 코드를 작성할 수 있다. 함수형 프로그래밍을 더 깊이 이해하고 적용하려면 이 두 가지 핵심 개념을 마스터하는 것이 중요하다.

커링(Currying)

커링은 여러 개의 인자를 받는 함수를 단일 인자를 받는 함수들의 체인으로 변환하는 기법이다.
이 기법은 수학자이자 논리학자인 하스켈 커리(Haskell Curry)의 이름을 따서 명명되었다.

커링의 기본 개념

커링된 함수는 한 번에 하나의 인자만 받고, 모든 필요한 인자가 제공될 때까지 새로운 함수를 반환한다.
마지막 인자가 제공되면 최종 결과가 계산된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 일반 함수
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

// ES6 화살표 함수로 더 간결하게 표현
const arrowCurriedAdd = a => b => c => a + b + c;
console.log(arrowCurriedAdd(1)(2)(3)); // 6

커링 유틸리티 함수

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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));
      };
    }
  };
}

// 사용 예시
const curriedSum = curry(function(a, b, c) {
  return a + b + c;
});

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

커링의 장점

  1. 함수 특수화: 기본 함수로부터 특화된 함수를 쉽게 생성할 수 있다.
  2. 코드 재사용성: 함수를 보다 작고 특화된 단위로 분해할 수 있다.
  3. 데이터 흐름 제어: 함수 호출 시점과 데이터 제공 시점을 분리할 수 있다.

부분 적용(Partial Application)

부분 적용은 함수의 일부 인자를 미리 고정하여, 나머지 인자만 받는 새로운 함수를 생성하는 기법이다.

부분 적용의 기본 개념

부분 적용은 원본 함수에 일부 인자를 적용하여 보다 특화된 함수를 만든다.
이 과정은 원본 함수가 필요로 하는 인자 수보다 적은 수의 인자를 사용한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 세 개의 인자를 받는 원본 함수
function multiply(a, b, c) {
  return a * b * c;
}

// 부분 적용을 통해 첫 번째 인자를 고정한 새 함수 생성
function multiplyBy2(b, c) {
  return multiply(2, b, c);
}

console.log(multiplyBy2(3, 4)); // 24 (2 * 3 * 4)

부분 적용 유틸리티 함수

더 일반화된 부분 적용을 위한 유틸리티 함수를 만들 수 있다:

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

// 사용 예시
const multiplyBy2 = partial(multiply, 2);
const multiplyBy2And3 = partial(multiply, 2, 3);

console.log(multiplyBy2(3, 4)); // 24 (2 * 3 * 4)
console.log(multiplyBy2And3(4)); // 24 (2 * 3 * 4)

Function.prototype.bind()를 활용한 부분 적용

자바스크립트 내장 메서드 bind()를 사용해서도 부분 적용을 구현할 수 있다:

1
2
3
4
5
const multiplyBy2 = multiply.bind(null, 2);
const multiplyBy2And3 = multiply.bind(null, 2, 3);

console.log(multiplyBy2(3, 4)); // 24
console.log(multiplyBy2And3(4)); // 24

부분 적용의 장점

  1. 코드 간결성: 반복적인 인자를 제거하여 코드를 간결하게 만든다.
  2. 인자 고정: 특정 설정이나 환경에 맞게 함수의 일부 인자를 고정할 수 있다.
  3. 응용 범위: 다양한 상황에서 유연하게 적용할 수 있다.

커링과 부분 적용의 주요 차이점

두 기법 모두 함수의 인자를 미리 설정하는 방법이지만, 핵심적인 차이가 있다:

  1. 인자 처리 방식

    • 커링: 항상 하나의 인자만 처리하며, 각 단계마다 새로운 함수를 반환한다.
    • 부분 적용: 여러 개의 인자를 한 번에 고정할 수 있습니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // 커링 - 한 번에 하나의 인자만 처리
    const curriedAdd = a => b => c => a + b + c;
    const step1 = curriedAdd(1); // a=1이 고정된 함수 반환
    const step2 = step1(2);      // b=2가 고정된 함수 반환
    const result1 = step2(3);    // c=3을 받아 최종 결과 계산
    
    // 부분 적용 - 여러 인자를 한 번에 처리 가능
    const partialAdd = partial(add, 1, 2); // a=1, b=2가 고정된 함수 반환
    const result2 = partialAdd(3);         // c=3을 받아 최종 결과 계산
    
  2. 함수 특성

    • 커링: 원본 함수를 완전히 변환하여, 각 함수가 항상 하나의 인자만 받도록 한다.
    • 부분 적용: 원본 함수의 일부 인자만 고정하고, 나머지 인자는 원래대로 받는다.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // 4개의 인자를 받는 함수
    function process(a, b, c, d) {
      return (a + b) * (c + d);
    }
    
    // 커링 - 각 함수는 항상 하나의 인자만 받음
    const curriedProcess = curry(process);
    const result3 = curriedProcess(1)(2)(3)(4); // ((1 + 2) * (3 + 4)) = 21
    
    // 부분 적용 - 일부 인자만 고정
    const partialProcess = partial(process, 1, 2);
    const result4 = partialProcess(3, 4);       // ((1 + 2) * (3 + 4)) = 21
    
  3. 반환 함수의 구조

    • 커링: 항상 단일 인자 함수의 중첩된 체인을 만든다.
    • 부분 적용: 원본 함수의 인자 수에서 고정된 인자 수를 뺀 만큼의 인자를 받는 함수를 만든다.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // 인자 3개를 받는 원본 함수
    function volume(length, width, height) {
      return length * width * height;
    }
    
    // 커링 - 항상 단일 인자 함수의 체인을 반환
    const curriedVolume = curry(volume);
    // => length => width => height => length * width * height
    
    // 부분 적용 - 남은 인자 수에 따라 달라짐
    const boxVolume = partial(volume, 10); // 길이가 10인 상자의 부피 계산
    // => (width, height) => 10 * width * height
    
  4. 유연성과 사용성

    • 커링: 인자 순서가 중요하며, 순서대로 적용해야 한다(플레이스홀더를 사용하지 않는 한).
    • 부분 적용: 더 유연하게 어떤 인자든 고정할 수 있다(특히 플레이스홀더 패턴 사용 시).

실제 사용 사례 비교

커링 사용 사례

  1. 로깅 함수 특수화

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // 로깅 함수를 커링으로 구현
    const log = level => message => timestamp => {
      console.log(`[${timestamp}] [${level}] ${message}`);
    };
    
    // 특수화된 로깅 함수
    const errorLog = log('ERROR');
    const infoLog = log('INFO');
    
    // 더 특수화된 함수
    const errorNow = errorLog('서버 연결 실패')(new Date().toISOString());
    const infoNow = infoLog('사용자 로그인 성공')(new Date().toISOString());
    
  2. 함수 파이프라인 구성

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    // 함수 조합 유틸리티
    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. API 요청 함수 구성

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    function fetchAPI(url, method, headers, body) {
      return fetch(url, {
        method,
        headers,
        body: JSON.stringify(body)
      }).then(response => response.json());
    }
    
    // 기본 설정으로 부분 적용
    const fetchFromUsers = partial(
      fetchAPI,
      'https://api.example.com/users',
      'GET',
      { 'Content-Type': 'application/json' }
    );
    
    // 사용
    fetchFromUsers(null).then(users => console.log(users));
    
  2. 이벤트 핸들러 구성

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    function handleEvent(eventType, elementId, data, event) {
      event.preventDefault();
      console.log(`${eventType} 이벤트 발생: ${elementId}`);
      console.log('데이터:', data);
      console.log('이벤트 객체:', event);
    }
    
    // 특정 이벤트와 요소에 대한 핸들러 생성
    const handleButtonClick = partial(handleEvent, 'click', 'submit-button', { action: 'save' });
    
    document.getElementById('submit-button').addEventListener('click', handleButtonClick);
    

커링과 부분 적용의 결합

두 기법은 함께 사용할 때 더욱 강력해진다:

 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
// 부분 적용과 커링을 결합한 유틸리티 함수
function curryAndPartial(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return function(...moreArgs) {
      return curried.apply(this, args.concat(moreArgs));
    };
  };
}

// 플레이스홀더 지원 추가
const _ = Symbol('placeholder');

function curryWithPlaceholders(fn) {
  return function curried(...args) {
    // 모든 플레이스홀더가 아닌 인자가 채워졌는지 확인
    const hasUnfilledPositions = args.length < fn.length || args.includes(_);
    
    if (!hasUnfilledPositions) {
      return fn.apply(this, args);
    }
    
    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 flexibleDivide = curryWithPlaceholders(divide);

console.log(flexibleDivide(100, 5, 2));      // 10
console.log(flexibleDivide(100)(5)(2));      // 10
console.log(flexibleDivide(_, 5, _)(100)(2)); // 10

커링과 부분 적용의 비교

특성커링(Currying)부분 적용(Partial Application)
정의여러 인자를 받는 함수를 단일 인자 함수들의 체인으로 변환함수의 일부 인자를 고정하여 나머지 인자만 받는 함수 생성
인자 처리한 번에 하나의 인자만 처리여러 인자를 한 번에 처리 가능
반환 함수항상 단일 인자 함수 반환남은 인자 수에 따라 가변적인 함수 반환
인자 순서인자 순서가 중요(기본적으로 순차적 적용)더 유연하게 어떤 위치의 인자든 적용 가능
원본 함수 변환완전히 새로운 함수 체인으로 변환원본 함수의 일부 인자만 고정
중간 단계 함수중간 단계 함수들이 재사용 가능주로 최종 함수만 사용
구현 난이도일반적으로 더 복잡함상대적으로 더 간단함
메모리 사용함수 체인으로 인해 더 많은 클로저 생성일반적으로 더 적은 클로저 생성
유연성각 단계에서 함수 변환 가능한 번의 변환으로 새 함수 생성
함수형 프로그래밍함수 조합과 파이프라인에 더 적합실용적인 코드 중복 제거에 효과적
주요 사용 사례함수 합성, 데이터 파이프라인, 단계별 특수화API 호출, 이벤트 핸들러, 설정 함수
표준 라이브러리Ramda의 curry 함수Lodash의 partial 함수, JS의 Function.prototype.bind
실행 시점모든 인자가 제공될 때까지 실행 지연새 함수가 호출될 때만 실행
부수 효과 처리단일 책임 원칙에 더 적합여러 작업을 한 번에 묶을 수 있음

어떤 것을 선택해야 할까?

상황에 따라 두 패턴 중 하나를 선택하거나 둘을 함께 사용하는 것이 좋다:

커링을 선택하는 경우:

  • 함수 조합과 파이프라인을 구성하려는 경우
  • 함수를 단계별로 특수화하고 재사용하려는 경우
  • 순수 함수형 프로그래밍 스타일을 선호하는 경우

부분 적용을 선택하는 경우:

  • 코드 중복을 줄이는 것이 주요 목표인 경우
  • API 호출, 이벤트 처리 등 실용적인 문제를 해결하려는 경우
  • 함수의 일부 인자만 미리 설정하려는 경우

참고 및 출처