[함수형 자바스크립트] Pipe로 가독성을 향상시키자

금교영·2023년 2월 25일
3
post-thumbnail

문제상황

다음과 같은 상황이 있을 때 코드를 어떻게 작성할지 생각해보자.

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는 다음과 같다.

Pipe는 프로그램의 output이 다른 프로그램의 input으로 전달되는 매커니즘을 말한다.

Unix에서도 Pipe를 쉽게 찾아볼 수 있다. | operator로 동작한다.

# ls의 결과물을 grep의 인자로 넘긴다. 

ls | grep .txt

이 예시에서는 ls의 결과가 grep 커맨드로 Pipe된다. 결과적으로 ls의 결과에서 .txt를 모두 찾는다.

grep : 파일이나 standard input에서 특정 패턴을 찾아주는 커맨드라인 명령어

Pipe는 여러 프로그램을 묶어서 복작한 작업을 수행할 수 있게 한다. 이는 Unix뿐만 아니라 다른 많은 운영체제나 프로그래밍언어에서 쓰이는 개념이다.

Javascript에서 Pipe 만들기

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

https://betterprogramming.pub/functional-programming-and-the-pipe-function-in-javascript-c92833052057

https://www.techtarget.com/whatis/definition/pipe

profile
SW Engineer를 꿈꾸는 👨‍🌾

2개의 댓글

comment-user-thumbnail
2023년 7월 29일

글 잘 봤습니다
적용하기 챕터에서 util 함수를 작성하실 때 _로 변수 이름을 선언하셨는데요, _로 util 함수를 선언하는 것은 자주 쓰이는 코딩 컨벤션인가요? 맞다면 어떤 키워드로 검색하면 해당 컨벤션에 대해 찾아볼 수 있을까요?

1개의 답글