Skip to content

Latest commit

 

History

History
275 lines (203 loc) · 10.5 KB

File metadata and controls

275 lines (203 loc) · 10.5 KB

부록. 클래스 없이 코딩하기

목차

  • 나머지 매개변수와 전개 연산자, 그리고 부분 적용
  • 장바구니
  • 정리

나머지 매개변수와 전개 연산자, 그리고 부분 적용

함수형 프로그래밍에서 로직을 구체화하는 기법에는 크게 세 가지가 있습니다.

  1. 함수들이 연속적으로 실행되도록 조합
  2. 보조 함수를 고차 함수에 적용
  3. 부분 적용

위 세 가지 중에 1과 2는 앞서 어느정도 확인했었습니다. 그렇다면 3은 무엇일까요. 3은 커링 혹은 부분 적용을 통해 인자들에 함수를 부분적으로 적용시키는 것을 말합니다. ES6+에서 부분 적용은 어떻게 할까요? ES6+은 나머지 매개 변수와 전개 연산자, 그리고 화살표 함수 등의 문법이 간결하고 강력하여 굉장히 다양한 경우의 부분 적용을 구현할 수 있습니다.

기본적인 사례를 보면 다음과 같습니다.

function sub(a, b) {
  return a - b;
}

const sub10 = _ => sub(_, 10);
console.log( sub10(15) ); // 5
console.log( sub10(30) ); // 20

화살표 함수를 이용하여 항상 10을 빼는 함수를 만들었습니다. sub의 두 번째 인자에 항상 10이 전달되게 됩니다.

이번엔 subAll을 만든 후 부분 적용을 해보겠습니다.

function sub(a, b) {
  return a - b;
}

const subAll = (...args) => reduce(sub, args);

console.log( subAll(30, 10, 5) );
// 15

const subAllAndSub5 = (..._) => subAll(..._, 5);

console.log( subAllAndSub5(30, 10) );
// 15
console.log( subAllAndSub5(30, 10, 5) );
// 10

나머지 매개 변수와 전개 연산자를 활용하면 위와 같이 유연한 부분 적용이 가능합니다.

다음은 나올 수 있는 다양한 부분 적용 패턴들을 확인하기 위한 예제입니다.

const f1 = _ => console.log(1);
f1();
// 1

const f2 = (..._) => console.log(1, ..._);
f2(2, 3);
// 1 2 3

const f3 = (..._) => console.log(1, ..._, 4);
f3(2, 3);
// 1 2 3 4
f3(2);
// 1 2 4
f3();
// 1 4

const f4 = (..._) => console.log(..._, 4, 5);
f4(1, 2, 3);
// 1 2 3 4 5
f4();
// 4 5

const f5 = (..._1) => (..._2) => console.log(..._1, 4, ..._2, 7);
f5(1, 2, 3)(5, 6);
// 1 2 3 4 5 6 7

위 문법들을 확인했다면, 다른 함수들과의 조합을 통해 더 많은 것들을 할 수 있습니다.

장바구니

장바구니의 담긴 상품들의 정보를 계산하는 로직을 클래스/객체 조합이 아닌 함수 조합으로 구현하면서 모듈화해보는 예제입니다. 함수 조합의 아이디어를 확인하기위해 최대한 작고 잘게 조개는 코드로 작성된 예제입니다.

const p1 = {
  id: 1,
  selected: true,
  name: 'A 반팔티',
  price: 12000,
  discount: 2000,
  quantity: 3
};

// map에 함수를 하나만 전달하여 자동 부분 적용
const gets = map(key => a => a[key]);

// gets 함수를 통해 객체의 key에 할당된 값을 얻어오는 함수들 생성
const [price, discount, quantity, selected] =
  gets(['price', 'discount', 'quantity', 'selected']);

console.log( price(p1) ); // 12000
console.log( discount(p1) ); // 2000
console.log( quantity(p1) ); // 3

priceconst price = p => p.price와 같습니다.

const add = (a, b) => a + b;
const sub = (a, b) => a - b;
const mult = (a, b) => a * b;

const baseCalcF = calc => (...fs) => a => go(
  map(f => f(a), fs),
  reduce(calc));

                // 5 + 10, 5 + 20
baseCalcF(add)(a => a + 10, a => a + 20)(5);
// 40

baseCalcF는 세 번째 실행에서 받은 인자인 5를, 두 번째 실행에서 받은 함수들을 적용한 배열로 만든 후, 해당 배열을 첫 번째 실행에서 받은 add 함수로 축약합니다.

const subF = baseCalcF(sub);
const multF = baseCalcF(mult);

subF(a => a + 10, a => a + 20)(0);
// -10
multF(a => a + 10, a => a + 20)(0);
// 200
multF(a => a + 10, a => a + 20)(10);
// 600

baseCalcF에서 축약에 사용할 함수를 전달하여 위와 같이 사용할 수 있습니다.

아래는 상품 하나를 다루는 함수들입니다.

// 네임 스페이스 - 상품에 대한 함수들
const pdt = {};

// 할인된 금액
pdt.discountedPrice = subF(price, discount);
console.log( pdt.discountedPrice(p1) ); // 10000

// 기본 금액 x 수량
pdt.totalPrice = multF(price, quantity);
console.log( pdt.totalPrice(p1) ); // 36000

// 할인된 금액 x 수량
pdt.discountedTotalPrice = multF(pdt.discountedPrice, quantity);
console.log( pdt.discountedTotalPrice(p1) ); // 30000

pdt 네임 스페이스에 필요한 함수들을 subFmultF를 이용해 만들었습니다. subF(price, discount)의 결과인 pdt.discountedPrice 함수는 p1로부터 p1.pricep1.discount를 추출한 배열을 만든 후 sub로 축약하며, multF도 동일하게 동작합니다. pdt.discountedTotalPrice는 동일 로직을 중첩하여 조합하였습니다.

아래는 상품 목록을 다루는 함수들입니다.

const products = [
  { id: 1, selected: true, name: 'A 반팔티', price: 12000, discount: 2000, quantity: 3 },
  { id: 2, selected: false, name: 'B 후드티', price: 30000, discount: 0, quantity: 3 },
  { id: 3, selected: true, name: 'C 폰케이스', price: 20000, discount: 0, quantity: 2 },
  { id: 4, selected: false, name: 'D 반팔티', price: 12000, discount: 2000, quantity: 4 },
  { id: 5, selected: true, name: 'E 쿠션', price: 25000, discount: 5000, quantity: 2 }
];

// 네임 스페이스 - 상품들에 대한 함수들
const pdts = {};

pdts.total = (getter, products, ...before) => go(
  products,
  ...before, // <--- before에 걸리는 함수가 있으면 여기에 추가될 예정
  map(getter),
  reduce(add));

// 모든 상품 수량을 뽑기 위해 pdts.total의 getter로 사용할 함수로 수량을 뽑는 quantity를 이용
console.log( pdts.total(quantity, products) );
// 14

// 부분 적용을 이용해 다양한 함수를 더 간결하게 생성하고 사용하도록 만들기
Object.assign(pdts, {
  quantity: _ => pdts.total(quantity, _),
  price: _ => pdts.total(pdt.totalPrice, _),
  discountedPrice: _ => pdts.total(pdt.discountedTotalPrice, _)
});

// 모든 상품 수량
console.log( pdts.quantity(products) ); // 14

// 모든 상품 기본 금액 x 수량
console.log( pdts.price(products) ); // 264000

// 모든 상품 할인된 금액 x 수량
console.log( pdts.discountedPrice(products) ); // 24000

위에서는 pdts.total를 만든 후 해당 함수에 원하는 함수를 조합하면서, 모든 상품에 해당하는 각각의 값들을 계산하는 함수들을 함수 조합을 통해 만들었습니다.

아래는 pdts.totalbefore를 전달하거나, pipe를 이용하여 선택된 상품들에 해당하는 각각의 값들을 계산하는 함수들을 만들었습니다.

// 세 번째 인자에 `filter(selected)`를 전달하여 선택된 상품의 수량만 합산
console.log( pdts.total(quantity, products, filter(selected)) ); // 7

pdts.selected = {
  // pdts.total의 before에 ...filter(selected) 추가
  quantity: _ => pdts.total(quantity, _, filter(selected)),
  price: _ => pdts.total(pdt.totalPrice, _, filter(selected)),
  // pipe로도 확장 가능
  discountedPrice: pipe(filter(selected), pdts.discountedPrice)
};

// 선택된 모든 상품 수량
console.log( pdts.selected.quantity(products) ); // 7

// 선택된 모든 상품 기본 금액 x 수량
console.log( pdts.selected.price(products) ); // 126000

// 선택된 모든 상품 할인된 금액 x 수량
console.log( pdts.selected.discountedPrice(products) ); // 110000

// 선택된 모든 상품 중 할인 금액이 있는 상품의 총 수량 - before 이용
pdts.selected.dq = _ => pdts.total(
  quantity,
  _,
  filter(selected),    // <-- ...before에 넘어감
  filter(discount));   // <-- ...before에 넘어감

console.log( pdts.selected.dq(products) ); // 5

일반적으로는 ...before보다는 pipe를 이용하지만 ...before와 같은 기법을 이용하면 원하는 특정 위치에 함수들을 추가할 수 있는 함수를 준비할 수 있습니다.

아래는 좀 더 복잡한 문제들을 위에 있던 함수들의 조합을 통해 해결한 사례입니다.

// 선택된 모든 상품 중 할인 금액이 있는 상품의 총 수량 - pipe 이용
pdts.selected.dq = pipe(
  filter(selected),
  filter(discount),
  _ => pdts.total(quantity, _));

console.log( pdts.selected.dq(products) ); // 5

// 선택된 모든 상품 중 할인 금액이 있는 상품의 총 수량 - pipe + and 이용
pdts.selected.dq = pipe(
  filter(and(selected, discount)),
  _ => pdts.total(quantity, _));

console.log( pdts.selected.dq(products) ); // 5

// 선택된 모든 상품 중 할인 금액이 없는 상품의 기본 금액 x 수량 뽑기
go(products,
  filter(selected),
  reject(discount),
  pdts.price,
  console.log); // 40000

정리

객체지향 프로그래밍에서 추상화의 단위가 클래스라면, 함수형 프로그래밍에서는 추상화의 단위가 함수입니다. 위에서 살펴본 것처럼 함수형 ES6+에서는 고차 함수, 전개 연산자, 나머지 매개변수, 화살표 함수, 구조 분해 등을 활용하여 함수의 조합성을 높일 수 있습니다. 함수만으로도 높은 수준의 모듈화가 가능하며, 인자와 리턴값만으로 소통하는 함수들은 조합성이 좋고 테스트하기도 쉽습니다.

값의 형을 표기하지 않는 자바스크립트에서는 코드에서 사용하는 값이 어떤 명세를 가졌는지를 알기 어렵습니다. IDE를 이용하더라도 코드를 분석하는 것에 한계가 있습니다. 인터프리터 언어인 자바스크립트에서는 class, this, super, 메서드를 이용한 프로그래밍이 어려우며, 모듈화를 세밀하게 할수록 오히려 코드 분석 및 유지 보수가 더 어려워 질 수 있습니다.

하지만 기본 객체를 중심으로한 함수형 프로그래밍은 IDE를 통해 함수를 찾아다니는 것도 쉽고, 값들의 명세 역시 명확하게 되며, 테스트가 쉬워집니다. 이것은 팀 단위 프로젝트의 생산성을 높이며, 유지 보수 및 기획 변경에 대한 대응 및 배포를 쉽게 합니다. 함수는 라이프 사이클이 단순합니다. 언제 선언했는지와 언제 평가되었는지만 중요합니다. 함수라는 값은 상태가 없으며, 콘솔에서 확인하기도 쉽고, 테스트 단위도 명확합니다. ES6+에서 함수를 통한 모듈화는 효율적이며 안전합니다.