티스토리 뷰

JavaScript

함수형 프로그래밍

da.som 2018. 12. 19. 01:24

프로그래밍 패러다임

프로그래밍 패러다임은 프로그래머에게 프로그래밍의 관점을 갖게 해 주고, 결정하는 역할을 한다.

프로그래밍 패러다임은 크게 명령형과 선언적 프로그래밍으로 나뉘며, 함수형 프로그래밍은 선언적 프로그래밍의 방법론이다.

명령형 프로그래밍 이 코드로 원하는 결과를 달성해 나가는 과정 에만 관심을 둔다면, 선언적 으로 코드를 작성하면 구체적인 절차 대신 어떤 작업을 할건지 를 기술한다. 그리고 실제로 그 작업을 처리하는 세부적인 방법은 추상화 시켜 감춰진다. 그렇기 때문에 많은 주석 없이도 추론하기 쉬운 것이 장점이다.

"명령형 프로그래밍은 어떻게 할 것인가(How)를 표현하고, 선언형 프로그래밍은 무엇을 할 것인가(What) 표현한다."

아주 쉽게 이해가 되는 문장이 있어서 인용했다.

ES6 를 사용하면 함수형 프로그래밍 기법을 더 풍부하게 해주기 때문에 이번 포스팅에서는 몇 가지 ES6 문법 을 사용하여 예시를 들 예정이다.

함수형 프로그래밍의 핵심 개념

1. 불변성 (Immutablility)

함수형 프로그래밍에서는 원본 데이터를 유지 해야하는데, 이를 불변성 데이터라고 한다. 그렇기 때문에 원본 데이터를 변경하지 않는 대신 그 데이터의 복사본을 만들어 필요한 작업을 진행 한다.

아래와 같이 식당 정보에 대한 객체가 있다고 하자.

const restaurant = {
  title: "다솜레스토랑",
  color: "#00FF00",
  rating: 0
}

이때 식당 객체의 원본 데이터를 변경하지 않고 점수를 매기는 함수를 만들려면 다음과 같이 작성하면 된다.

const rateRestaurant = (restaurant, rationg) =>
  ({
    ...restaurant,
    rating
  })
console.log(rateRestaurant(restaurant, 5).rating) // 5
console.log(restaurant.rating) // 0

여기서 ES6의 화살표 함수 와, 객체 스프레드 연산자 가 사용되었다. 스프레드 연산자로 원본 restaurant 를 새로운 객체 안에 복사 한 다음 rating 프로퍼티를 덮어쓰는 방식이다. restaurant 을 변경불가능한 객체로 취급 하여 짧고 명확한 코드로 표현했다.

2. 순수 함수 (Pure Function)

순수 함수란 최소 하나 이상의 인자 를 받아햐 하고 동일한 입력에 항상 같은 값 이나 함수 를 반환하는 함수다. 또한 함수 내부나 밖의 다른 상태를 변경 하거나 입출력을 수행해서는 안된다. 즉, 부수효과(side effect) 가 없어야 한다.
순수 함수를 사용하면 프로그램에 영향을 미치지 않기 때문에 코딩이 편하고 테스트하기 쉬워진다.

3. 데이터 변환

함수형 프로그래밍은 앞에서 말했듯이 데이터 변경이 불가능하기 때문에, 원본을 변경한 복사본을 만드는 방식으로 한 데이터 집합에서 다른 데이터 집합을 만들어내는 도구 가 필요하다.
JavaScript에서는 복사본을 만들 수 있는 함수들을 제공하는데, Array.mapArray.reduce 가 가장 핵심함수다.

const pasta = [
  { source: "Tomato" },
  { source: "Cream" },
  { source: "Rose" },
  { source: "Aglio e Olio" }
];

// map
const editSource = (oldSource, source, arr) =>
  arr.map(item => (item.source === oldSource) ?
    ({...item,source}) :
    item
  )

console.log( editSource("Tomato", "Pomodoro", pasta)[0] ); // {source: "Pomodoro"}
console.log( pasta[0] ); // {source: "Tomato"}

위의 예제는 배열의 원소 중 하나만 변경하는 순수 함수 를 작성했다. 이런 경우 map 함수로 원본 데이터는 유지하고 다른 데이터 집합을 만들 수 있다.
여기서는 3항 연산자(? :)화살표 함수 를 이용해 editSource 함수 를 만들었다. 그리고 map 함수 를 사용해 원본 pasta 배열을 변경하지 않으면서 새로운 객체로 이루어진 배열을 만들고 , 배열의 원소를 item 파라미터로 받는다. 만약 파라미터가 oldSource 와 같으면 새 이름을 객체에 넣어서 반환하고 이름이 다르면 그대로 반환한다.

아래의 예시를 보면 JavaScript에는 다양한 데이터 변환 함수가 있는 것을 알 수 있다.

const pasta = [
  "Tomato",
  "Cream",
  "Rose",
  "Aglio e Olio"
];

// Array.join : 배열의 모든 원소를 인자로 받아 구분자로 연결한 문자열 반환, 원래 배열은 유지된다.
console.log(pasta.join(",")); // Tomato,Cream,Rose,Aglio e Olio

// Array.filter : 글자수가 6자리 이상인 파스타만 있는 새로운 배열 반환.
console.log(pasta.filter(item => item.length >= 6)); // ["Tomato", "Aglio e Olio"]

// 원본 배열이 유지되고 있는 것을 확인 할 수 있다.
console.log(pasta) // ["Tomato", "Cream", "Rose", "Aglio e Olio"]

// Array.reduce : 배열에서 최대 값을 찾아 반환 (배열을 하나의 값으로 축약)
const ages = [21, 18, 42, 40, 64, 63, 24];
const maxAge = ages.reduce(
  (max, age) => age > max ? age : max,
  0
);
console.log(maxAge); // 64

4. 고차 함수 (HOF, High Order Function)

고차 함수 란, 함수를 인자로 받거나 반환하는 것이 가능 한 복잡한 함수이다. 즉, 다른 함수를 조작할 수 있다.

앞에서 살펴본 Array.map, Array.filter, Array.reduce 는 다른 함수를 인자로 받는 고차 함수다.

커링 또한 고차 함수 사용법과 관련된 함수형 프로그래밍 기법이다. 이전 포스팅에서 다룬 적이 있기 때문에 예제는 생략하도록 하겠다.

5. 재귀 (Resursion)

재귀자기 자신을 호출하는 함수를 만드는 기법 이다. 루프는 모두 재귀로 바꿀 수 있고 재귀는 데이터 구조를 검색할 때도 유용하다.

카운트 다운을 구현하는 간단한 예제로 살펴보자.

const countdown = (value, fn) => {
  fn(value) // 현재 값을 인자로 콜백 호출
  return (value > 0) ? countdown(value-1, fn) : value
}

countdown(10, value => console.log(value));

// 10
// 9
// 8
// 7
// 6
// 5
// 4
// 3
// 2
// 1
// 0

countdown 은 숫자와 콜백함수를 인자로 받는다. countdown 이 호출되면 현재 값을 로그에 남기는 콜백이 호출된다. 그 후 현재 값이 0보다 큰지 검사하여 0보다 크면 숫자를 1감소시켜서 자기 자신을 호출한다. 여기서 countdown 은 재귀 함수다.

6. 합성 (Composition)

함수형 프로그램은 작은 순수 함수들이 구체적인 작업을 담당한다. 그리고 그 함수들을 연결해 고차 함수를 만들어 프로그램을 구축해야한다. 합성에는 여러 구현 방법이 존재한다. 예를 들어 체이닝 기법'.'을 사용하여 이전 함수의 반환값에 다음 함수(메서드)를 적용 할 수 있다.

좀 더 유연한 방법으로는 compose 함수 를 사용하는 것이다. 원하는 위치에 언제든 함수를 추가할 수 있으므로 쉽게 확장할 수 있으며, 합성한 함수의 순서도 쉽게 바꿀 수 있다.

const compose = (...fns) =>
  (arg) =>
    fns.reduce(
      (composed, f) => f(composed),
      arg
    )

compose 는 여러 함수를 인자로 받아서 fns라는 배열로 만든다. 그 후 arg라는 인자를 받는 함수를 반환한다. 이렇게 반환된 화살표 함수에 인자를 전달해 호출하면 fns 배열에 reduce가 호출되면서 arg로 받은 값이 전달된다. arg 값은 reduce의 초기값이 되고 각 이터레이션마다 배열의 각 원소(함수)와 이전 값을 변환 함수를 사용해 축약한 값을 전달한다. 이때 reduce의 변환함수는 이전 이터레이션의 결과값인 composedf를 인자로 받아서 fcomposed를 적용해 반환한다. 결국 마지막 함수가 호출되며 최종 결과를 반환 한다.

아래는 단순 합성 방식과 compose를 활용한 합성 방식에 대한 예제다. compose를 활용한 합성 방식이 가변 갯수의 함수 합성에 더 유리한 것을 확인할 수 있다.

See the Pen 합성함수 by Dasom Cho (@ssom13) on CodePen.

마무리

사실 나도 코드를 작성할 때, 어떻게 문제를 풀어야 할 지에만 집중하다보니 자연스레 명령형 프로그래밍에 익숙했었다. 함수형 프로그래밍에 대해 처음 접하게 된건 React를 공부하면서 부터였는데, React가 DOM을 구축하는 방식에 있어서 선언적이고 UI를 순수 함수로 표현하기 때문이다. 또한 ES6문법으로 작성하니 훨씬 코드가 깔끔해지고 재사용성이 높아져 비교적 짧은 기간 내에 프로젝트를 만들어본 경험을 했다. 무조건 함수형 프로그래밍이 좋다는 것은 아니다. 프로그램의 특성에 따라 적합한 프로그래밍 패러다임을 선택해 사용한다면 훨씬 생산성이 높아질 것이다.

Reference

'JavaScript' 카테고리의 다른 글

이벤트 위임 (Event Delegation)  (0) 2018.12.17
이벤트 버블링(Bubbling)과 캡쳐링(Capturing)  (0) 2018.12.17
메모이제이션 (Memoization)  (0) 2018.12.14
커링 (Currying)  (0) 2018.12.14
클로저 (Closure)  (0) 2018.12.09
댓글