: if-else 이런거 안쓰는게 함수형에서는 지향하는 바인 것은 알겠는데 그럼 pipe, curry, partial 이런걸로만 세세한 처리를 한다는건가 ?
: 조합기란 함수 또는 다른 조합기 같은 기본 장치를 조합하여 제어 로직처럼 작동시킬 수 있는 고계함수이다.
: 항등은 사실 A -> A 이다
처럼 너무 당연한 무언가다(?). A를 넣으면 A가 나오는 처리 로직을 의미하기 때문이다. 사실 이걸 어디에 쓸 수 있을까? 라는 생각을 나도 했었는데, 생각해보니 이런 방향으로 써볼 수 있을 것 같다.
// 이런 경우는 identity가 유용
interface Options<T> {
transform?: (data: T) => T; // 함수를 받는 상황
}
// Good: transform이 없으면 identity 함수 사용
const processData = <T>(data: T, options: Options<T> = {}) => {
const transform = options.transform ?? identity;
return transform(data);
}
// Bad: 이렇게 하면 타입 에러나 런타임 에러 발생 가능
const processData = <T>(data: T, options: Options<T> = {}) => {
return options.transform?.(data) ?? data; // 타입스크립트가 싫어할 수 있음
}
: 이건 DOM API를 호출하는 returnType이 void인 함수를 Pipe 라인 중간에(혹은 compose) 쓰고자 하거나, 중간에 로그를 출력하면서 디버깅 혹은 테스팅을 하고 싶을 때 사용하면 유용한 처리 로직이다. 사실 나도 함수형 프로그래밍을 배운 뒤에 pipe문을 쓰다가 디버깅을 어떻게 하지..? 라는 생각을 자주 했었다. 근데 생각해보면, 꼭 이렇게 함수 조합기가 있어야만 뭔가를 할 수 있는게 아니다. 사실 탭 조합기도 그냥 직접 함수로 만들면 상당히 간단한 함수고, 함수형 프로그래밍은 이러한 아이디어로 만든 함수들을(최대한 순수함수로) 조합해서 하는 프로그래밍일 뿐이다.
const tap =
<T>(fn: (x: T) => void) =>
(x: T): T => {
fn(x);
return x;
};
위 tap 함수를 가지고
const objectUrl = await asyncPipe(
imageUploadService.getPresignedUrl,
tap((state) => console.log(state)),
imageUploadService.uploadToS3,
tap((state) => console.log(state)),
imageUploadService.handleSuccess,
)(initialState);
이런식으로 중간중간 값이 잘 전달되고 있는지를 확인할 수 있는 로그를 출력할 수 있다. 추가로 예를 들어, input value를 받아서 validation 검사를 하고, 틀린게 있으면 마지막에 errorMessage를 업데이트 해주는(UI를 그리는) 로직을 pipe 로 짠다고 하면 UI를 업데이트하는 로직에는 return 값이 없을테니(void) tap을 사용하면 UI를 그리는 작업만 하고, 그대로 값을 전달할 수 있다(순서가 꼭 그 중간 어디에 있어야할 때 쓰면 좋을듯).
: 특정 함수를 실행하고 난 다음에 return 값이 없으면 그에 대한 후속처리 함수를 대신 실행하도록 하려면 ? '선택 조합기'를 쓰면 된다. 무슨 말인가하면, 예를 들어, 특정 사원의 데이터를 조회하는 함수가 있다고 했을 때, 특정 사원의 사원 ID를 매개변수로 조회했을 때, 조회 결과 데이터가 없다면, 그 ID로 데이터를 생성하도록 하려고 할 때 쓰면 좋을 것 같다.
export const alt = (func1: (param: any) => any, func2: (param: any) => any) => {
return (param: any) => {
return func1(param) || func2(param);
};
};
이런식으로 alt 조합기 함수를 만들었을 때, 화면에 사원 정보를 출력하는 로직을 pipe를 이용해서 만들어보자(세부적인 함수 내용은 생략)
const showEmployee = pipe(alt(findEmployee, createNewEmployee), convertToUIData, append('#employee-info'))
showEmployee('4241fdsf34$dfef');
alt(findEmployee, createNewEmployee)
여기서 볼 수 있듯이 만약 회사원 정보가 없다면 새롭게 정보를 만들어서 리턴하도록 했다. 그러면 그 정보를 받아서 UI 출력에 맞게 데이터를 포매팅하고 convertToUIData
마지막으로 실제로 append 하도록 했다(append('#employee-info')
). 이런식으로 선택 조합기를 활용할 수 있다.
const seq = (...funcs: any[]) => {
return (val: any) =>
funcs.forEach((fn) => {
fn(val);
});
};
예를 들어, 같은 매개변수로 콘솔 출력도 하고 싶고, 특정 로직도 돌리고 싶을 때 써주면 좋다. 이 때, 순차열 조합기의 경우 이전 매개변수를 전달하는 역할은 없기 때문에 그런 역할까지 같이 쓰고 싶다면 tap과 seq를 같이 써주면 된다.
: 마지막으로 포크 or 조인은 다음과 같은 세개의 인자를 받는다.
join(fork1(A), fork2(A))
이런 원리로 생각하면 된다. 쉬운 예시로 평균값을 구하는 함수를 fork, join으로 만들어보자.
const fork = (join: any, func1: any, func2: any) => {
return (val: any) => join(func1(val), func2(val));
};
const arrSum = (arr: any[]) => arr.reduce((prev, next) => prev + next); // fork 1
const arrLength = (arr: any[]) => arr.length; // fork 2
const divideValues = (a: any, b: any) => a / b;
const arrAverage = pipe(fork(divideValues, arrSum, arrLength), console.log);
arrAverage([1,2,3,4,5,6]); // 3.5
결론적으로 fork 내부에서는
divideValues(arrSum([1,2,3,4,5,6]), arrLength([1,2,3,4,5,6]))
이러한 과정이 이뤄진걸로 볼 수 있다.
: 사실 함수형 프로그래밍을 할 때 저러한 조합기(?)를 자신의 아이디어로 만들면서 진행해도 되고, 위의 조합기들을 또 조합해서 뭔가를 만들어낼수도 있다. 따라서, 함수형 프로그래밍은 저러한 조합기를 써야하는거구나 라는 마인드보다는 저러한 아이디어로 하나의 '논리'를 만들어낼 수 있고, 순수함수로 만들었기 때문에 범용적으로 쓸 수 있겠구나. 그리고 나도 저러한 조합기들을 만들어서 쓸 수 있겠다 하는 마인드로 공부하면 좋을 것 같다.