치춘짱베리굿나이스

자바스크립트에서의 함수형 프로그래밍 본문

Javascript + Typescript/이론과 문법

자바스크립트에서의 함수형 프로그래밍

치춘 2022. 7. 30. 12:23

함수형 프로그래밍

굳이 ‘자바스크립트에서의' 라는 문구를 넣은 것은… 예제 작성할 때 자바스크립트로 작성했기 때문이다

특별한 의미는 없다…

정의

객체지향과는 또 다른 프로그래밍 패러다임

‘자료 처리를 수학적 함수의 계산으로 취급하고, 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임’

…이라고 하는데 이 말만 봐서는 뭔 얘긴지 아리송하다

  • 객체지향은 여러 종류의 객체들을 유기적으로 엮어 프로그램을 완성시키는 기법
  • 함수형 프로그래밍은 기능을 작은 함수 단위로 쪼개고, 작은 함수의 연산 반환값을 이용하여 큰 함수를 만들고, 이 큰 함수들로 더 큰 함수에 필요한 연산을 계산하고… 쌓아올리는 방식으로 프로그램을 완성시키는 기법

쉽게 말해 프로그램을 이루는 작은 연산 하나하나를 만들 때에도 함수의 계산을 이용하는 방식이다

프로그래밍 패러다임?

프로그램을 만들 때에도 무슨 프로그램을 만들 지에 따라 그 절차와 방법이 천차만별이다

객체와 클래스를 쌓아올리면 더 깔끔하게 만들어지는 프로그램이 있는가 하면, 함수를 쌓아올렸을 때 더 빛나는 프로그램이 있는 법이지만, 처음에는 어느 방법이 더 좋을 지 찾기 조금 어려울 수 있다

프로그래밍 패러다임은 프로그래머가 프로그래밍의 관점을 갖고, 어떤 방법을 사용하여 프로그래밍을 하는 게 좋은지 결정을 돕는다

 

예를 들어 객체지향은 프로그래머가 프로그램을 객체들의 상호작용 관점으로 바라보게 해주고, 함수형은 상태와 가변 데이터를 갖지 않는 단순 함수들의 계산 결과의 집합 관점으로 생각하게 해준다

각 패러다임을 다른 패러다임의 특징들로 완벽하게 대체할 수 없지만, 프로그래밍의 관점을 넓혀 매 순간 유연한 사고로 최선, 최적의 선택을 하는 것을 도와준다

패러다임을 지원하는 언어들

프로그래밍 언어들은 이러한 패러다임 흐름을 지원하기도 하는데, 예를 들어 함수형 프로그래밍 패러다임을 지원해주는 언어로 Haskell, OKaml, Erlang 등이 있고, 객체지향 프로그래밍 패러다임을 지원해주는 언어로 Java, C++ 등이 있다

복수형 패러다임을 지원해주는 언어들 (멀티 패러다임 언어) 로는 Javascript, Kotlin, Python, Go, Swift 등이 있으며, 이러한 언어들은 비교적 신생이라 기존에 있던 패러다임들을 고려하여 설계되었다

함수를 만든다고 함수형 프로그래밍이 아니다?

처음 함수형 프로그래밍을 접했을 땐 단순히 ‘함수로만 만들면 함수형 프로그래밍 아닌가’ 생각했는데, 전혀 아니었다

함수형 프로그래밍의 지향점은 단순히 함수를 잔뜩 만드는 것이 아니라, ‘일정한 입력값에 대하여 일정한 출력을 반환하는’ 순수 함수들의 계산 결과만으로 모든 자료를 처리하고, 가변 데이터를 멀리하는 것에 있다

특징

‘수학적 함수의 계산’ 이라는 말에서 볼 수 있듯 함수형 프로그래밍은 수학과 밀접한 연관이 있기 때문에 배우기 조금 난해하고, 사람의 사고방식과 유사한 패러다임인 절차지향 / 객체지향과 다르게 함수형 프로그래밍은 이러한 경향이 적어 익숙하지 않을 수 있다

일정한 입력값을 받으면 일정한 반환값을 내보내며, 외부의 값에 영향을 받지 않는 작은 단위의 함수들 (순수함수) 로 연산을 수행하고, ‘상태값' (= 변수의 사용 등) 을 지양하기 때문에 프로그램 내부의 상태들이 함수의 입력값과 반환값을 통해 흘러간다

이러한 경향 때문에 함수형 코드는 객체 지향 코드에 비해 간결하고, 입력에 대한 출력이 명확하기 때문에 예측가능하여 테스트가 쉽지만, 익숙하지 않으면 오히려 직관적이지 않아 난해하게 보일 수 있다

장점

  • 함수형 코드는 특정한 입력값에 대하여 무조건 같은 결과만을 반환한다
    • 대부분의 코드가 순수함수로 이루어져 있기 때문에, 부수효과가 없고 외부의 영향을 받지 않아 사용자가 쉽게 결과를 예측가능하다
    • 이 이점은 함수를 테스트할 때 상당한 이점으로 작용된다 (복잡한 상호작용이 없으므로)
    • 이 덕에 예기치 못한 상황에서 프로그램이 중단되었을 때, 디버깅도 쉽다
  • 수학적 계산을 하는 작은 함수들의 조합으로 구성되기 때문에, 함수를 재사용하기 쉽다
    • 특히나 위의 순수함수 특성 덕에 각 함수가 무슨 역할을 하는지도 상당히 명확하다
    • 다른 모듈에서 함수를 가져다 써도 부수효과가 없고 참조 투명하기 때문에, 부작용이 적다

쉽게 생각해보자면 함수 프로그래밍은 여러 생김새를 가진 (여러 수학적 연산을 하는) 레고 조각들을 조립하여 하나의 큰 작품 (프로그램) 을 만든다고 할 수 있다

단점

  • 순수함수, 고차함수, 선언형 함수 등 알아둬야 할 특징들이 많다
    • 또한 이런 부분에 대해 세세하게 신경쓰다 보면 오히려 가독성이 더 떨어지는 코드가 나올 수 있다
  • 함수형 프로그래밍에 익숙하지 않을 경우, 가독성을 지키기 어렵다
  • 언어에 익숙하지 않은 사람이 코드를 읽을 때, 한눈에 알아보기 힘들다
    • 특히 각종 메서드나 재귀함수 등에 익숙치 않을 경우, 함수의 목적이나 동작 방식에 대한 이해가 느릴 수밖에 없다
  • 순수함수로 이루어진 작은 함수들을 사용하는 것은 쉬울 지 몰라도, 이를 조합하기엔 어려움이 따른다

개인적으로 나는 이러한 부분에서 함수형 패러다임이 어렵다고 느꼈다…

적절하게 사용하면 코드의 재사용성을 끌어올릴 수 있지만, 익숙치 않은 사람에겐 오히려 독으로 작용하는 것 같다

변수선언, if, for을 쓰지 말라고? 조건이 너무 빡빡한 것 아니오?

함수형 프로그래밍의 의미는 ‘일정한 연산을 하는 작은 단위의 함수들을 이용하여 큰 프로그램을 구성하는 현상' 에 있지, 어떠한 키워드의 사용을 아예 금지하려는 의도는 아닌 듯하다

함수형 프로그래밍은 말 그대로 하나의 패러다임이며, 너는 반드시 이걸 지켜서 코딩해야해! 라는 제약조건이 아니기도 하고, 패러다임이란 효율적인 프로그래밍을 위한 하나의 관점 제시일 뿐이지 그 안에 있는 세세한 조건을 전부 지킬 필요는 없어보인다

부득이하게 사용해야 할 경우엔, 캡슐화 등을 이용하여 함수로 감싸 사용하는 것이 순수함수나 선언형 등의 특성에 맞게 사용하는 방법일 것

 

요지는 순수함수, 부수효과, 불변성 등 세세한 규칙을 하나하나 따져가며 프로그램을 작성하기엔 무리가 있는 경우가 많다

당장 프론트엔드만 해도 화면을 통한 상호작용과 부수적 효과를 사용자에게 제공해야 하는데, 이를 다 막아버리면 사실상 의미가 없다

함수형 프로그래밍의 조건을 하나하나씩 지켜가며 함수를 작성하려 하지 말고, 하나의 관점이라고 생각하고 부분적인 특성을 적용해서 코드를 개선시키려 노력하자

 

이에 관련한 아주 좋은 글이 있다 (감사합니다)

다시 쓰는 함수형 프로그래밍

 

다시 쓰는 함수형 프로그래밍

> 참 좋은데 어떻게 표현할 방법이 없네... 오랜 기간 개발을 공부하게 되면서 여러가지 패러다임의 변화를 겪었는데 그 중에서 인상깊었던 것중에 하나는 객체지향 패러다임에서 함수형 패러다

velog.io

나중에 또 읽어봐야지

순수함수

순수함수는 ‘동일한 입력에 대하여 동일한 값을 반환하는 함수' 로, 외부 값에 일절 영향을 받지 않고, 외부에 영향을 주지도 않아야 한다 (참조 투명성)

또한, 함수 내부에서 인자의 값을 변경하거나, 프로그램의 상태를 변경하는 부수효과 (Side-effect) 가 없어야 한다

참조 투명성

let b = 0;

function foo(a) {
    return a + b;
}

foo(10); // 10

b = 2;
foo(10); // 12

위와 같은 함수를 예시로 들면, 같은 입력값인 10에 대하여 다른 결과값을 반환하고 있다

이 함수는 전역 스코프에 선언된 변수 b에 의존적이기 때문에, b가 바뀌면 결과값도 바뀐다

따라서 이는 순수함수라고 할 수 없다

 

function foo(a) {
    return a + 5;
}

foo (10); // 15

이 함수는 바깥의 어떤 자료도 참조하지 않고, 입력 값에 대해서 언제나 같은 출력값을 반환하기 때문에 순수함수이다

이처럼 외부의 영향 없이 일정한 결과값을 반환하는 것을 ‘참조 투명성' 이라고 한다

참조 투명성을 가진 코드는 아래와 같은 특성을 갖고 있다

  • 함수 외부에 의존하지 않는다 (함수 밖에서 데이터를 가져오는 것은 매개변수밖에 없다)
  • 동일한 매개변수에 대해서는 항상 동일한 결과가 나와아 햔다
  • 예외 (Exception) 를 던지지 않아야 한다
  • 예기치 않게 실패하지 말아야 한다
  • 데이터베이스, 파일 시스템 등 외부 디바이스로 인해 동작이 멈추면 안 된다

부수효과 (side-effect)

function foo(a) {
    return a / 0;
}

function bar(a) {
    console.log(a);
}

위의 함수들은 함수 바깥의 세계에 영향을 준다

0으로 나누는 함수는 예외가 발생하고, 콘솔에 출력하는 함수는 외부 디바이스에 내용을 쓴다

이러한 함수들은 순수함수라고 할 수 없다

 

function foo(a) {
    return ++a;
}

위의 함수는 함수 바깥의 변수에 영향을 주지 않는다

자바스크립트에서 원시 타입 (Primitives) 은 매개변수를 넘겨줘도 참조값이 아닌 그 복사본이 들어가기 때문에, 원본 변수는 전위연산이나 후위연산, 단순 연산자를 통한 계산 등에 영향을 받지 않는다

따라서 이 함수는 순수함수이다

 

이처럼 함수 내부의 로직이 외부에 영향을 주는 것을 ‘부수 효과' 라고 하며, 순수함수는 부수 효과가 없어야 한다

대표적인 부수효과는 다음과 같다

  • 외부 변수 (부모 스코프, 전역 등), 외부 객체, 힙 영역 변수 등의 속성을 변경하는 행위
  • console.log 등 입출력
  • 화면에 변화를 주는 것
  • 파일에 값을 쓰거나, 값을 삭제하는 것
  • 네트워크 통신
  • 외부 프로세스를 실행시키거나 종료하는 행위
  • 또는 위의 부수효과를 일으키는 함수의 호출

쉽게 생각하면, 함수 내부의 로직은 딱 함수 내부에서만 작용해야 한다는 것이다

일급함수 (일급 객체)

function foo(bar) { // 함수를 인자로
    return bar(10);
}

function bar(a) { // 함수를 반환값으로
    return function dap() {
        console.log(a);
    }
}

console.log(foo); // 함수를 값처럼

함수를 다른 변수와 똑같이 다룰 수 있는 경우 일급함수를 사용한다고 표현한다

마찬가지로 객체를 다른 변수와 똑같이 다룰 수 있는 경우 일급 객체라고 한다

일급 함수를 가진 언어는 함수를 다른 함수에 매개변수로 제공하거나, 함수를 반환할 수 있다

자바스크립트는 함수도 객체 (일급 객체) 취급되므로 함수를 반환하거나 인자로 사용할 수 있다

고차함수 (상위함수)

[1, 2, 3].map((v) => v + 5); // [6, 7, 8]

일급 함수의 성질 (다른 함수의 매개변수로 사용되거나, 반환값으로 사용할 수 있음) 을 이용하여, 함수를 값처럼 다루는 함수

보통 함수를 인자로 받아서 안에서 실행하는 함수를 고차함수라 일컫는다

수학에서 합성함수 f(g(x)) 를 생각하면 쉽다

불변성

함수 프로그래밍에서의 모든 데이터는 불변성을 유지하여야 한다

데이터의 변경이 필요한 경우, 원본 데이터를 건드리지 않고 복사본 (깊은 복사 등) 을 만들어서 이를 변경해서 사용해야 한다

let obj = {a: 1, b: 2};

function foo(a) {
    obj.a = 3;
    return obj;
}

위의 예제에서 obj는 힙 메모리에 할당된 값의 참조일 뿐이므로, 함수에 인자로 가져다 사용하면 원시값들처럼 복사본이 들어가는 것이 아니라 원본 값이 변하게 된다

따라서 obj.a = 3 연산은 실제로 obj 객체를 변화시키고 있으므로, 불변성 법칙에 위배된다

가변 데이터 (변경될 수 있는 데이터) 를 지양하는 함수 프로그래밍 특성상, 이러한 연산은 지양되어야 한다

 

let obj = {a: 1, b: 2};

function foo(a) {
    return {...obj, a: 3};
}

위의 예제를 조금 변형해 보았다

obj 객체를 전개 연산자를 통해 깊은 복사를 해 주었으므로, foo 함수의 반환값인 객체 참조와 obj 변수는 전혀 다른 값을 가리킨다

obj 객체의 값은 변하지 않았으므로, 불변성을 지켰다고 볼 수 있다

 

let obj = Object.freeze({a: 1, b: 2})

Object.freeze를 이용하면 속성이 변질될 가능성이 큰 힙 메모리 객체라도, 값을 고정시켜 버릴 수 있다

선언형 함수 (↔ 명령형)

function imperative() {
    const arr = [1, 2, 3, 4];
    const newArr = [];
    for (let value of arr) newArr.push(arr + 5);
    return newArr;
}

function declarative() {
    const arr = [1, 2, 3, 4];
    return arr.map(value => value + 5);
}

위의 함수는 명령형, 아래의 함수는 선언형이다

명령형 프로그래밍은 과정 (How) 에 집중하고, 선언형 프로그래밍은 목적 (What) 에 집중한다

  • 명령형: ‘나는 18시에 맥도날드 갈 거고, 가서 상스치콤 주문해서 먹을 거다’
  • 선언형: ‘상스치콤 먹고싶다’

이처럼 명령형은 어떠한 목적을 달성하기 위한 과정을 단계별로 나열하고, 선언형은 목적이 무엇인지 명시한다

예시의 두 함수 모두 arr 배열의 각 원소에 5를 더해서 만든 새 배열을 반환하는데,

  • 위의 imperative 함수는 for문을 이용하여 목적 (배열의 각 원소에 5를 더하고, 새 배열을 만들기) 의 절차를 하나하나 나열한다
  • 아래의 declarative 함수는 map 메서드를 이용해서 배열을 바로 반환하고 있다

선언형 함수는 세세한 과정 (How) 은 되도록 추상화하고, 무엇을 원하는지에 조금 더 집중한 함수라고 할 수 있다

map 내부를 보면 명령형과 다를바가 없지 않을까?

명령형 프로그래밍 언어도 메서드나 함수 내에서 비선언형인 부분은 외부로 노출되지 않도록 캡슐화를 해주어 선언형처럼 보이게 할 수 있다

앞서 말했듯 명령형, 선언형과 같은 특성들은 하나의 패러다임이기 때문에, 세세한 것까지 지키지 않더라도 최종 목적에 부합하도록 코드를 깔끔하게 구성하는데 집중하는 것이 중요하다

결론

한참 무언가 결과를 내는 것에만 치중하느라 이론을 등한시했는데 요즘 이론 공부를 틈틈히 하다 보니 시야가 넓어진 기분이 든다

사실 프론트엔드에서 로직, 뷰, 상태관리 등 함수를 훅으로 분리하고 오직 하나의 목적에만 집중하는 것도 넓~~~게 보면 함수형 프로그래밍의 의도에 부합하지 않을까 생각한다

작은 연산 (로직 구성, 뷰 렌더링 등) 을 하는 함수 (훅) 를 모아 큰 프로그램 (웹 페이지) 을 구성한다는 점에선 프론트엔드 코드들도 충분히 함수형 프로그래밍의 특성을 사용하여 코드를 개선시킬 수 있을 것 같다


참고자료

함수형프로그래밍이 대세다?! (함수형 vs 객체지향)

함수형 프로그래밍이란?

함수형 프로그래밍이란?

참조 투명성, 참조적 투명함수란?

부수 효과 (Side Effect), 참조 투명성 (Referential Transparency)

일급함수란 무엇인가?

선언형 프로그래밍이란 무엇일까?

다시 쓰는 함수형 프로그래밍

Comments