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 함수 예시 코드


꼬리말

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




참고

Wanna get in touch?

All Icons byiconiFy