JavaScript 커링(Currying) 이해하기: 6가지 실전 활용 사례

작성 : 2023-12-13수정 : 2023-12-13

목차 펼치기

머리말

커링(Currying)은 함수형 프로그래밍의 중요한 개념으로 코드의 재사용성을 향상시킬 수 있지만 쉽게 이해하고 적용하기에는 다소 어려울 수 있다. 이 글의 커링 함수에 대한 기본적인 개념과 더불어 유용하게 사용될 수 있는 사례를 같이 정리해보는 과정을 통해 함수형 프로그래밍 사고로 한 걸음 더 걸어가보자.



입문편 - 커링이란

커링은 카레가 아니다.

커링은

여러 개의 입력을 받는 함수

한 개의 입력만 받는 여러 개의 함수

로 변환하는 과정을 의미한다.

커링이라는 이름의 유래에는 20세기 미국의 수학자인 해스켈 커리(Haskell Brooks Curry)가 있다. 커링의 개념을 만든 것은 아니지만 조합 논리와 람다 대수 분야에 기여하며 함수형 프로그래밍 발전에 큰 영향을 끼치신 분으로 그의 이름을 따 커링이라고 명명하게 되었다. 하스켈(Haskell) 함수형 프로그래밍 언어도 이 분의 이름을 따서 만들어졌다.

함수형 프로그래밍에서 자주 사용되는 기법 중 하나로 하스켈이나 스칼라(Scala)와 같은 함수형 프로그래밍 언어에는 기본적으로 내장되어 있다. 자바스크립트에서는 특유의 유연한 문법으로 이를 구현할 수 있다.


커링 함수 사용하기

javascript
1// 커링 함수 선언
2const applyDiscount = discountRate => price => {
3  return price - price * discountRate;
4};
5
6// 이렇게도 쓸 수 있고
7const tenPercentDiscount = applyDiscount(0.10);
8const discountedPrice = tenPercentDiscount(100);
9console.log(discountedPrice); // 출력: 90
10
11// 연속적으로 호출해서 이렇게도 쓸 수 있다.
12const directlyDiscountedPrice = applyDiscount(0.15)(50);
13console.log(directlyDiscountedPrice); // 출력: 42.5

커링 함수 예시 코드

  • 커링 함수(Currying Function)란 커링을 통해 생성된 함수를 의미한다.

  • 커링 함수는 한 번에 하나의 매개변수만 전달받는다.

  • 함수를 호출하는

    ()

    를 연속적으로 사용하는 방식을 통해 커링 함수를 연속적으로 호출할 수 있다.

  • 커링 함수는 함수를 반환하기 때문에 고차 함수의 특성을 지닐 수 있다. 고차 함수란 다른 함수를 인자로 받거나 함수를 결과로 반환하는 함수를 말한다.


커링 함수의 장점

  • 재사용성

    : 일부 매개변수를 미리 설정하고, 나머지 매개변수에 대해 나중에 처리할 수 있게 함으로써 함수의 재사용성을 높일 수 있다.

  • 모듈성

    : 크고 복잡한 함수를 작고 관리하기 쉬운 단위로 분할할 수 있다.

  • 유연성

    : 동일한 기본 로직을 효과적으로 재사용할 수 있다.


커링 함수의 단점

  • 성능 저하

    : 매개변수가 많아질수록 추가적인 함수 호출과 메모리 할당을 발생시켜 성능 이슈가 발생할 수 있다.

  • 러닝 커브

    :익숙하지 않은 사람들에게는 코드의 가독성을 저하시킬 수 있다.

  • 어려운 디버깅

    : 함수 호출 구조가 복잡해지고 여러 단계로 나뉘어 있기 때문에 추적이 더 어려워 질 수 있다.


실전편 - 6가지 활용 사례

TIP

변동 가능성이 낮은 인자를 우선하고 높을 수록 뒷 순서로 받는 것이 좋다.


활용 사례 1. 함수의 지연 실행

javascript
1function multiply(a) {
2  return function(b) {
3    return a * b;
4  };
5}
6
7// a=2라는 상태를 기억하는 클로저다.
8const double = multiply(2); 
9
10// 두 번째 인자를 전달해 함수를 지연 실행할 수 있다.
11console.log(double(4));  // 8 (2 * 4)

커링 함수와 클로저를 이용한 함수의 지연 실행 코드

클로저를 이용해 상태를 보존하고 함수가 지연 실행되도록 할 수 있다. 위 코드에서

double

함수는

multiply

함수의

a

2

라는 값을 할당해 자유 변수로 가지는 클로저다.

double

함수를 호출할 때 두 번째 인자를 전달해 최종적으로 계산을 수행하도록 할 수 있다.


활용 사례 2. React 이벤트 핸들러 간소화

javascript
1function MyComponent() {
2
3	// 이벤트 핸들러를 커링 함수로 사용하면 가독성이 좋아진다.
4  const handleItemClick = itemId => event => {
5    console.log(`Item ${itemId} clicked`, event);
6  };
7
8  return (
9    <div>
10      {['item1', 'item2', 'item3'].map(itemId => (
11        <button key={itemId} onClick={handleItemClick(itemId)}>
12          Click {itemId}
13        </button>
14      ))}
15    </div>
16  );
17}

커링을 이용한 React 이벤트 핸들러 간소화 코드

리액트 컴포넌트에서 이벤트 핸들러에 추가적인 매개변수를 전달할 때 유용하다.
더 이상

onClick={(e) => handleItemClick(itemId)}

와 같이 선언하지 않아도 된다.


활용 사례 3. API 호출 처리

javascript
1const createAPIEndpoint = base => endpoint => params => {
2  const query = new URLSearchParams(params).toString();
3  return `${base}/${endpoint}?${query}`;
4};
5
6// 기본 API URL
7const baseAPI = createAPIEndpoint('https://example.com/api');
8
9// 엔드포인트 확장
10const fetchUser = baseAPI('user');
11const fetchPosts = baseAPI('posts');
12
13// 사용 예시
14const userAPIPath = fetchUser({ id: '123' });
15console.log(userAPIPath); // "https://example.com/api/user?id=123"
16
17const postsAPIPath = fetchPosts({ userId: '123', limit: 10 });
18console.log(postsAPIPath); // "https://example.com/api/posts?userId=123&limit=10"

커링을 이용한 API 호출 처리 코드

API 엔드포인트 경로를 확장하거나 데이터 처리 로직을 유연하게 구성할 수 있다.


활용 사례 4. 고차 컴포넌트(HOC)

javascript
1// 커링 함수를 이용한 HOC 선언
2function withLoading(WrappedComponent) {
3  return function({ isLoading, ...rest }) {
4    if (isLoading) {
5      return <div>Loading...</div>;
6    } else {
7      return <WrappedComponent {...rest} />;
8    }
9  };
10}
11
12// 예시 컴포넌트 선언
13function MyComponent({ data }) {
14  return (
15    <div>
16      <h1>My Component</h1>
17      <p>{data}</p>
18    </div>
19  );
20}
21
22// 사용 예시
23const MyComponentWithLoading = withLoading(MyComponent);
24
25function App() {
26	// 데이터를 가져오는 상태 시뮬레이션
27  const [loading, setLoading] = useState(true);
28  const [data, setData] = useState(null);
29
30	...
31
32	return (
33		<MyComponentWithLoading isLoading={loading} data={data} />
34	);
35}

커링을 이용한 고차 컴포넌트 코드

고차 컴포넌트는 다른 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수다. 컴포넌트 간의 공통 로직을 추출해서 재사용하기 위해 사용되는데 여기에 커링을 적용할 수 있다.


활용 사례 5. 팩토리 패턴(Factory Pattern)

javascript
1import React from "react";
2
3// 커링 함수를 사용한 컴포넌트 생성 함수
4const createComponent = (Component) => (properties) => {
5  return <Component {...properties} />;
6};
7
8// 커링 함수 사용하여 특정 컴포넌트 생성 함수 만들기
9const createButton = createComponent("button"); // HTML button 요소
10const createLabel = createComponent("label"); // HTML label 요소
11
12// 개별 컴포넌트 생성
13const BlueButton = () =>
14  createButton({ style: { color: "blue" }, children: "Click me" });
15const LargeLabel = () =>
16  createLabel({ style: { fontSize: "large" }, children: "Label text" });
17
18// 사용 예시
19function App() {
20  return (
21    <div>
22      <BlueButton />
23      <LargeLabel />
24    </div>
25  );
26}
27
28export default App;

커링을 이용한 팩토리 패턴 코드

팩토리 패턴은 객체 지향 프로그래밍에서 객체 생성을 캡슐화하는 디자인 패턴으로, 동일한 프로퍼티를 가진 여러 작은 객체를 만들어낼 때 유용하다. 커링을 이용하여 재사용 가능한 팩토리 함수를 구현할 수 있다.


이렇게 컴포넌트가 렌더링 된다.



활용 사례 6. Reducer 간소화

javascript
1/** 커링 함수로 구현한 상태 업데이트 함수
2* field: 업데이트할 상태의 필드명.
3* newValue: 새로 설정할 값.
4* state: 현재 상태.
5**/
6const updateField = field => newValue => state => ({
7  ...state,
8  [field]: newValue
9});
10
11function userReducer(state = {}, action) {
12  switch (action.type) {
13		// 커링 함수를 사용하는 경우
14    case 'UPDATE_USER_FIELD':
15      return updateField(action.field)(action.value)(state);
16		// 일반적으로 사용하는 경우
17		case 'UPDATE_USER_FIELD':
18      return {
19        ...state,
20        [action.field]: action.value
21      };
22    // 다른 액션 핸들링...
23    default:
24      return state;
25  }
26}
27
28// 이하 사용 방식 동일
29const updateUserField = (field, value) => ({
30  type: 'UPDATE_USER_FIELD',
31  field,
32  value
33});
34
35
36dispatch(updateUserField(field, value));

커링을 이용한 Reducer 간소화 코드

커링 함수를 사용하여 Redux 상태 관리의 Reducer를 더 명확하고 간결하게 표현하여 Reducer 함수가 길어지거나 복잡해지지 않을 수 있다. 하지만, 각 액션에 대한 처리를 Reducer 내에서 구현하는 것이 기본 원칙으로 더 직관적이기 때문에 커링을 사용하지 않는 게 유지보수에 더 용이할 수 있다.


응용편 - Ramda.js 라이브러리 이용하기

Ramda.js

는 함수형 프로그래밍을 위한 자바스크립트 라이브러리로 커링을 포함한 여러 기법을 지원하고 있다. Ramda가 제공하는

compose

pipe

함수들을 이용해 여러 함수를 하나의 함수로 결합하여 처리할 수 있다. 두 함수의 차이점은 함수의 처리 순서가 다르다는 것이다.


pipe

  • 제공한 순서대로 함수를 처리한다.

  • 사람의 인지 체계와 유사하다.

javascript
1import { pipe, add, multiply } from 'ramda';
2
3const multiplyByTwo = multiply(2);
4const addOne = add(1);
5
6const doubleAndIncrement = pipe(
7  multiplyByTwo, // 먼저 적용
8  addOne         // 나중에 적용
9);
10
11const result = doubleAndIncrement(3); // 먼저 3에 2를 곱하고, 그 결과에 1을 더함
12console.log(result); // 결과: 7 (3 * 2 + 1)
13
14// 즉, 이 코드와 동일하다.
15console.log(addOne(multiplyByTwo(3)));

Ramda.js pipe 함수 예시 코드


compose

  • 제공한 순서의 역순대로 함수를 처리한다.

javascript
1import { compose, add, multiply } from 'ramda';
2
3const multiplyByTwo = multiply(2);
4const addOne = add(1);
5
6const incrementAndDouble = compose(
7  multiplyByTwo, // 나중에 적용
8  addOne         // 먼저 적용
9);
10
11const result = incrementAndDouble(3); // 먼저 3에 1을 더하고, 그 결과에 2를 곱함
12console.log(result); // 결과: 8 (3 + 1) * 2
13
14// 즉, 이 코드와 동일하다.
15console.log(multiplyByTwo(addOne(3)));

Ramda.js compose 함수 예시 코드


꼬리말

커링을 이해하면 함수형 프로그래밍으로 사고하는 데 한층 더 도움이 될 것이다. 하지만 대부분의 기술이 그렇겠지만 커링 함수도 무조건적으로 도입하면 좋은 기법은 아니다. 장점과 단점이 명확한 만큼 이를 이해하고 상황에 맞게 적절히 활용하여 접근하는 것이 중요하다.




참고

Contact Me

All Icons byiconiFy