커링에 관하여

2024년 7월 25일

커링은 함수형 프로그래밍에서 중요한 개념 중 하나이다.

커링은 다수의 인수를 받는 함수를 한번에 하나의 인수만 받는 여러 개의 함수로 변환하는 과정을 의미한다.

이게 무슨 의미일까?

검색을 해보면 대표적으로 add 함수를 통해서 커링을 구현한 것을 볼 수 있다.

const curriedAdd = (x) => (y) => x + y;

const addTwo = curriedAdd(2);
console.log(addTwo(3)); // 5
console.log(curriedAdd(2)(3)); // 5

보면 초기에 인자 x값인 2를 받고, 그 후에 y 인자 값을 받아서 값을 리턴해주는 것을 볼수 있다.

여기서의 장점은 초기 값(x)를 할당해놓고 후에 계산 될 값(y)를 동적으로 결정해줄 수 있다.

즉, 함수의 인자를 나누어서 각각의 함수로 분리가 가능하다. 라고 볼 수 있다.

하지만 이러한 예시로는 커링의 실질적인 장점이 와닿지 않았다.

더 와닿는 실질적인 예시가 필요했다.

실질적인 예시

1. 유효성 검증

각각의 필드에 관한 유효성 검증을 진행한다고 가정해보자.

const isLengthGreaterThan = (minLength) => (value) => value.length > minLength;

const isPasswordValid = isLengthGreaterThan(8);
const isUsernameValid = isLengthGreaterThan(5);
const isEmailValid = (value) => value.includes('@');

const validate = (value, ...validators) => validators.every((validator) => validator(value));

console.log(validate('123456789', isPasswordValid, isUsernameValid));
console.log(validate('user@example.com', isUsernameValid, isEmailValid));

위처럼 각각의 필드 값들에 대해서 여러개의 유효성 검증을 적용시킬 수 있다.

isLengthGreaterThan 함수의 인자를 각각 8과 5로 고정하고 부분 적용 된 함수를 만들어서 재사용성을 높일 수 있다.

또한 커링 함수는 작은 함수들로 나누어져 있기 때문에 필요한 때에 따라서 조합하여 복잡한 검증 로직도 처리할 수 있다.

const isLengthGreaterThan = (minLength) => (value) => value.length > minLength;
const isValidRegex = (regex) => (value) => regex.test(value);

const validate = (value, ...validators) => validators.every((validator) => validator(value));

const validatePassword = validate(
  'StrongPassword1',
  isLengthGreaterThan(8),
  isValidRegex(/\d/),
  isValidRegex(/[A-Z]/),
);

console.log(validatePassword);

위처럼 여러개의 조건식을 간단히 처리해줄수 있고, 작은 함수들로 나누어져있는 상태이기때문에 디버깅과 테스트의 용이성도 갖고 있다.

위의 예시로도 커링의 장점을 파악할 수 있지만, 사실 이는 가장 작은 함수들로 나누어주어도 상관이 없는 로직이다.

더욱 실질적인 예시가 없을까?

2. 많은 데이터가 필요할때

만약 음식점과 관련 된 기능을 구현해야 한다고 가정해보자.

우리가 필요한것은 사람, 식당, 음식에 관한 데이터이고 이를 어떠한 스토리지에서 관리해야한다.

이를 PersonStorage, RestaurantStorage, FoodStorage 같이 각각의 스토리지를 내부에서 선언해서 사용해줄수도 있을것이다.

const PersonStorage = {
  get: () => {
    return storage.getItem('person');
  },

  set: (data) => {
    storage.setItem('person', data);
  },

  //...
};

const RestaurantStorage = {
  get: () => {
    return storage.getItem('restaurant');
  },

  set: (data) => {
    storage.setItem('restaurant', data);
  },

  //...
};

하지만 이는 동일한 로직을 반복하는 것이므로 재사용성이 좋지 않다.

이를 커링을 통해 해결해보자.

const storageHandler = (key) => (action) => (itemOrPredicate) => {
  const get = () => {
    return storage.getItem(key);
  };

  const set = (data) => {
    storage.setItem(key, data);
  };

  //...

  switch (action) {
    case 'get':
      return get();
    case 'set':
      return set(itemOrPredicate);
    //...
  }
};

// 각각의 인스턴스 생성 후 액션 생성

const personStorage = storageHandler('person');
const restaurantStorage = storageHandler('restaurant');
const foodStorage = storageHandler('food');

const getRestaurantData = restaurantStorage('get');
const getPersonData = personStorage('get');
const getFoodData = foodStorage('get');

const setRestaurantData = restaurantStorage('set');
const setPersonData = personStorage('set');
const setFoodData = foodStorage('set');

이런식으로 각각의 인스턴스를 커링을 통해 생성해주고 스토리지 관련된 공통 된 로직을 재사용할 수 있다.

커링을 사용하지 않은 예시에서는 스토리지와 관련된 로직이 수정되어야한다면 각각의 인스턴스에서 수정 작업을 해주어야한다.

하지만 커링을 활용한다면 한 곳에서 수정 작업만 이루어진다면 전체 모듈에 수정사항을 적용시킬 수 있다.

무조건 커링을 사용해야할까?

사실 위의 예시들은 커링이 아니라 다른 방법으로도 충분히 재사용성을 높일 수 있다고 생각한다.

첫번째 예시는 가장 작은 단위의 유효성 함수들로 분리하여 validate 안에서 실행시켜볼 수 있다.

두번째 예시 또한 각각의 객체를 만들어서 그 안에서 추상화 된 storageHandler를 불러와도 상관없다.

심지어 어플리케이션의 규모가 크지 않다면 위의 방법이 더욱 효과적일수 있다.

그렇다면 커링은 왜 탄생한걸까?

커링과 클로저

자바스크립트에서 함수는 일급 객체이다.

일급 객체가 가지고 있는 여러개의 특성이 있다.

  1. 변수에 할당할 수 있다.

  2. 함수의 인수로 전달할 수 있다.

  3. 함수의 반환 값으로 사용할 수 있다.

위의 특성들 때문에 함수가 실행이 될때 해당 컨텍스트를 기억하는 클로저가 가능하고 이 클로저를 활용하여 커링을 구현할 수 있다.

커링은 위처럼 함수를 일급객체로 다루는 자바스크립트의 특성을 이용하여 탄생한 기법이고 함수형 프로그래밍에서만 가능하다.

말인 즉슨, 커링은 함수형 프로그래밍의 생태계에서 탄생한 하나의 방법론일뿐이지 정답은 아니라는 것이다.

객체 지향 프로그래밍에서 클래스뿐 아니라 인터페이스나 추상 클래스 같은 다른 방법도 있듯이, 커링 또한 함수형 프로그래밍에서 재사용성을 높이고 함수들을 조합하여 사용할 수 있는 방법 중 하나이다.

그렇기 때문에 적절한 상황에서 알맞게 적용해야한다.