함수형 프로그래밍 - Pipe
Pipe란 무엇이고 어떻게 사용할까?
만약 코드를 작성하고 있는데 모든 것을 하나의 기능에 맞추기에는 너무 복잡해진다면, 여러분은 코드를 더 우아하게 만들기 위해 무엇을 할 수 있나요?
한 가지 해결책은 로직을 분리하기 위해 여러 기능을 만드는 것이지만, 더 우아한 방법이 없을까요?
TC39 GitHub의 1단계에서 한 가지 흥미로운 제안은 이 게시물에서 논의할 주제와 관련이 있습니다. 아직 미완성이지만 읽을 가치가 있는 좋은 제안입니다.
이 게시물에서, 저는 "Pipe" 또는 "Pipeline"이라는 기능적 프로그래밍에 사용되는 기술을 소개할 것입니다.
자바스크립트의 파이프 정의 먼저, 사용 방법을 논의하기 전에 "Pipe"가 무엇인지 설명하겠습니다. "Pipe"라는 단어를 들으면 가장 먼저 떠오르는 것은 무엇인가요? 천장에 연결된 가스관이나 지하에 매설된 수도관을 생각해보셨을 겁니다.
이러한 파이프는 일반적으로 A 지점에서 B 지점으로 리소스를 전송하는 데 사용됩니다. 따라서 단순한 터널과 같은 개념이라고 생각할 수 있습니다.
Pipe 기능은 실생활에서 사용되는 "파이프"의 아이디어에서 영감을 얻었습니다.
Pipe는 단방향 통신에 사용됩니다. 각 파이프는 이전 파이프에서 전달된 결과를 모수로 사용하여 다른 결과를 생성합니다.
순수 함수 Pipe를 이해하려면 먼저 순수 함수에 대해 배워야 합니다. 순수 함수는 다음 규칙을 따르는 함수입니다
- 동일한 입력 값에 대해 동일한 반환 값을 보장합니다.
- 함수의 범위를 벗어나는 변수의 값은 변경되지 않습니다.
하지만 파이프에서 순수한 기능이 중요한 이유는 무엇일까요?
위에서 언급한 것처럼 한 파이프에서 반환되는 값은 다음 파이프에 입력 값으로 전달됩니다. 그러나 파이프가 상황에 따라 다른 값을 반환하는 경우 다음 파이프의 입력 값은 상황에 따라 변경될 수 있는 불안정한 값이 됩니다.
결과적으로 반환값도 불안정할 가능성이 높습니다.
Without Using Pipe
4를 추가하고 5를 뺀 두 가지 기능이 있습니다. 각 함수에서 쉽게 반환 값을 얻을 수 있습니다. 두 함수를 모두 사용하여 단일 반환 값을 얻으려면 다음과 같이 쓸 수 있습니다.
addFour(minusFive(0)); // -1
multiplyByTen(addFour(minusFive(0))); // -10
만약 여러분이 음수를 좋아하지 않고 결과의 절대값을 취하고 싶다면 어떻게 해야 할까요?
Math.abs(multiplyByTen(addFour(minusFive(0)))); // 10
이 과정을 보면 어떻게 생각하나요? 좀 지저분해 보이지 않나요?
특히 마지막에 괄호가 닫히는 부분을 보면 머리가 빙빙 돌 수 있어요.
물론 실행 과정에서 오류는 없었습니다. 하지만, 코드를 좀 더 깨끗하게 쓸 수 있을 것 같습니다.
How to Create a Pipe
파이프라인을 만들려면 먼저 몇 가지 기능이 있어야 합니다. 그리고 Array 클래스 안에는 reduce라고 불리는 매우 유용한 방법이 있습니다. reduce 는 array를 통과하여 궁극적인 단일 반환 값을 생성하는 것입니다.
예를 들어, 모든 학생들의 총점을 알고 싶다면, 다음과 같이 함수를 쓸 수 있습니다.
const scores = [90, 100, 40, 50, 10];
let total = 0;
scores.forEach(score => total += score);
console.log(total); // 290
여기서는 forEach를 사용했습니다. map 및 filter 와 같은 기능은 새 배열을 생성하고 반환하기 때문에 이 예제에는 적합하지 않습니다.
그러나 이 예제는 몇 가지 문제가 있습니다. 각 항목에 대해 값이 계속 할당되기 때문에 변수 합계는 let 키워드를 사용하여 선언해야 합니다.
여기서 reduce를 사용하면 하나의 결과를 얻을 수 있습니다.
const scores = [90, 100, 40, 50, 10];
const total = scores.reduce((acc, cur) => acc + cur, 0);
console.log(total); // 290
1. reduce 에 포함할 모든 기능을 포함할 수 있도록 합니다.
const pipe = (funcs) => {
return funcs.reduce((acc, cur) => {
...
});
}
2. 기능 내부를 채웁니다.
funcs.reduce((res, func) => {
return func(res);
});
3. 초기 값을 설정합니다.
const pipe = (funcs, v) => {
return funcs.reduce((res, func) => {
return func(res);
}, v);
};
이제 이 기능이 제대로 작동하는지 확인해봅니다.
const addFive = v => v + 5;
const identity = v => v;
console.log(pipe([
addFive,
identity
], 5));
// 10
4. 일반화 하기
단일 함수도 요인으로 받을 수 있도록 파이프를 수정하는 것이 좋습니다. 이 단계에서는 Rest 매개 변수를 사용합니다.
Rest 매개 변수는 함수가 형식 제한 없이 하나의 배열에 있는 모든 매개 변수를 수신하는 매개 변수를 의미합니다.
const pipe = (v, ...funcs) => {
return funcs.reduce((res, func) => {
return func(res);
}, v);
};
const subtract = v => v - 5;
console.log(pipe(10, subtract)); // 5
여기서는 Closer를 사용합니다. Closer 것은 이미 실행된 외부 기능의 속성이 참조할 수 있는 내부 기능임을 의미합니다.
const pipe = (...funcs) => v => {
return funcs.reduce((res, func) => {
return func(res);
}, v);
};
pipe(add)(5) // 10
Finally. Test
const minusFive = v => v - 5;
const addFour = v => v + 4;
const multiplyByTen = v => v * 10;
const identity = v => v;
const res = pipe(
minusFive,
addFour,
multiplyByTen,
Math.abs,
identity
)(0);
console.log(res); // 10.
Pipe는 기능 프로그래밍에서 매우 유용한 기술입니다. 그는 기능의 복잡성을 줄이고 가독성을 높이는 매우 훌륭한 사람입니다.
그러나 한 가지 기억해야 할 점은 Pipe에 사용되는 모든 기능은 순수한 기능 상태에서 이루어져야 한다는 것입니다.
Composing Functions with Pipe
앞 부분에서는 함수 배열을 하나의 함수로 구성하는 단순 파이프 함수를 만드는 방법에 대해 알아봤습니다. 또한 rest 매개 변수를 사용하여 어레이와 개별 기능을 모두 전달할 수 있도록 함으로써 보다 일반적이고 유연하게 만드는 방법도 확인했습니다.
이 파트에서는 파이프를 사용하여 함수를 구성하는 몇 가지 고급 기술에 대해 알아보겠습니다.
- Composing Functions with Arguments
지금까지 Pipe 함수는 단일 인수를 사용하는 함수만 구성합니다. 하지만 만약 우리가 여러 개의 인수를 받는 기능을 가진다면 어떨까요? 우리는 이 상황을 처리하기 위해 스프레드 연산자를 사용할 수 있습니다.
예를 들어, 다음과 같은 기능이 있다고 가정해 보겠습니다.
const add = (a, b) => a + b; const square = a => a * a;
스프레드 연산자를 사용하여 다음과 같은 함수를 구성할 수 있습니다.
const addAndSquare = pipe( ([a, b]) => add(a, b), square );
addAndSquare([2, 3]); // 25
여기서는 두 개의 숫자 배열을 두 개의 함수로 구성된 addAndSquare 함수로 전달합니다. 첫 번째 함수는 두 숫자의 배열을 취하여 함께 더하고, 두 번째 함수는 결과를 제곱합니다.
- Composing Async Functions
만약 우리가 비동기 함수를 구성하고 싶다면요? 비동기 함수는 값이 아닌 Promise를 반환하므로 여기서 reduce를 사용할 수 없습니다. 대신 async/await 를 사용해야 합니다.
예를 들어 다음과 같은 비동기 함수가 있다고 가정해 보겠습니다.
const fetchData = async url => {
const response = await fetch(url);
const data = await response.json();
return data;
};
const filterData = async (data, filterFunc) => { return data.filter(filterFunc); };
다음과 같이 함수를 구성할 수 있습니다.
const composedFunc = pipe( fetchData, data =>
filterData(data, item => item.completed),
data => data.map(item => item.title)
);
const url = 'https://jsonplaceholder.typicode.com/todos';
const filteredData = await composedFunc(url);
여기서는 async/await 구문을 사용하여 fetchData 및 filterData 함수를 API에서 데이터를 가져와 완료된 상태로 필터링하고 제목 배열을 반환하는 단일 함수로 구성합니다.
- Using Ramda’s Pipe Function
지금까지, 우리는 우리만의 Pipe 기능을 처음부터 만들어 왔습니다. 그러나 Pipe의 보다 강력하고 기능이 풍부한 구현을 제공하는 라이브러리가 있습니다. 그러한 라이브러리 중 하나는 Ramda입니다.
Ramda는 자바스크립트를 위한 기능성 프로그래밍 라이브러리로 데이터 작업에 유용한 많은 기능을 제공합니다. 이러한 기능 중 하나는 Pipe입니다. Pipe는 사용자 지정 구현과 유사한 방식으로 작동하지만 몇 가지 추가 기능이 있습니다.
다음은 Ramda의 Pipe 함수를 사용하는 방법의 예입니다.
import { pipe, filter, map, prop } from ‘ramda’;
const filterByCompleted = filter(prop('completed'));
const getTitle = map(prop('title'));
const composedFunc = pipe( fetchData, filterByCompleted, getTitle );
const url = 'https://jsonplaceholder.typicode.com/todos';
const filteredData = await composedFunc(url);
여기서는 Ramda의 filter, map 및 prop 기능을 사용하여 API에서 데이터를 가져와 완료된 상태로 필터링하고 일련의 제목을 반환하는 단일 기능으로 기능을 구성합니다.
Ramda의 Pipe 함수에는 다양한 함수 지원, 자리 표시자 값 등과 같은 많은 추가 기능이 있습니다.
결론
Pipe 를 사용하여 함수를 구성하는 것은 코드를 단순화하고 더 모듈화되고 재사용 가능하도록 만들 수 있는 강력한 기술입니다. 여러 인수로 함수를 처리하고 구성하는 등의 고급 기술을 사용할 수 있습니다.