Pandaman Blog

[JS] Javascript의 Functional Programming 본문

Front end/Javascript

[JS] Javascript의 Functional Programming

oyg0420 2020. 3. 1. 15:14

함수형 프로그래밍(functional programming)은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다.

함수형 프로그래밍이란 부수 효과(side effect)를 방지하고, 조합 성을 강조하는 프로그래밍의 패러다임이다.
여기서 부수 효과(side effect)를 방지라는 말은 우리는 함수 선언할 때 순수 함수를 만들어야 한다는 말이다.
순수 함수는 들어온 인자가 같다면, 항상 동일한 값을 리턴해주는 함수이다. 그리고 함수가 받은 인자외의 상태에 영향을 끼치는 않는 함수를 말한다. 리턴 값 외에는 외부와 소통할 수 없는 함수이다.
조합 성을 강조한다는 말은 순수 함수를 통해 모듈화 수준을 높인다면 오류를 줄이고 안전성이 높아지며 그로 인해 생산성을 높인다는 말이다.

우리는 자바스크립트의 함수형 프로그래밍을 배우기 위해 알아야 할 개념들이 있다.

일급 함수란?

자바스크립트는 일급 함수이다. 일급 함수는 함수를 값으로 다룰 수 있다는 말이다. 따라서 아래와 함수를 같이 다룰 수 있다.
첫 번째로 함수를 변수에 할당할 수 있다.

const add = (a, b) => a + b;

두 번째 함수를 인자로 전달할 수 있다.

const add = (a, b) => a + b;

function multipleFn(someFn) {
  return someFn * someFn;
}

multipleFn(add(2, 2));

multipleFn 함수의 인자로 add 함수를 할당해 주었다.

세 번째 리턴 값으로 사용할 수 있다.

const add = (a, b) => a + b;

function addReturnFn(someFn) {
  return someFn;
}
console.log(addReturnFn(add)); // (a, b) => a + b

addReturnFn 함수의 리턴 값은 add 함수인 것을 확인할 수 있다.

high Order Function(고차 함수)란?

고차 함수는 함수를 인수로 받거나, 함수를 반환함으로써 작동하는 함수를 말한다. 우리가 자바스크립트에서 고차 함수를 사용할 수 있는 이유는 위에서 배운 자바스크립트의 함수는 일급 함수이기 때문이다.

우리가 자주 사용하는 javascript에 내장된 map, filter, reduce 함수도 고차 함수이다.

간단한 예로 map 함수의 동작을 살펴보자.

const square = n => n * n
const result = [1, 2, 3, 4, 5].map(square);
console.log(result); // [1, 4, 9, 16, 25]

map 함수는 인자로 함수를 인자로 받은 함수로 그 기능을 수행하여 새로운 배열을 리턴해준다.

이제 이 함수를 map를 사용하지 않고 만들었다면 어떻게 될까?

let numberArray = [1, 2, 3, 4, 5];
let newNumberArray = [];

for(let i = 0; i < numberArray.length; i++) {
    newNumberArray[i] = numberArray[i] * numberArray[i];
}

console.log(newNumberArray) // [1, 4, 9, 16, 25]

고차 함수를 사용하지 않는다면 코드는 복잡해지며 그에 따른 오류가 발생하기 쉽다.

다시 우리는 우리만의 고차 함수를 만들어 보자.

const people = [
  { firstName: 'Panda', lastName: 'Oh', age: 31 },
  { firstName: '철수', lastName: 'Kim', age: 20 },
  { firstName: '영희', lastName: 'Lee', age: 40 },
];
const getMaximumAge = items => {
 return Math.max(...items.map(item => item.age))
}
const getPersonByAge = (items, age) => {
  const index  = items.findIndex(item => item.age === age);
  return items[index];
}
const getSayHello = name => 'hi I am ' + name;
const getMyFirstName = person => person.firstName;

const person = getPersonByAge(people, getMaximumAge(people));
console.log(person); // {firstName: "영희", lastName: "Lee", age: 40}
const sayHello = getSayHello(getMyFirstName(person));
console.log(sayHello); // hi I am 영희

위에 4가지 함수를 만들었다.
getMaximumAge 함수는 사람들 중 가장 나이가 많은 나이를 리턴한다.
getPersonByAge 함수는 사람들 중 할당한 나이에 해당하는 사람을 리턴한다.
getMyFirstName 함수는 할당한 사람의 첫 번째 이름을 리턴한다.
getSayHello 함수는 할당한 name과 조합한 문자열을 리턴한다.
우리는 결론적으로 가장 나이가 많은 사람이 누구 인지 문자열을 확인해보고 싶다. 결과는 사람들 중 가장 나이가 많은 영희인 것을 확인할 수 있다.

여기서 중요한 것은 우리는 각각의 작은 기능을 하는 함수를 만들어 복잡한 함수를 구성했다는 점이다. 이러한 기술을 통해서 우리는 버그를 줄이고, 코드를 이해하기 쉽게 만들어줄 뿐만 아니라 테스트하기가 훨씬 쉬워진다.

Memoization이란?

메모이제이션은 결과를 메모리에 저장하고, 다음 작업에서 캐싱된 데이터를 재사용하는 자바스크립트 기술이다.

예제를 살펴보자.

function memorize(fn) {
  let cache = {};
  return function(x) {
    if (cache[x] === undefined) {
      console.log('Not use Cache');
      cache[x] = fn(x);
    }
    return cache[x];
  } 
}

const multiple = memorize(a =>  a * a );

코드에 대해 설명하자면 memorize 함수는 fn 함수를 인자로 받는다.
memorize 에는 결과를 캐싱할 변수 cache가 선언되어 있다.
그리고 함수를 리턴한다. 리턴되는 함수는 인자 x에 해당하는 속성이 cache에 없다면 fn(x)의 결과 값을 할당하고, x에 해당하는 속성이 cache에 있다면 그대로 cache [x]를 리턴한다.
multiple 함수는 memorize 함수에 숫자를 제곱하는 함수를 할당한다.

자 이제 코드를 실행시켜보자

console.log(multiple(5)); 
// Not use Cache
// 25
console.log(multiple(5)); // 25

multiple(5)를 실행하면 'Not use Cache'문자열과 숫자 25를 리턴한다.
반복하여 multiple(5) 실행하면 25를 리턴한다. 캐시 된 데이터를 사용하는 것이다.

그럼 어떻게 캐시 된 데이터를 사용하게 되는 것일까?
우리는 이전 포스팅에서 클로저에 대해 배웠다. 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.
memorize 함수에서 리턴한 함수는 클로저다. 따라서 자신이 선언되었을 때의 LexicalEnvironment를 참조한다.
두 번째 multiple(5)를 실행했을 때 첫 번째 multiple(5)를 실행했을 때의 cache[5] = 55; 를 참조한다. 따라서 반복되는 multiple(5)를 실행했을 때 계속해서 계산 없이 이전 결과를 불러올 수 있는 이유이다.

Memoization은 반복되는 계산이 많을수록 효과가 높아진다. 반복되는 작업을 피할 수 있어, 웹 애플리케이션의 성능을 최적화할 수 있다.

Currying이란?

Curring은 함수형 프로그래밍에서 여러 인수를 갖는 함수를 연속적인 중첩 함수로 변경할 수 있는 프로세스이다. Curring은 다음 인수가 할당될 것을 예상하고 새로운 함수를 리턴한다.
Curring과 비교하기 위해 간단한 예제를 살펴보자.

const sum = (x, y) => x + y;
console.log(sum(2, 4)); // 6

간단한 함수이다. 인자로 받은 x, y를 더해 리턴하는 함수이다.

우리는 위 예제를 Curring 패턴으로 변경해보자.

const sum = x => y => x + y;
console.log(sum(2)(4)); // 6

함수 sum을 하나의 인수를 받는 중첩된 함수로 변경했다. sum(2)를 실행하면 새로운 함수 y => x + y가 리턴된다.
sum(2)(4)을 실행하면 받을 수 있는 인수를 소진했기 때문에 새로운 함수를 리턴하지 않고 결과를 얻을 수 있다.
여기서 생각해봐야 할 부분이 있다. sum(2)(4)을 실행했을 때 함수 4 => x + 4 가 실행된다. x값은 어떻게 알 수 있는 것일까?
y => x + y는 클로저이다. sum(2)가 실행될 때 함수 y => x + y 는 선언될 때의 LexicalEnvironment를 참조할 수 있다. 따라서 우리는 원하는 값을 얻을 수 있다.

그런데 왜 컬링을 만들어야 하는가가 의문이다. 함수의 인자를 하나씩 받아서 얻는 이점은 무엇인가?

일상적인 예제를 통해 알아보자.
애플 스토어에 왔다. 마침 지인에게 선물 받은 20% 할인쿠폰을 갖고 왔다.

function DiscountedAmount(price, rate) {
  return price - (price * rate/100)
} 

discount는 인자로 제품의 가격인 price, 할인율인 rate을 받는다. 애플스토어에서 여러 제품에 할인가를 적용했다.

const iphoneDiscount = DiscountedAmount(770000, 20); 
iphoneDiscount // 616000
const macBookDiscount = DiscountedAmount(3190000, 20) 
macBookDiscount // 2552000
const appleWatchDiscount = DiscountedAmount(539000, 20) 
appleWatchDiscount // 431200

갖고 있는 쿠폰은 20%로 고정값이라 두 번째 인자로 동일한 값을 할당하기에 귀찮을 수 있다.

위에서 배운 currying으로 DiscountedAmount 함수를 변경해보자.

 function DiscountedAmount(rate) {
  return function (price) {
    return price - (price * rate/100)
  }
 }
const twentyPercentDiscountedAmount = DiscountedAmount(20);

위에서 함수명은 좀 길지만 twentyPercentDiscountedAmount라는 20%의 할인한 값만을 얻을 수 있는 함수를 생성했다.

const iphoneDiscount = twentyPercentDiscountedAmount(770000); 
iphoneDiscount // 616000
const macBookDiscount = twentyPercentDiscountedAmount(3190000);
macBookDiscount // 2552000
const appleWatchDiscount = twentyPercentDiscountedAmount(539000) ;
appleWatchDiscount // 431200

결과는 동일하다. 하지만 우리는 함수 DiscountedAmount를 활용해 함수 twentyPercentDiscountedAmount 특정한 작업만 하는 함수를 생성했다. 그리고 언제든지 함수 DiscountedAmount를 활용하여 10%, 30% 할인가를 반환하는 함수를 만들 수 있다.

const tenPercentDiscountedAmount = DiscountedAmount(10);
const iphoneDiscount = tenPercentDiscountedAmount(770000); 
iphoneDiscount // 693000
const macBookDiscount = tenPercentDiscountedAmount(3190000);
macBookDiscount // 2871000
const appleWatchDiscount = tenPercentDiscountedAmount(539000) ;
appleWatchDiscount // 485100

위 예제를 통해 확인했듯이 Currying의 장점은 동일한 인수를 직접 할당하여 연속적으로 호출하는 방식을 피할 수 있다. Currying를 이용해 동일한 인수 갖는 함수를 생성하고, 생성된 함수를 이용해 특정 기능을 정의하여 사용할 있게 된다. 

사실 Currying은 연습이 많이 필요하다고 생각한다. 필자도 아직 curring에 대해 연습이 부족하여 실무에서 사용한 적은 없다. 앞으로 필요에 따라 배운 것을 적용해보는 습관을 길러야겠다

 

파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음

Comments