JavaScript | 함수형 프로그래밍

dev_hee·2021년 12월 6일
2
post-thumbnail

들어가며...

클린한 코드를 작성하기 위해 공부를 시작하면 항상 함수형 프로그래밍이란 단어를 듣게된다. 함수형 프로그래밍은 무엇이며, 어떤 이유 때문에 우리에게 익숙한 객체지향 프로그래밍이 아닌 함수형 프로그래밍이 떠오르고 있는지 알아보자.

⚙️ 함수형 프로그래밍에서 함수란?

함수는 입력이 주어지면 어떤 처리 과정을 거쳐서 출력을 내보낸다.

함수를 설명하기 위한 가장 많이 사용되는 방법은 파이프라인이다.

  • 파이프라인 외부에서는 내부의 상태를 보거나 접근할 수 없다.
  • 파이프라인 외부에서도 외부의 상태를 보거나 접근할 수 없다.

이런 특징을 가진 파이프라인(함수)들을 가지고 묶어서 프로그램을 구성해 나가는 기법을을 함수형 프로그래밍이라고 한다.

위와 같은 함수의 특징을 조금 더 자세히 표현하면 다음과 같이 4가지로 정리할 수 있다.

순수 함수, 불변성, 식, 일급 객체와 고차함수

해당 특징들을 알아보고, 왜 함수형 프로그래밍은 그런 특징을 가져야 하는지도 얘기해보자.

🔧 함수형 프로그래밍의 특징

1. 순수 함수

순수 함수는 함수에서 외부의 상태 값을 참조하거나 변경할 수 없다.
즉, 외부에 전혀 영향을 받지 않는 함수를 순수 함수라고 부른다.
→ 만약 외부 상태를 참조 또는 변경하면 절차지향형일 확률이 높다.

  • 순수 함수가 아닌 경우
// 순수 함수 X
let num = 1;
function add(a){
  return a + num; // 외부 상태 num에 의존적이다.
}
  • 순수 함수인 경우
// 순수 함수 O
function add(a, b){
  return a + b; // 오직 입력만 가지고 결과를 반환한다.
}
const result = add (2, 3);

함수형 프로그래밍은 이런 외부 상태로부터 독립적인 순수 함수를 사용해서, 동일한 입력을 넣으면 동일한 출력이 나오도록 예측 가능성을 높인다.

만약 외부 상태에게 의존적이면, 외부 상태가 변하게 되면 함수의 결과 값도 바뀌게 되는데 이는 예측하기 매우 어려워진다.

2. 불변성 유지 (Stateless, Immutability)

함수에 인자로 전달된 데이터를 변경하지 않는다. 즉,side effect 를 만들지 않으면서 불변성을 유지한다. 따라서 동시다발적인 multi-thread 환경에서도 안정적으로 동작할 수 있다.

side effect : 함수를 호출하면 외부의 상태가 변경되는 효과

  • 불변성을 지키지 못한 경우
// 불변성 X
let person = { name: 'hee', age: 24 }

function increaseAge(person) {
  person.age = person.age + 1; // 외부 상태를 변경한다.
  return person;
}
  • 불변성을 지키는 경우
    전달된 데이터의 새로운 버전의 새로운 객체를 만들어서 전달한다.
// 불변성 O
const person = { name: 'hee', age: 24 }

function increaseAge(person){
  return {...person, age: person.age + 1}; // 새로운 객체를 반환한다.
}

AI와 딥러닝과 같은 과학 기술의 발전으로 동시다발적으로 여러 함수를 한번에 실행할 수 있는 환경이 마련되었지만, 절차지향형 프로그래밍을 하는 경우 병렬적으로 함수를 안전하게 실행할 수 없다. 절차지향형은 외부 상태에 의존적이며 계속해서 변경하기 때문이다. 그러면 어떤 상태가 변경되면 다른 함수들의 결과도 다 달라질수 밖에 없다.

따라서 함수형 프로그래밍을 통해 불변성을 지키면, 전달된 데이터의 새로운 버전의 데이터를 만들어서 전달하기 때문에 함수들은 상태를 변경하지 않고 안전하게 프로그래밍을 할 수 있다.

3. 문을 사용하지 않고, 식을 사용한다.

함수형 프로그래밍은 for, if, switch ... 등과 같은 문을 사용하지 않고, 값을 반환하는 식을 사용한다.

  • 문을 사용한 경우
// 문 
let numbers = [1, 2, 3];

function multiply(numbers, multiplier){
  for (let i = 0; i < numbers.length; i++) {
    numbers[i] = numbers[i] * multiplier;
  }
}
  • 식을 사용한 경우
// 식
let numbers = [1, 2, 3];

function multiply(numbers, multiplier){
  return number.map(num => num * multiplier);
}

문을 사용하는 경우는 명령형, 절차지향형 프로그래밍이다. 개발자가 하나 하나 어떤 절차를 밟아가야 하는지 다 서술해야하기 때문이다. 이는 가독성도 떨어지고, 유지보수하기도 어려워진다.

식을 사용하는 경우는 함수형 프로그래밍에 해당한다. 파이프라인으로 함수형 프로그래밍을 설명한 것처럼, 식은 파이프라인과 유사하게 어떤 입력을 받으면 결과를 반환한다. 그 내부는 외부에서 들여다볼 수 없고, 그럴 필요도 없다. 위의 예제에서 배열의 고차함수인 map을 사용한 것이 그를 대표한다.

map은 배열의 메서드로 호출되면, 그 배열을 가공하여 같은 크기의 배열을 반환한다. map 고차함수의 내부엔 어떤식으로 코드가 구성되어 있는지는 우리는 관심 가질 필요가 없다. 입력으로 콜백함수를 넘겨주면 그 콜백을 호출해서 가공된 배열을 반환받기만 하면 된다. 즉, map 파이프라인의 내부는 관심이 없고, '해당 파이프라인에 입력을 넣으니 우리가 원하는 출력을 얻을 수 있다!' 는게 함수형 프로그래밍의 주된 목적이다.

이런식으로 프로그래밍을 진행하면, 작성된 코드가 무엇을 목적으로 쓰였는지 빠르게 파악할 수 있다. 따라서 보통 배열 고차함수를 사용하여 명시적으로 어떤 목적으로 해당 배열을 가공하는지 알릴 수 있다.

  • map : 전달된 배열과 동일한 크기의 가공된 새로운 배열을 반환함.
  • filter : 전달된 배열보다 작거나 같은 크기로, 기존 배열에서 몇개의 요소를 골라서 만들어진 새로운 배열을 반환함.
  • map, filter, reduce의 재미있는 예시

4. 일급 객체(First class), 고차 함수(Higher-order functions)

함수형 프로그래밍에서 함수는 다음과 같은 두 가지 중요한 특징을 가진다.

  1. 일급 객체
    함수가 일급 객체라면 함수를 변수에 할당하거나, 인수로 함수를 전달할 수 있다. 즉 함수를 처럼 다룰 수 있다.

  2. 고차 함수
    함수 자체를 인자(콜백)로 전달 받거나, 함수에서 또 다른 함수를 반환하는 함수를 고차함수라고 한다.

🥇 일급 객체

함수가 일급 객체라는 것은 함수를 객체와 동일하게 처럼 사용할 수 있다는 의니이다. 따라서 함수는 값을 사용할 수 있는 곳(변수 할당문, 객체의 프로퍼티 값, 배열의 요소, 함수 호출의 인수, 함수 반환문)이라면 어디서든지 리터럴로 정의할 수 있다. 또한 런타임에 함수 객체로 평가된다.

이렇게 함수가 일급 객체이기 때문에 함수의 매개변수로 함수를 넘길 수 있는데, 이를 통해서 3번 문이 아닌 식을 사용하여 파이프라인처럼 함수를 사용할 수 있게 된다. 자세한 예시는 고차함수를 얘기하며 알아보자.

⛰ 고차함수

함수형 프로그래밍에서 고차함수는 매개 변수로 콜백 함수를 받는 함수를 얘기한다. 또는 함수를 반환하는 함수도 고차함수라고 부른다.

고차 함수는 보통 배열의 메서드map, filter, reduce... 등을 설명할 때 많이 나오는 단어다. map 메서드를 대표로 고차 함수에 대해 알아보자.

Array.prototype.map

map메서드는 자신을 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출한다. 그리고 콜백 함수의 반환값들로 구성된 새로운 배열을 반환한다.

map의 polyfill

  if (!Array.prototype.map) {
    Array.prototype.map = function (callback, thisArg) {
      if (typeof callback !== 'function') throw new TypeError();

      thisArg = thisArg || window;

      const newArray = [];
      for (let i = 0; i < this.length; i++) {
        newArray.push(callback.call(thisArg, this[i], i, this));
      }
      return newArray;
    };
  }

map의 폴리필을 보면 위와 같이 콜백 함수를 전달 받는다.
map의 코드 내부에서 for문을 통해 this.length만큼 반복하며 콜백 함수를 호출한다. 즉, 함수를 매개변수로 전달 받는 map은 고차 함수이고, 고차 함수 내부에서 전달 받은 콜백을 호출함으로서 유연하게 함수 내부 로직을 갈아 끼울 수 있다.

따라서 직접 배열을 순회하지 않고도 배열을 가공해주는 로직을 작성한 함수를 배열의 고차함수인 map의 매개 변수로 넘겨주면 가공된 새로운 배열을 반환받을 수 있다.

이런식으로 함수를 값처럼 매개 변수로 전달할 수 있는 일급 객체의 특징과, 그 특징을 사용한 고차 함수를 활용하여 프로그래밍을 하는 것이 바로 함수형 프로그래밍이다.

마무리...

함수형 프로그래밍은 순수 함수를 사용하여 부소 효과를 최소화하여 불변성을 지키는 프로그래밍 패러다임이다. 프로젝트의 규모가 커질수록 공통으로 관리해야 할 상태들이 많아지고 그 상태를 최대한 안전하게 변경하며 예측 가능한 코드를 작성해야한다. 그러기 위해서 함수형 프로그래밍은 많은 관심을 받고 있다.

하지만 순수 함수만으로 규모 있는 프로젝트를 진행할 수 없다. 상태를 변경하는 부수 효과는 필연적이다. 함수형 프로그래밍을 지향하며 반드시 상태가 변경되어야 하는 부분에서만 상태를 변경하고, 그렇지 않는 영역에서는 순수 함수를 사용하여 최대한 부수 효과를 막아 예측 가능성을 높이는 것이 우리의 궁극적인 목표라고 할 수 있다.

profile
🎨그림을 좋아하는 FE 개발자👩🏻‍💻

0개의 댓글