다음과 같은 상황이 있을 때 코드를 어떻게 작성할지 생각해보자.
rating이 4점 이상인 상품들 중에서 가장 비싼 상품의 이름을 구하라!
다음은 filter, sort에 익숙하다면 작성할만한 코드다.
// https://dummyjson.com/products에서 가져온 데이터임
interface Product {
id: number;
title: string;
description: string;
price: number;
discountPercentage: number;
rating: number;
stock: number;
brand: string;
category: string;
thumbnail: string;
images: string[];
}
// 1번 코드
const sortedProducts = products
.filter((product) => product.rating >= 4)
.sort((a, b) => a.price > b.price
const title = sortedProducts[0].title;
// 2번 코드 더 짧은 코드
const title = products
.filter((product) => product.rating >= 4)
.sort((a, b) => a.price > b.price)[0].title;
위 코드들은 사소한 문제점이 있다. 1번에서는 title을 얻기위해 쓸데없이 sortedProducts
라는 변수를 만들었다. 개발자는 변수를 만들기 위해 변수의 이름을 지어야한다.
2번에서는 코드 계층상의 문제가 있다. filter, sort로 고차함수들이 나타나다가 그 후에는 [0]
, title
으로 배열에 접근하기도 하고 프로퍼티에 접근한다. 이는 코드를 읽기 어렵게 만든다.
Pipe로 이를 더욱 단순화할 수 있다. Pipe가 무엇이고 어떻게 코드를 더욱 개선시킬 수 있을까?
프로그래밍에서 Pipe는 다음과 같다.
Pipe는 프로그램의 output이 다른 프로그램의 input으로 전달되는 매커니즘을 말한다.
Unix에서도 Pipe를 쉽게 찾아볼 수 있다. |
operator로 동작한다.
# ls의 결과물을 grep의 인자로 넘긴다.
ls | grep .txt
이 예시에서는 ls
의 결과가 grep
커맨드로 Pipe
된다. 결과적으로 ls
의 결과에서 .txt
를 모두 찾는다.
grep : 파일이나 standard input에서 특정 패턴을 찾아주는 커맨드라인 명령어
Pipe
는 여러 프로그램을 묶어서 복작한 작업을 수행할 수 있게 한다. 이는 Unix뿐만 아니라 다른 많은 운영체제나 프로그래밍언어에서 쓰이는 개념이다.
ECMASCRIPT
에서도 pipe
연산자를 정식으로 포함시키자는 의견이 있다. 아직 Stage2(proposal) 단계다. 다음은 proposal에서 작성된 파이프 연산자의 예시다.
// Status quo
console.log(
chalk.dim(
`$ ${Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')}`,
'node',
args.join(' ')
)
);
// With pipes
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
|> console.log(%);
pipe
를 이용해서 가독성이 향상됨을 느낄 수 있다. 하지만 아직 파이프 연산자가 없으니 간단히 구현해서 사용해보자.
function pipe(...funcs) {
return (initial)=> funcs.reduce((result, func) => {
return func(result);
}, initial);
}
pipe
구현은 정말 간단하다. 인자로는 첫 pipe의 input으로 쓰일 것과 나머지는 pipe를 구성하는 함수들이 있다. 그런다음 내부에서 reduce
로 함수들을 실행시키면서 값을 계속 전달하면 끝이다.
// 사용예시
// person의 나이를 출력하라
const person = {name:"kyo", age:20,birthDay:'05-04', hobby:"game"}
const getAge = pipe((p)=> p.age, console.log)
getAge(person)
// 출력결과: 20
직접 구현한 pipe 함수에는 아쉬운 점이 있다. 인자를 하나밖에 취하지 못한다. 여러 인자를 받을 수 있으면 더욱 편하다. 다른 언어(Go)에서는 함수의 결과값을 여러 개로 리턴하는 기능이 있다. 이런 기능을 Multiple Result라고 한다. Javascript에는 이런 기능이 없지만 pipe
함수를 조금 변경하면 구현할 수 있다.
function multipleResult() {
arguments.mr = true;
return arguments;
}
function pipe2(...funcs) {
return (initial) =>
funcs.reduce((result, func) => {
if (result.mr) {
return func(...result);
}
return func(result);
}, initial);
}
const person = { name: "kyo", age: 20, birthDay: "05-04", hobby: "game" };
const printAgeAndName = pipe2((p) => multipleResult(p.age, p.name), console.log);
printAgeAndName(person)
// 출력결과 : 20 kyo
처음으로 돌아와서 rating이 4점 이상인 상품들 중에서 가장 비싼 상품의 이름을 구하라!
를 Pipe
를 이용해서 해결해보자.
// 기존코드
const sortedProducts = products
.filter((product) => product.rating >= 4)
.sort((a, b) => a.price > b.price
const title = sortedProducts[0].title;
// pipe를 적용한 코드
// util함수 작성
const _ = {
filter(predicate) {
return (arr) => arr.filter(predicate);
},
sort(compareFn) {
return (arr) => arr.sort(compareFn);
},
getVal(key) {
return (obj) => obj[key];
},
getFirst(arr) {
return arr[0];
},
};
const getBestProductTitle = pipe(
_.filter((product) => product.rating > 4),
_.sort((a, b) => a > b),
_.getFirst,
_.getVal("title"),
console.log
);
getBestProductTitle(products);
// 출력결과 : iPhone 9
한 호흡에 읽히기도하고 코드 계층을 같게 두어서 기존코드보다 코드를 이해하는데 쉬워졌다. 그리고 불필요한 변수들이 사라졌다. 이름짓기의 고통을 조금이라도 줄일 수 있다. 만약 한번만 쓰일 함수라면 pipe의 결과물인 getBestProductTitle
도 불필요한 변수라고 생각할 수 있다. 그럴 때는 즉시실행하는 함수인 go
함수를 사용한다.
function go(initial,...funcs) {
return funcs.reduce((result, func) => {
return func(result);
}, initial);
}
const bestProductTitle = go(
products,
_.filter((product) => product.rating > 4),
_.sort((a, b) => a > b),
_.getFirst,
_.getVal("title"),
console.log
);
pipe함수는 더욱 고도화 될 부분들이 남아있다. 현재 pipe 함수는 tc39 proposal 처럼 원하는 곳에 인자들을 위치시키지 못한다. javascript로 이런 부분도 구현이 가능하다. partial.js의 더 나은 부분 적용 (Partial application)을 살펴보면 그 기능들을 볼 수 있다.
// 예시
jQuery.merge( this, jQuery.parseHTML(
match[ 1 ],
context && context.nodeType ? context.ownerDocument || context : document,
true
) );
// With pipes
context
|> (% && %.nodeType ? %.ownerDocument || % : document)
|> jQuery.parseHTML(match[1], %, true)
|> jQuery.merge(%);
실무에서도 pipe
는 충분히 쓸 수 있을 것 같다. pipe를 쓰다보면 의식적으로 함수를 작게 나누려고 한다. 함수를 작게 나누면 유지보수성과 테스트에도 이점이 있으니 pipe
도입을 생각해보자.
도움을 받은 글들
함수형 자바스크립트 프로그래밍 - 유인동
https://www.freecodecamp.org/news/pipe-and-compose-in-javascript-5b04004ac937/
https://velog.io/@hoi/Pipe-function%EA%B3%BC-lodash.debounce-%EC%A0%81%EC%9A%A9%EA%B8%B0
글 잘 봤습니다
적용하기 챕터에서 util 함수를 작성하실 때
_
로 변수 이름을 선언하셨는데요,_
로 util 함수를 선언하는 것은 자주 쓰이는 코딩 컨벤션인가요? 맞다면 어떤 키워드로 검색하면 해당 컨벤션에 대해 찾아볼 수 있을까요?