맛있는 함수형 프로그래밍 레시피

Lellow_Mellow·2023년 7월 22일
5

JavaScript

목록 보기
3/4
post-thumbnail
post-custom-banner

블로깅 시작 전에

함수형 프로그래밍, 코딩 공부를 하다보면 간혹 마주치는 키워드입니다.

✨ 함수형 프로그래밍? 함수를 사용하는건가? 근데 함수라면 지금도 사용하고 있는데... 요즘에는 함수형 프로그래밍이 대세라는데 이게 뭐지?

객체지향 프로그래밍도 어렵고 잘 모르겠는데... 함수형 프로그래밍이 도대체 무엇인가요? 동일한 의문점을 가지신 분이라면, 이번 포스팅이 도움이 되길 바라며 포스팅을 진행하게 되었습니다.


이번 포스팅의 목표

이번 포스팅은 과연 함수형 프로그래밍이란 무엇인가와 더불어 왜 함수형 프로그래밍이 필요한가, 어떻게 함수형 프로그래밍을 사용할 수 있는가에 대해 알아보는 것을 목적으로 합니다.

마냥 아, 함수형 프로그래밍이라는게 있었지라고 넘어갔던 함수형 프로그래밍을 맛있게 요리해봅시다.


프로그래밍 패러다임

함수형 프로그래밍, 이게 무엇일까요? 함수형 프로그래밍은 프로그래밍 패러다임에 해당합니다. 패러다임? 그게 뭐죠? 먹는건가요?

[ 패러다임의 사전적 정의 - paradigm ]

  • 한 시대의 사람들의 견해나 사고를 근본적으로 규정하고 있는 인식의 체계. 또는, 사물에 대한 이론적인 틀이나 체계. 순화어는 '틀'

조금 더 쉽게 다가가봅시다. 패러다임은 어떤 문제에 대해 대다수의 사람들이 공통적으로 공유하고 받아들이는 가치관이라고 할 수 있겠습니다. 패러다임을 설명할 수 있는 정확한 예시는 아니겠지만, 이러한 예시가 도움이 되지 않을까 싶습니다. (혹 틀린 비유라면 지적해주시면 감사드리겠습니다.)

세상에 타르트는 🍓딸기로 만든 타르트만 있었다고 가정해봅시다. 사람들은 디저트 중에서 '타르트'하면 당연하게 딸기 타르트를 떠올리곤 했습니다. 그런 어느 날, 혜성처럼 나타난 새로운 디저트 카페에서 다양한 과일들로 만든 타르트를 선보였습니다. 🍊오렌지 타르트, 🫐블루베리 타르트, 🍇청포도 타르트 등등... 이때부터 사람들의 인식이 바뀌기 시작합니다.

🍰 아! 타르트는 다른 과일로 만들어도 맛있구나! 딸기 타르트만 있는게 아니였어!

이제 타르트를 생각하면 사람들은 딸기 타르트 이외에도 다양한 타르트를 떠올리게 될겁니다. 이러한 변화가 하나의 패러다임이라 할 수 있겠습니다.

프로그래밍 패러다임이 정확히 무엇인지, 어떠한 과정으로 변화하였는지에 대한 내용은 추후 다른 포스팅으로 살펴보겠습니다.

그렇다면 함수형 프로그래밍이란 프로그래밍과 관련된 패러다임이다, 라는 문장이 어떠한 느낌일지 살짝은 감이 잡혔을 것이라 생각합니다. 그렇다면 프로그래밍에서의 패러다임은 무엇일까요?

💻 프로그래밍에서의 패러다임 === 어떻게 프로그래밍을 할 것인가

다시 말해 특정 관점과 방식을 바탕으로 프로그래밍을 하는 것을 의미합니다. 그렇다면 함수형 프로그래밍은 어떠한 패러다임일까요?


함수형 프로그래밍?

객체지향 프로그래밍은 객체를 중심으로 코드를 작성하는 패러다임이라 할 수 있습니다. 비슷한 맥락으로 함수형 프로그래밍은 함수가 중심이 되는 프로그래밍 패러다임입니다. 정의를 함께 살펴봅시다.

[ 함수형 프로그래밍 - functional programming ]

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

다시 말하자면 데이터를 함수로 연결하는 것을 중심으로 사고하고 프로그래밍을 하는 것이라 정리할 수 있습니다. 그런데 어떠한 이유로 함수형 프로그래밍이 탄생하게 되었을까요?

🔗 함수형 프로그래밍은 1930년대에 계산가능성, 결정문제, 함수정의, 함수응용과 재귀를 연구하기 위해 개발된 형식체계인 람다 대수에 근간을 두고 있다. 다수의 함수형 프로그래밍 언어들은 람다 연산을 발전시킨 것으로 볼 수 있다.

최근에 유행하는 패러다임인데, 그 탄생은 무려 1930년대였습니다. 여기서 람다 대수, 람다 표현식이 함수형 프로그래밍의 아주 중요한 근간이라 할 수 있습니다. 우리가 생각하는 람다 표현식은 어떤 것인가요?


람다 대수? 람다 표현식?

람다 표현식에 대해 살펴보기 전에, 익명 함수와 일반 함수에 대해서 간단하게 살펴보겠습니다.

// 우리가 아는 일반적인 함수
function name() {
	// logic 
}

// 우리가 아는 익명 함수
function() {
	// logic 
}

우선 우리가 아는 일반 함수는 함수명을 선언하여 메모리를 차지하게 됩니다. 하지만 익명 함수는 함수명을 선언하지 않습니다. 익명이기 때문에 함수명이 있는 함수와는 달리 익명 함수는 메모리를 차지하지 않게 됩니다.

// 변수에 저장한 익명 함수
const variable = function() {
	// logic 
}

// arrow function === 람다 함수
const variable = () => {
	// logic 
}

이러한 익명 함수는 위와 같이 변수에 저장하여 사용하게 됩니다. 두 번째 함수의 형태가 좀 더 익숙할 것입니다. JavaScript에서는 이제 아무렇지 않게 자주 사용하는 arrow function입니다. 이는 람다 표현식이라고도 불립니다.

여기서 람다 표현식이 바로 람다 대수와 큰 연관이 있습니다. 람다 대수에서는 λ(lambda) 기호를 사용하여 입력을 받고 계산을 수행하여 출력을 생성하는 함수를 표현합니다. 아주 간단한 예시를 살펴보자면 다음과 같습니다.

✨ 람다 대수를 깊게 다루기에는 매우 심오한 주제라고 생각합니다. 여기서는 람다 표현식이 왜 람다 표현식이라고 불리울까? 라는 궁금증에 간단한 부분만 살펴봅니다.

[ λx.expression ]

  • 람다 기호를 사용하여 x를 입력받습니다.
  • 이에 대하여 expression을 처리하여 결과를 실행하는 함수를 정의합니다.
//  lambda expression
const addTwo = (x) => x + 2;

// 람다 표현식 사용
console.log(addTwo(3));

JavaScript에서의 람다 표현식 역시 람다 대수만큼 형식적이지는 않으나 기본 아이디어는 동일합니다. x를 입력으로 받고 x + 2를 출력으로 반환하는 익명 함수를 정의하게 됩니다. 즉, 함수형 프로그래밍은 이러한 람다 연산을 근간으로 하여 발전한 형태입니다.


함수형 프로그래밍의 특징

함수형 프로그래밍이 람다 대수를 근간으로 하여 발전했다는 사실과 우리가 사용하는 람다 표현식에 대해 간단하게 살펴보았습니다. 그렇다면, 함수형 프로그래밍은 어떠한 특징을 가지고 있을까요? 정확히는, 우리가 어떻게 코드를 작성해야 함수형 프로그래밍이라고 할 수 있을까요?

이에 대해 키워드 단위로 정리하자면 아래와 같습니다

  • 순수 함수
  • 선언형 함수
  • 불변성
  • 1급 객체와 고차 함수
  • 참조 투명성
  • 재귀

각 키워드에서는 대략 짐작이 가는 부분도 있겠지만, 처음 들어보는 키워드도 많습니다. 하나씩 맛있게 구워봅시다. 🥐🔥


순수 함수 | Pure Function

순수 함수, 함수는 함수인데 특별한 특징을 가지고 있는 함수입니다.

항상 동일한 인수에 대한 같은 출력을 생성한다는 의미는 오직 입력값만을 가지고 처리하기 때문에 입력값이 변경되지 않는 한 항상 같은 결과를 반환한다는 것과 같습니다.

즉, 함수를 실행해도 다른 것에 영향을 미치지 않는 함수라는 의미입니다. 이는 참조 투명성이라는 개념과 연관지을 수 있습니다. 예시를 살펴보겠습니다.

let number = 0;

function add(a) {
	return number + 1; 
}

function increase(a) {
	return number += 1; 
}

위 함수를 살펴보겠습니다. 첫 번째 addnumber라는 외부 변수를 참조합니다. increase는 심지어 외부 변수의 값을 변화시킵니다. add는 외부 변수에 따라 결과값이 달라지며, increase 역시 외부 변수에 따라 값이 바뀌는 것은 물론 외부 변수에 영향까지 미치기 때문에 순수 함수라고 부를 수 없습니다.

function add(a, b) {
 	return a + b;
}

이번 add 함수는 외부 값을 참조하지도 않고, 변화시키지도 않습니다. 오직 입력값만 이용하여 출력하게 됩니다. 다른 부분에 영향을 미치지 않고, 동일한 입력값에 대해 항상 동일한 출력을 생성하기 때문에 이러한 형태가 순수 함수라고 할 수 있겠습니다. 이렇듯 side effect가 없는 것을 참조 투명성을 가진다고 하며, 이러한 특성을 가진 함수를 순수 함수라고 칭합니다.

그런데 여기서... 입력값이 변경되지 않는 한 항상 같은 결과를 반환한다는 특징, 어딘가 많이 익숙하지 않나요? 수학에서의 함수의 정의는 무엇이었나요?

임의의 x ∈ X 에 대해여 그에 대응하는 y ∈ Y가 유일하게 존재하는 대응 관계

입력값에 대한 출력값, 유일하게 존재... 어딘가 많이 닮지 않았나요? 순수 함수를 더 깔끔하게 정리해보자면 수학의 함수를 프로그래밍으로 가져온 모델이라고 표현할 수 있겠습니다.

🔗 순수 함수에 대한 깊은 이해가 필요하다면 여기를 참고해보세요.


불변성 | Immutability

함수형 프로그래밍에서는 불변성을 중요하게 여깁니다. 간단하게 생각하자면 상태를 변경하지 않는 것이라고 해석할 수 있겠습니다. 근데 왜 불변성이 중요할까요?

상단에서 설명한 순수 함수는 수학의 함수와 비슷한 함수라고 설명하였습니다. 이러한 순수 함수와 불변성은 아주 밀접한 연관성이 존재합니다. 수학은 프로그래밍과 달리 상태라는 개념이 존재하지 않습니다. 다시 말해, 무언가를 저장하고 변경하고 불러올 수 있는 상태라는 개념은 프로그래밍에 존재하며, 수학에서는 존재하지 않습니다.

순수 함수를 수학에서의 함수를 가져온 개념이라고 생각한다면, 당연히 상태라는 개념이 없기 때문에 불변성이 필요합니다. 그렇다면 불변성은 어떤 것을 의미할까요?

[ 불변성 - Immutability ]

  • 데이터가 변하지 않음을 의미합니다.
  • 단순히 변수의 값을 변경하지 않음에서 더 나아가, 메모리에 저장된 값을 변경하지 않는 행위까지 포함됩니다.

이에 대한 아주 간단한 예시를 살펴보겠습니다.

let array= [1, 2, 3];

function addValue(value) {
 	array.push(value);
 	return array;
}

위 예시는 전역으로 선언된 arraypush를 이용하여 값을 변경하게 됩니다. 따라서 불변성을 만족한다고 할 수 없습니다. 그렇다면 불변성을 만족하기 위해서는 어떻게 해야할까요?

function addValue(array, value) {
  	return [...array, value];
}

위 함수는 전달받은 arrayvalue를 사용하여 새로운 배열을 생성하여 전달하게 됩니다. array를 직접 변경하지 않고 복사본을 사용하여 작업을 진행했기 때문에 불변성을 지킨 함수라고 할 수 있습니다. 또한, 외부의 값과 상관없이 입력값으로만 결과값을 생성하기 때문에 동시에 순수 함수라고 할 수 있습니다.

🔗 순수 함수에 대한 깊은 이해가 필요하다면 여기를 참고해보세요.


선언형 함수

함수형 프로그래밍은 선언형 프로그래밍에 해당합니다. 선언형 프로그래밍은 어떻게가 아닌 무엇을 할 것인지를 주목하여 작성해야 합니다. 하나의 예시를 살펴봅시다.

function sum_1_to_n(n) {
	let result = 0;
  	for(let i = 1; i <= n; i++) result += i;
  	return result;
}

위 함수를 살펴봅시다. 입력값으로 주어진 n에 따라 1부터 n까지의 합을 구하여 반환하는 함수입니다. for문을 사용하여 반복 작업을 진행하는데, 이는 선언적이라고 할 수는 없습니다. 어떻게 작업을 수행할지에 대한 명령형에 가깝습니다. 이를 재귀로 표현하면 이 함수에서 무엇을 할지에 대해 표현하므로 선언형으로 표현이 가능합니다.

function sum_1_to_n(n) {
	if(n === 1) return 1;
  	else return n + sum_1_to_n(n - 1);
}

위와 같이 재귀를 사용하여 작성하는 것으로 선언형으로 작성할 수 있다는 점 이외에도 다양한 이점이 있습니다. 우선 for문을 사용하는 코드를 살펴보면 result 변수와 더불어 for문에서의 i 역시 상태(값)가 변화하게 됩니다. 재귀로 이를 표현하면 불변성을 만족할 수 있습니다.

또한 재귀로 함수를 구현하는 것으로 순수 함수로 작성할 수 있습니다. 이 때문에 재귀를 사용하는 것으로 선언형을 만족함과 더불어 다양한 함수형 프로그래밍의 조건을 만족할 수 있도록 코드를 작성할 수 있습니다.


1급 객체와 고차 함수

함수형 프로그래밍에서는 함수는 1급 객체로 취급한다고 합니다. 1급 객체가 무엇이길래, 그리고 왜 함수형 프로그래밍에서 이와 같이 취급하는지에 대해 알아봅시다.

[ 1급 시민 - first class citizen ]

  • 변수나 자료구조에 저장할 수 있다.
  • 함수의 매개변수로 전달할 수 있다.
  • 함수의 반환값으로 사용할 수 있다.

프로그래밍에서 1급 시민은 위와 같이 사용할 수 있음을 의미하며, 이러한 특징을 가진 객체를 1급 객체라고 부릅니다. 즉, 다시 설명하자면 함수를 변수나 자료구조에 저장할 수 있고, 매개변수로 전달하고 반환값으로 사용할 수 있도록 취급하는 것이 함수형 프로그래밍의 하나의 특징이라는 것입니다.

이렇게 함수를 1급 객체로 사용하는 것으로 함수를 단순히 사용만 하는 것에서 더 나아가 재사용 가능한 모듈 형태로 활용이 가능해집니다. 무엇보다 함수가 1급 객체인 것은 고차 함수를 구성하는 것이 가능하다는 점에서 중요하다고 할 수 있습니다. 그럼 고차 함수가 무엇일까요?

[ 고차 함수 - Higher-Order Function ]

  • 함수를 매개변수로 사용하거나 함수를 반환하는 함수를 의미합니다.

우리가 당연하게 사용하는 몇 가지 함수들은 이러한 고차 함수에 해당합니다. 여기에는 Array.map, Array.filter, Array.reduce 등이 있습니다. 이러한 고차 함수를 활용하거나 직접 구성하는 것으로 유연한 코드 작성이 가능해지는 것이 큰 장점입니다.

🔗 이러한 고차 함수가 왜 필요한지에 대한 내용은 여기를 참고해보세요.


글을 마무리하며...

단순히 듣고 넘기기 바빴던 함수형 프로그래밍에 대해 얕게 학습하기 위해 이번 포스팅을 진행하게 되었습니다. 함수형 프로그래밍은 하나의 패러다임이기 때문에 이번 포스팅에 포함된 내용은 그 일부에 불과합니다.

특히 함수형 프로그래밍이 왜 필요한지, 어떠한 이점을 가져올 수 있을지 등 다양한 부분에 대한 고민이 필요합니다. 이에 대한 내용은 추후 다른 포스팅으로 다루어 보도록 하겠습니다.

부족한 글 읽어주셔서 감사드리며, 잘못된 부분이 존재하거나 궁금한 점이 있으시다면 댓글 남겨주시면 감사드리겠습니다.

🔗 함수형 프로그래밍과 관련하여 여기를 참고해보시면 큰 도움이 될 것 같습니다.


참고

profile
festina lenta
post-custom-banner

0개의 댓글