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)를 동적으로 결정해줄 수 있다.
즉, 함수의 인자를 나누어서 각각의 함수로 분리가 가능하다.
라고 볼 수 있다.
하지만 이러한 예시로는 커링의 실질적인 장점이 와닿지 않았다.
더 와닿는 실질적인 예시가 필요했다.
각각의 필드에 관한 유효성 검증을 진행한다고 가정해보자.
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);
위처럼 여러개의 조건식을 간단히 처리해줄수 있고, 작은 함수들로 나누어져있는 상태이기때문에 디버깅과 테스트의 용이성도 갖고 있다.
위의 예시로도 커링의 장점을 파악할 수 있지만, 사실 이는 가장 작은 함수들로 나누어주어도 상관이 없는 로직이다.
더욱 실질적인 예시가 없을까?
만약 음식점과 관련 된 기능을 구현해야 한다고 가정해보자.
우리가 필요한것은 사람, 식당, 음식에 관한 데이터이고 이를 어떠한 스토리지에서 관리해야한다.
이를 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
를 불러와도 상관없다.
심지어 어플리케이션의 규모가 크지 않다면 위의 방법이 더욱 효과적일수 있다.
그렇다면 커링은 왜 탄생한걸까?
자바스크립트에서 함수는 일급 객체이다.
일급 객체가 가지고 있는 여러개의 특성이 있다.
변수에 할당할 수 있다.
함수의 인수로 전달할 수 있다.
함수의 반환 값으로 사용할 수 있다.
위의 특성들 때문에 함수가 실행이 될때 해당 컨텍스트를 기억하는 클로저
가 가능하고 이 클로저를 활용하여 커링
을 구현할 수 있다.
커링은 위처럼 함수를 일급객체로 다루는 자바스크립트의 특성을 이용하여 탄생한 기법이고 함수형 프로그래밍
에서만 가능하다.
말인 즉슨, 커링
은 함수형 프로그래밍의 생태계에서 탄생한 하나의 방법론일뿐이지 정답은 아니라는 것이다.
객체 지향 프로그래밍에서 클래스뿐 아니라 인터페이스나 추상 클래스 같은 다른 방법도 있듯이, 커링 또한 함수형 프로그래밍에서 재사용성을 높이고 함수들을 조합하여 사용할 수 있는 방법 중 하나이다.
그렇기 때문에 적절한 상황에서 알맞게 적용해야한다.