Currying#
커링(Currying)은 함수형 프로그래밍에서 유래한 중요한 개념으로, 여러 개의 인자를 받는 함수를 단일 인자를 받는 일련의 함수들로 변환하는 기법이다.
이 기법은 수학자이자 논리학자인 하스켈 커리(Haskell Curry)의 이름을 따서 명명되었다.
커링은 자바스크립트의 함수형 프로그래밍 패러다임에서 특히 유용하며, 함수 합성과 부분 적용을 가능하게 하는 강력한 도구이다.
커링은 자바스크립트에서 함수형 프로그래밍을 구현하는 데 중요한 기법 중 하나이다.
이 기법은 코드의 재사용성과 모듈성을 높이고, 함수 조합을 용이하게 하며, 복잡한 로직을 더 작고 관리하기 쉬운 단위로 분해하는 데 도움이 된다.
커링의 주요 이점:
- 함수 재사용성 증가: 기본 함수로부터 특화된 함수를 쉽게 생성
- 코드 가독성 향상: 복잡한 로직을 더 작고 명확한 단계로 분해
- 함수 조합 용이: 파이프라인과 체이닝 패턴을 더 쉽게 구현
- 지연 평가: 모든 인자가 제공될 때까지 함수 실행을 지연
반면, 커링은 처음 접하는 개발자에게는 익숙하지 않을 수 있으며, 디버깅이 복잡해질 수 있고, 코드 흐름을 추적하기 어려울 수 있다는 단점도 있다.
커링은 단순히 이론적인 개념이 아니라 실제 애플리케이션 개발에서 유용하게 활용될 수 있는 실용적인 패턴이다.
함수형 프로그래밍의 개념을 이해하고 적용하면, 코드의 품질과 유지보수성을 크게 향상시킬 수 있다.
자바스크립트의 유연한 특성 덕분에 커링은 다양한 방식으로 구현하고 활용할 수 있으며, 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
|
클로저 덕분에 각 함수는 이전에 받은 인자들의 값을 유지할 수 있으며, 이는 커링이 작동하는 핵심 메커니즘이다.
커링의 장단점#
- 함수 재사용성: 기본 함수로부터 특수화된 함수를 쉽게 생성할 수 있다.
- 부분 적용: 일부 인자만 미리 적용하여 새로운 함수를 만들 수 있다.
- 가독성: 복잡한 함수 호출을 더 작고 관리하기 쉬운 단계로 나눌 수 있다.
- 조합 용이성: 함수 조합과 파이프라인 구성이 쉬워진다.
- 지연 평가: 모든 인자가 제공될 때까지 함수 실행을 지연시킬 수 있다.
- 디버깅 어려움: 중첩된 함수 호출은 디버깅을 어렵게 만들 수 있다.
- 복잡성 증가: 커링 패턴에 익숙하지 않은 개발자에게는 코드가 더 복잡해 보일 수 있다.
- 성능 오버헤드: 여러 함수 호출과 클로저 생성으로 인한 미세한 성능 오버헤드가 있을 수 있다.
- 인자 순서 중요성: 커링은 인자 순서에 의존하므로, 인자 순서가 잘못되면 코드를 재구성해야 할 수 있다.
실전에서의 커링: 함수형 라이브러리#
실제로 많은 함수형 프로그래밍 라이브러리는 커링을 지원한다.
예를 들어, 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();
|
참고 및 출처#