회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
오랜 기간 개발을 공부하면서 여러 패러다임의 변화를 겪었는데, 기억에 남는 것 중 하나가 객체지향 패러다임에서 함수형 패러다임으로 넘어오는 일이었습니다. 힙스터 기질이 있던 저에게 함수형 프로그래밍은 굉장히 좋은 소재였습니다. 좋다고 하는데 아직은 비주류인, 나만 알고 싶은 그런 것들을 탐구하고 공부하는 것을 참 좋아하기 때문입니다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
오랜 기간 개발을 공부하면서 여러 패러다임의 변화를 겪었는데, 기억에 남는 것 중 하나가 객체지향 패러다임에서 함수형 패러다임으로 넘어오는 일이었습니다. 힙스터 기질이 있던 저에게 함수형 프로그래밍은 굉장히 좋은 소재였습니다. 좋다고 하는데 아직은 비주류인, 나만 알고 싶은 그런 것들을 탐구하고 공부하는 것을 참 좋아하기 때문입니다.
함수형 프로그래밍을 배우는 것은 험난한 여정이었습니다. 이렇다 할 지침서가 있는 것도 아니고 저마다 다른 방식으로 함수형 프로그래밍을 설명하고 있었기 때문이죠. 프론트엔드 개발자로서 JS에서 그리고 실전에서 사용할 수 있는 함수형 프로그래밍 체계를 잡기까지 오랜 시간이 걸렸습니다.
그렇게 함수형 프로그래밍을 배우면서 깨달은 것이 있습니다. 실제 함수형 프로그래밍의 본질은 그렇게 어려운 것이 아닌데 이걸 설명하기 위해서는 대단히 어려운 일이 많았습니다. 아마 함수형 프로그래밍에 쓰이는 용어들이 대부분 낯설기 때문일지도 모릅니다.
함수형 프로그래밍을 설명하기 위해 여러 번 글을 써보았지만, 용어 설명 이상의 제대로 된 설명이 어려웠습니다. 그러다 최근 함수형 프로그램을 더 깊이 공부하다가 실제로 알려주고 싶었던 것이 방법이나 용어가 아닌, 코드를 함수형으로 생각하는 ‘함수형 사고 패러다임’이라는 걸 깨달았습니다.
그래서 오늘은 함수형 사고 패러다임을 바탕으로 새롭게 함수형 프로그래밍에 관한 글을 써보려고 합니다.
우리들은 언제나 자신이 짜는 코드가 좋은 코드이기를 바랍니다. 그리고 끊임없이 '내가 짠 코드는 좋은 코드일까?'라고 고민을 합니다. 물론 좋은 코드의 명확한 기준은 없지만, 적어도 구조적으로 좋은 설계를 가지고 있을수록 좋은 코드라는 걸 우리는 알고 있습니다.
프로그램 덩치가 작을 때는 좋은 코드와 나쁜 코드에 대한 구분을 잘하지 못합니다. 오히려 그렇지 않은 코드의 생산성이 더 높기 때문이죠. 그러나 프로그램이 일정 이상의 크기를 가지게 됐을 때 코드의 설계가 없으면 점점 더 생산성이 떨어지게 됩니다. 그렇기에 우리는 좋은 설계를 유지하려는 노력이 필요합니다.
‘좋은 설계를 유지한다’라는 표현을 쓴 까닭은 한 번의 작업이 아니라 코드 전반에 걸쳐 일관적인 원칙과 규칙으로 작성이 되어야 하기 때문입니다. 이러한 원칙과 방법이 되는 관점을 우리는 패러다임이라고 부릅니다.
우리가 익히 들어 알고 있는 객체지향 프로그래밍 패러다임은 객체를 중심으로 사고하고 프로그램을 작성하는 것입니다. 반면 데이터를 함수로 연결하는 것을 중심으로 사고하고 프로그래밍을 하는 것을 함수형 프로그래밍(패러다임)이라고 부릅니다.
패러다임과 관련해서는 우리가 잘 아는 천동설과 지동설이 있습니다. 흔히들 ‘지동설이 맞고 천동설은 틀렸다’라고 알고 있습니다. 하지만 사실 천동설은 틀린 게 아니라 조금 더 복잡할 뿐 충분히 우주의 이동을 설명할 수 있습니다. 단지 과학자들은 지구를 중심에 두는 것보다 태양을 중심으로 둘 때 훨씬 더 간결하고 단순하게 설명할 수 있기 때문에 더 단순한 것을 채택했을 뿐입니다.
제가 쓴 다른 글에서 천동설 지동설을 포함한 프로그래밍 패러다임에 대한 이야기를 읽어 볼 수 있습니다.
함수형 프로그래밍은 객체지향 프로그래밍을 더 단순하게 그리고 간결하게 보도록 도와줍니다. 그러나 천동설과 지동설과는 달리 함수형 프로그래밍이 객체지향보다 반드시 더 나은 것은 아닙니다. 오히려 대부분의 언어는 객체지향으로 되어 있지만, 순수 함수형 언어는 극소수에 인지도도 낮은 상황이 이를 방증하고 있습니다. 그러나 자바스크립트가 쏘아 올린 작은 공이 프로그래밍 패러다임의 변화를 야기하였습니다.
자바스크립트는 함수형 프로그래밍 기반 위에 객체지향 언어의 껍데기를 씌운 언어입니다. 이렇게 다소 실험적으로 탄생한 이 언어로 인해 개발자들은 ‘객체지향에 함수형 프로그래밍을 적당히 섞으면 훨씬 더 좋다’라는 사실을 알게 됐습니다. 이후 탄생한 다른 언어에도 객체지향 언어에 함수형을 결합하는 형태의 멀티 패러다임의 근간을 마련했습니다.
‘자바스크립트(javascript)’를 창시한 Brendan Erich는 언어를 개발할 당시 유행하던 객체지향에 한계를 느끼고 LISP, scheme 등 함수형 프로그래밍에 관심을 가졌습니다. 그래서 함수형 프로그래밍 형태로 언어를 만들고 싶어 했지만, 그의 상사는 당시 개발자들이 제일 많이 쓰던 Java와 같은 문법으로 만들기를 요구했습니다. 결국 둘을 섞은 형태로 세상에 나오게 되었습니다.
제 글의 전반에서 나오는 얘기인데, 자바스크립트는 함수형 패러다임을 기반으로 하면서 객체지향의 문법을 쓰는 독특한 언어입니다. 그 말인즉슨, 우리가 자바스크립트를 가장 잘 활용하기 위해서는 객체지향으로 작성하면서도 함수형 프로그래밍 패러다임으로 개발하는 것이 가장 좋습니다.
자바스크립트를 활용한 함수형 패러다임 객체지향이 궁금하다면 이 글을 한번 읽어 보세요.
함수형 프로그래밍을 제대로 배운 적이 없더라도 Array Method, Promise, Event Listener, setTimeout, React의 hook이나 Redux와 같이 우리가 쓰고 있는 대부분의 JS 라이브러리나 API에 이 함수형 패러다임이 고스란히 녹아 있습니다. 너무 멀리 있는 개념이 아닌 셈입니다.
자바스크립트는 완전히 함수형 언어도, 완전히 객체지향 언어도 아닌 멀티 패러다임을 가진 매력적인 언어입니다. 어느 개념이든 원하는 대로 가져가 만들 수 있고 심지어는 이러한 체계 없이 어쨌든 돌아가는 코드를 만들기 너무 좋은 언어입니다. 물론 이 말은 반대로 언제든 나쁜 코드를 작성할 수 있다는 얘기이기도 합니다.
결국 이 둘의 패러다임을 잘 버무려서 쓸 수 있어야 자바스크립트로 좋은 코드를 작성할 수 있습니다. 무엇보다 프론트엔드 개발을 잘하기 위해서는 함수형으로 사고하는 패러다임을 잘 이해할 필요가 있습니다.
재밌는 것은 함수형 프로그래밍이 자바스크립트의 근간이 되는 하나의 큰 축인데도 이에 대한 개념이 잘 정리된 글들은 그리 많지 않습니다. Redux로 인해 프론트엔드 업계에서 함수형 프로그래밍이 한 차례 유행했는데도 정작 개념과 이론들은 아직까지 파편화되어 있습니다.
특히 함수형 프로그래밍 학습과정은 실용적인 관점보다 학술적인 관점으로 발전해왔습니다. 그래서 순수함수, 불변성, 1급객체, 고차함수, 커링과 같은 다소 난해한 용어들이 초기 진입장벽으로 작용합니다. 어려운 함수형 프로그래밍이 이러한 용어로 인해 더 어렵게 느껴집니다.
그래서 함수형 프로그래밍을 검색하면 매일 보게 되는 어려운 용어 대신 액션, 계산, 데이터, Copy on Write, 방어적 복사, 함수 계층 구조, 추상화 벽, 명시적 입출력 등 개발자들에게 조금 더 직관적인 용어를 통해 함수형 프로그래밍을 다시 한번 설명하고자 합니다.
함수형 프로그래밍에서는 ‘순수함수’, ‘불변성’, ‘선언적 패턴’이라는 3가지 요소가 굉장히 중요합니다. 하지만 학술적으로는 해당 요소를 이해해도 실무에 적용하려면 이해하기가 힘듭니다. 그래서 이 글에서는 같은 개념을 다음과 같은 방식으로 이야기하고자 합니다.
다시 풀어낸 문장 역시 이해가 쉽게 되지는 않겠지만, 학술적인 용어보다는 이렇게 실무적으로 풀어낸 뜻이 함수형 프로그래밍을 그나마 이해하기 쉬울 것입니다.
함수형 프로그래밍은 대개 순수함수(Pure-Function)와 부수효과(Side-effect)의 정의로 시작하며, ‘부수 효과를 멀리하고 순수함수의 합성을 통해서 프로그래밍하자’라고 설명합니다. 보통 이를 처음 접하면 ‘부수 효과와 가변된 상태를 멀리하고 순수함수로 프로그래밍하라’고 이해합니다. 하지만 대부분 프로그램의 목적은 부수 효과에 있습니다. 서버에서 데이터를 조회하고 화면을 변경하고 로그를 남기고 파일을 읽고 쓰는 행위들을 하지 않는 프로그램은 의미가 없습니다. 순수함수만 가지고는 우리가 만들고자 하는 응용프로그램이 되지 않습니다.
이런 잘못된 오해를 부르는 용어 대신 ‘함수형 프로그래밍은 프로그램을 크게 액션, 계산, 데이터 등 3가지로 구분하여 만드는 것’이라고 다시 정의해 봅시다. 그렇다면 조금 더 함수형 프로그래밍이 하고자 하는 방향성에 대해 명쾌하게 이해를 할 수 있을 것입니다.
이제부터 프로그램을 액션, 계산, 데이터로 구분해서 설명하겠습니다. 일단은 각각의 핵심적인 특징만 한번 짚어보고 자세히 한번 알아보겠습니다.
새로운 것을 익힐 때는 일단 막연하게라도 ‘대충 뭔지 알겠다’라는 느낌을 받는 것이 중요합니다. 이런 느낌을 받기 편하도록 프론트엔드 개발자에게 익숙한, 버튼을 누르면 숫자가 올라가는 카운터 프로그램을 간단하게 작성했습니다.
<button id="button">0</button>
<script>
document.getElementById("button").onclick = function() {
button.innerText++
}
</script>
위 프로그램을 새롭게 함수형 프로그래밍의 정의에 따라 액션과 계산 그리고 데이터로 한 번 분리해보면서 감을 한번 잡아봅시다. 느낌으로 어디가 액션이고, 계산이고, 데이터일지 상상해 보는 게 중요합니다.
데이터는 이벤트에 대한 사실입니다. 이 프로그램에서 숫자가 바로 데이터입니다. 데이터는 화면에 보여줄 수 있습니다. 액션은 실행 시점이나 횟수에 의존합니다. ‘사용자가 버튼을 클릭했을 때 숫자 1이 커지는 것을 언제 하느냐’에 따라서 혹은 ‘여러 번 클릭할수록’ 다른 결과가 만들어지기에 액션입니다. 계산은 입력값을 통해 출력값을 만들어 내는 것입니다. 이 프로그램에서는 클릭하면 기존에 있는 숫자에 1을 더해 새로운 숫자를 만들어내는 계산을 하고 있습니다. 위 프로그램에서 각자의 관계는 액션이 발생하면 미리 정의된 계산에 의해 데이터가 바뀌게 됩니다.
함수형 프로그래밍의 관점으로 다시 한번 코드를 작성해봅시다.
// 함수형 프로그래밍 관점에서 분리해보자.
function App() {
// 데이터
const [count, setCount] = useState(0)
// 계산
const increase = (value) => value + 1
// 액션
const onClick = () => setCount(increase(count))
// 선언적 패턴
return <button onClick={onClick}>{count}</button>
}
함수형 프로그래밍은 멀리 있지 않았습니다. 우리가 현재 쓰고 있는 이러한 패턴은 이미 함수형 패러다임이 충분히 반영된 코드였습니다. 우리가 여기서 주목할 코드는 바로 increase를 별도의 함수로 만들었다는 점입니다.
// 잘못 분리된 계산함수
const increase = () => {
...
setState(count + 5)
}
const onClick = () => {
...
increase()
}
계산은 반드시 입출력으로 이루어져야 하며, 같은 입력에 대해서는 항상 같은 출력값을 내놓아야 합니다. 계산이 여러 번 실행되어도 외부 세계에 영향을 주지 않아야 합니다. 위 예시에는 쓰인 increase 함수는 실행 횟수에 따라 시점에 따라 달라지므로 계산처럼 보이지만 계산이 아니라 액션입니다.
함수형 프로그래밍의 핵심은 액션과 계산을 확실히 분리해서 액션을 최소화하고 계산함수를 많이 만들어서 관리하는 것을 목표로 합니다.
함수는 언제나 입출력이 존재합니다. 자바스크립트 코드는 매우 자유롭기 때문에 명시적인 인자와 리턴값외에도 암묵적인 입출력이 존재할 수 있습니다.
const increase = () => {
...
// count는 함수 외부에서 왔으므로 암묵적 입력입니다.
const result = count + 1 // 1이라는 값은 변경할 수 없는 암묵적 입력입니다.
setState(result) // 함수 외부에 있는 값을 변경하는 암묵적 출력입니다.
...
// result는 명시적 출력입니다.
return result
}
const action = () => {
...
increase()
}
위와 같이 하나의 함수에 암묵적 입출력과 명시적 입출력이 섞여 있다면 계산이 아니라 액션입니다. 액션은 실행회수와 시점에 의존하므로 테스트하기가 어렵습니다. 액션과 계산이 섞여 있는 함수라면 계산 부분을 분리해야 합니다.
우선 암묵적인 입출력을 제거하도록 합니다.
const increase = (count, offset) => {
const result = count + offset // count와 offset을 명시적 입력으로 변경합니다.
// setState와 같은 명시적 출력은 사용하지 않습니다.
return result
}
// 암묵적인 입출력은 별도의 action에 모아줍니다.
const action = () => {
setState(increase(count, 1)) // 외부에서 필요한 모든 입력을 넣어줍니다.
}
예시가 하나도 현실적이지 않은 점 양해 부탁드립니다.
이렇게 만들어진 계산은 이제 독립적입니다. 언제든 재사용이 가능하며 테스트하기에도 용이합니다. 계산은 조립해도 언제나 같은 결과를 만들기 때문에 조합에 의한 폭발적인 테스트 시나리오를 만들지 않도록 도와줍니다. 계산은 명시적인 입력과 출력만을 가지며 어떠한 부수 효과도 만들어내지 않습니다. 같은 입력에 대해서는 언제나 같은 결과만을 만들어내야 합니다.
함수형 프로그래밍을 순수함수와 부수 효과가 아니라 액션 - 계산 - 데이터의 관점으로 이해해보면 조금 더 쉽게 이해할 수 있을 것입니다.
프로그램은 곧 데이터의 변화입니다. 그리고 데이터는 액션에 의해 변합니다. 데이터가 변하는 방법을 따로 독립적으로 계산했을 때 액션과 계산과 데이터를 함수로 연결하여 작성하는 개념이 바로 ‘함수형 프로그래밍 패러다임’입니다. 특히 실행 시점과 횟수에 의존적인 액션에서 독립적인 계산을 분리하여, 복잡한 코드를 간단하게 만들고 테스트를 쉽게 만드는 것이 중요합니다.
순수함수: 코드를 액션과 계산, 데이터로 분리하자. 특히 액션에서 계산을 분리하는 코드를 작성하자.
다시 강조하지만, 계산은 여러 번 실행해도 외부 영향에 값이 변경되지 않아야 합니다. 하지만 함수가 숫자나 문자열이 아닌 객체나 배열을 사용한다면, 자바스크립트는 기본적으로 pass by reference 방식을 사용합니다. 따라서 언제든 외부에서 값이 수정되거나 함수 내부에서 외부에 영향을 미칠 수 있는 점을 알아야 합니다. 이걸 방지하기 위해서는 객체나 배열을 pass by value 형태로 변경하는 방식을 알아야 합니다.
앞서 만든 카운터 프로그램에서 요구사항을 살짝 변경해보겠습니다. 버튼을 누를 때마다 숫자를 더하는 게 아니라 배열을 만들어 놓고, 배열 값을 하나씩 늘려가는 형태로 만들어봅시다. 그러면 기존의 코드는 어떻게 바뀌어야 할까요?
// 1씩 증가하는 계산 함수
const increase = (value) => value + 1
// 1씩 증가하는 값을 배열에 넣는 함수 ex) [1] -> [1,2] -> [1,2,3]
const increase = (arr) => {
const value = arr[arr.length - 1]
arr.push(value + 1)
return arr
}
// 이렇게 작성을 하면 어떤 문제가 있을까?
이렇게 만들어진 코드는 계산이 아니라 액션이 됩니다. 어떤 부분이 문제가 되는지 한번 살펴봅시다.
자바스크립트에서 Object나 Array와 같은 덩치가 큰 값을 다룰 때에 pass by refereence라는 방식을 통해 원본을 그대로 전달합니다. 그리고 원본을 직접 수정할 수 있는 방식을 통해 효율적으로 값을 조작할 수 있습니다.
그러나 함수형 프로그래밍 세계에서 Array나 Object를 다룰 때는 조심해야 합니다. 계산(순수함수)은 함수의 동작이 외부 세계에 영향을 끼치지 않아야 하고, 실행회수와 시점과는 무관해야 합니다. 함수에서 Array나 Object의 원본 값을 직접 수정하면 메모리상으로는 효율적이겠지만, 외부 세계에 영향을 끼치지 말아야 한다는 제약조건을 깨게 됩니다.
그렇다면 배열이나 객체의 값을 조작하지 않고서 어떻게 계산을 할 수 있을까요? 원본을 직접 수정하는 것이 문제라면 pass By Value처럼 값을 복사해서 수정하면 됩니다. 이를 통해 원본을 건들지 않고도 원하는 계산을 할 수 있습니다.
const increase = (arr) => {
arr = arr.slice() // array를 조작하기 전에 복사해서 사용한다.
const value = arr[arr.length - 1]
arr.push(value + 1)
return arr
}
// spread 표기법을 쓴다면 더 간결하게 작성할 수 있다.
const increase = (arr) => [...arr, arr[arr.length - 1]]
이렇게 값을 조회하고 변경하여 출력값을 만들어야 할 때 원본의 값을 복사해서 수정하면 외부 세계에 영향을 끼치지 않는 계산이 됩니다. 이러한 방식이 ‘카피 온 라이트(Copy on Write)’ 혹은 얕은 복사입니다. 우리는 카피 온 라이트 방식을 통해서 액션을 계산으로 만들 수 있습니다. Object에서도 마찬가지 방법으로 변경된 값을 원본 수정없이 출력할 수 있습니다.
const setObjectName = (obj, value) => {
return {
...obj
name: value
}
}
const setObjectName = (obj, name) => ({...obj, name})
앞서 배운 카피 온 라이트 방식으로 액션을 계산으로 변경을 할 수도 있습니다. 그렇지만 만약 해당 액션이 우리가 수정할 수가 없는 라이브러리라면 어떻게 할까요? 계산 함수에 액션이 하나라도 존재한다면 그 함수는 액션이 됩니다. 그렇게 만들어진 액션들은 코드 전체에 퍼져서 개발을 어렵게 만듭니다. 이러면 어떻게 해야 할까요?
// 액션을 써야하지만 라이브러리 함수라서 내가 수정할 수가 없다.
import someActionLibray from "lib"
const someCalcuation = (obj, value) => {
someActionLibray(obj, value) // obj의 값을 변경해서
return obj // 출력한면 이 함수는 계산일까?
}
이렇게 특수한 경우 혹은 mutaion 함수를 이용해야만 하는 경우에는 방어적 복사라는 기법을 이용할 수 있습니다.
const someCalcuation = (obj, value) => {
const clone = structuredClone(obj); // 완전한 clone을 만들어 낸다.
someActionLibray(clone, value) // clone값을 변경해도 원본은 변하지 않는다.
return clone
}
이렇게 중첩된 모든 구조를 복사하는 방식을 깊은 복사라고 부릅니다. JS에서는 원래 이러한 기능이 없었지만, 최근 structuredClone() API가 Native 기능을 지원하니 이를 사용하면 됩니다.
IE에서는 아직 지원하지 않는 API이므로 필요할 경우에는 polyfill을 사용할 수도 있습니다. 이렇듯 외부 세계의 API 중 mutaion한 함수를 해야 하는 경우에는 방어적 복사 혹은 깊은 복사를 통해서 액션을 계산으로 만들 수 있습니다.
불변성: 카피 온 라이트와 방어적 복사를 이용하여 불변성을 유지하자
지금까지의 내용을 통해 함수형 프로그래밍은 코드를 액션 - 계산 - 데이터로 구분하고, 불변성을 이용해 액션에서 최대한 계산을 분리하자고 하였습니다. 이렇게 각 영역을 함수로 구분을 짓다 보면 자연스럽게 좋은 구조를 만들어 낼 수가 있습니다.
코드를 작게 분리하면 좋은 점
그리고 우리는 이렇게 분리한 코드를 조합하는 과정에서 자연스레 함수 간의 계층이 생기는 것을 알 수 있게 됩니다.
최초 예를 들었던 코드를 다시 가져와 보았습니다. 그리고 한번 함수들이 어떠한 계층을 가지는지 그림으로 표현해보았습니다.
위와 같이 함수들이 이룬 계층과, 계층별로 어떠한 유형의 함수들이 추가될지 예상해서 그림을 추가해보았습니다. 어떤가요? 새로운 기능을 구현하려고 한다면 각 코드가 어느 계층에 들어가야 할지 상상이 잘 되나요?
이렇게 액션-계산-데이터로 코드를 구분하고, 계층을 만들고, 계층을 넘나들지 않는 코드를 짜다 보면 자연스럽게 좋은 코드의 구조를 만들 수 있습니다. 여기서 계산의 비중을 높여가고 계층을 넘나들지 않도록 코드를 쪼개다 보면 좋은 설계와 리팩토링에 대한 좋은 근거가 될 수 있습니다.
액션으로 갈수록 코드의 형태는 ‘무엇을 하는 것인지’ 행동을 기반한 기획서에 가까운 코드가 만들어지며, 데이터 구조를 몰라도 되는 형태의 코드를 작성하게 됩니다. 반면 계산과 데이터에 가까워질수록 데이터 중심적인 코드를 작성하게 되고 상대적으로 재사용성이 높고 테스트하기 좋은 코드 형태를 갖추게 됩니다.
이런 식으로 계층을 나누고 각 계층을 침범하지 않도록 코드를 작성하면 자연스럽게 추상화 벽이라는 것이 만들어지게 되면서 벽 상단으로의 코드 변화가 하단에 영향을 미치지 않고 하위의 코드 변화가 상위에 영향을 주지 않도록 할 수 있습니다.
우리가 API를 콜할 때 mock으로 작성하던, 실제로 연결하던, dev서버에 연결하던, 동일한 API 콜이 호출하는 것과 유사한 맥락입니다.
이렇게 계층이 견고해지는 구조로 작성하게 되면 유연하면서도 변화에 국지적인 형태의 좋은 설계를 가져가게 됩니다. 상위에는 ‘무엇을 해야 할지’ 같은 기획서에 가까운 선언적 패턴으로 코드를 가져갈 수 있고, 하위에는 테스트가 쉬운 코드 조각들로 구성이 되는 좋은 설계 방향의 코드가 만들어지게 됩니다.
선언적 패턴: 계층형 설계와 추상화 벽을 이용하여 ‘무엇’과 ‘어떻게’를 구분하여 좋은 설계를 유지하자.
지금까지 함수형 프로그래밍이 지향하는 방향과 함수형 사고에 대해서 알아보았습니다. 프로그램을 액션-계산-데이터의 영역으로 구분하고, 각 코드를 부수 효과가 있는 함수-순수함수-데이터로 분리하여 작성하면 엉키지 않고 계층별로 잘 정리된 코드를 만들 수 있다고 설명했습니다. 특히 액션에서 계산과 데이터를 많이 분리해낼수록 테스트가 쉬워지고 재사용과 조합을 하기 좋은 코드가 된다고 강조했습니다.
설명만으로는 분명 간결하고 좋은 방식처럼 느껴집니다. 하지만 이러한 함수형 프로그래밍이 현재 널리 쓰이지 않는 이유는 무엇일까요? 개인적으로 제가 느끼는 이유를 한번 공유해보고자 합니다.
함수형 프로그래밍 세계가 아닌 곳에는 원본을 넘기고 원본의 특정 값을 바로 수정합니다. 그편이 메모리 관점에서는 훨씬 더 효율적이기 때문입니다. 자바스크립트는 순수 함수형 프로그래밍 언어가 아니기 때문에 자바스크립트에서 copy를 하는 형태의 문법이 존재하지 않습니다. 그래서 중첩된 객체에 대해서는 아래와 같이 다소 복잡한 형태의 문법을 사용해야 합니다.
// 원본값을 직접 수정하는 액션
const someMutationAction = (obj) => {
obj.foo.bar.baz = 200
}
// 계산
const someCalcuation = (obj, value = 200) => {
return {
...obj,
foo: {
...obj.foo,
bar: {
...obj.foo.bar,
baz: value
}
}
}
}
이런 식으로 계속 복잡한 코드를 만드는 대신 함수나 기타 라이브러리를 통해서 조금 더 간결하게 만들 수도 있습니다.
// immutable-js
// Object, Array대신 불변객체를 제공하는 라이브러리
const obj = Map({foo: {bar: baz: 200}}});
obj.setIn(['foo', "bar", "baz"], 50);
// immer.js
// proxy를 이용한 라이브러리
const nextState = produce(obj, draft => draft.foo.bar.baz = 200)
이러한 자바스크립트의 문법적인 한계로 인한 약점으로 함수형 프로그래밍은 바닐라 JS로만 하기에는 코드가 복잡해집니다. 결국 함수형 라이브러리 하나를 익혀야 하는 과제가 존재합니다.
함수형 프로그래밍으로 외부 환경에 영향을 주지 않는 계산 함수를 만들면 언제든 재사용이 가능한 함수가 됩니다. 이러한 함수들을 쌓이면 반복적으로 만들어지는 코드 패턴에 대응하는 유틸리티 함수들이 많이 생깁니다.
const array = [
{name: 'jack', age: 14},
{name: 'jill', age: 15},
{name: 'humpty', age: 16}
];
// 특정 속성만 추출하고 싶을 때,
const ages = array.map(c => c.age)
const names = array.map(c => c.name)
// 반복되는 패턴을 함수화 한다.
const pluck = (array, prop) => array.map(c => c[prop])
const ages2 = pluck(array, "age")
이런 식으로 간단하고 유용한 함수들을 쓰다 보면 언젠가 내가 분명히 귀찮게 작성했던 코드들을 훨씬 간결하고 풍성한 문법으로 코드를 작성을 할 수 있게 됩니다. 마치 알고 있는 단어가 많아질수록 말을 더 잘하는 것처럼 말이죠.
저도 함수형 패러다임으로 설명을 시작했지만, 조금 더 배우다 보면 결국 함수를 다루는 테크닉이나 방법을 배우게 됩니다. 이러한 테크닉과 함수형 패러다임이 만나게 되면 비슷한 형태의 함수형 도구들이 많아지게 됩니다. 그러다 보면 대부분 실전에서 필요한 코드들의 형태가 비슷해지고, 이러한 유틸리티들을 새롭게 구현하기보다는 이미 만들어진 함수형 도구들을 사용하는 편이 낫다는 결론에 이르게 됩니다.
그렇게 발전한 lodash와 ramda와 같은 함수형 라이브러리들은 처음에는 그 편리함 덕분에 인기를 끌었습니다. 하지만 점점 방대해지다 보니 오히려 코드가 더 어려워지는 효과가 생겼습니다.
이러한 이유로 유틸리성 함수형 프로그래밍 라이브러리 등은 더 발전하거나 다채로운 코드가 만들어지지 않아 호불호가 갈리는 방식이 되었습니다.
앞서 소개했듯이 결국 순수함수형을 깊게 파고 발전하는 것은 더욱 함수형 프로그래밍을 어렵게 만드는 방향이 되었습니다.
그래서 나온 것들은 함수형 프로그래밍 개념을 이용해 상태관리를 하는 Redux라거나 반응형 프로그래밍에 함수형 프로그래밍 개념을 엮은 rxjs, 불변성 관리를 위해서 만들어진 immutable.js, 날짜만 다루는 함수형 라이브러리 date-fns 등입니다. 이처럼 특정 목적성을 가진 여러 라이브러리가 함수형 패러다임을 적절히 결합하는 방식으로 발전하고 있습니다.
자바스크립트는 함수형 패러다임은 지향하지만, 언어 레벨에서는 순수 함수형 기능 등을 문법적으로 제공하지는 않습니다. 그러다 보니 함수형 프로그래밍하기 위해서는 특정한 라이브러리나 혹은 그에 준하는 유틸리티 함수를 직접 만들어서 사용해야 하는 문제가 있습니다.
그러다 보면 ‘내가 함수형 라이브러리를 다 만들 바에 함수형 라이브러리를 써야겠다’라고 생각하면서 기존의 함수형 프로그래밍을 배우게 됩니다. 그렇게 순수함수와 부수 효과를, 1급 객체로써 함수를 다루는 방법을, 체이닝과 컬렉션을, 특정 함수형 라이브러리를 다루는 방법과 같은 형태로 학습하다가 어느새 실무와는 점점 멀어지게 됩니다.
그래서 이번 글은 함수형 프로그래밍에 대한 용어 설명이나 테크닉이 아닌 함수형 패러다임과 함수형 사고에 관련된 이야기에 많이 집중하였습니다. 이를 정리하면 아래와 같습니다.
실전에서 적용하기 좋은 함수형 사고
함수형 라이브러리를 이용하여 온전히 모든 프로그래밍을 함수형 프로그래밍하지 않더라도 일부 이러한 패러다임을 조금씩 코드에 적용을 시켜본다면 훨씬 더 좋은 코드를 작성하는 데 도움이 될 거로 생각합니다.
StateOfJS 설문조사에서 실시한 내용 중 자바스크립트에서 부족하다고 느끼는 영역 중에 무려 3가지나(하위권입니다만) 함수형 프로그래밍과 관련된 내용입니다. JS에서 불변성에 대한 지원과 Pipe Operator가 정식 문법이 된다면 훨씬 더 함수형 프로그래밍하기 좋아질 것이라고 기대합니다.
자바스크립트는 함수형 프로그래밍할 수 있도록 만들어졌지만, 함수형 프로그래밍을 위해서 만들어진 언어는 아닙니다. 그래서 온전한 함수형 프로그래밍을 하기 위해서는 반드시 별도의 코딩이 필요하며 이는 곧 관련 라이브러리가 필요하다는 의미입니다. 결국 그러한 라이브러리를 만들기 위해서는 이러한 함수형 프로그래밍의 기초지식을 바탕으로 라이브러리를 사용하거나 제작하는 방법을 배우는 것이 종착지가 됩니다.
대부분의 함수형 프로그래밍의 책들이 특정한 함수형 프로그래밍 라이브러리를 사용하거나 만드는 방법의 책들로 되어 있는 것은 이러한 이유입니다. 저 역시 rxjs를 실무에서 잘 사용하는 것이 함수형 프로그래밍의 종착지라고 생각했으니까요.
그렇지만 함수형 프로그래밍을 배운다는 것은 lodash나 Ramda, 혹은 rxjs를 익히기 위한 수단이라거나 함수형 라이브러리를 만들기 위한 것은 아닙니다. 함수형 사고개념이 없는 상태에서 함수형 라이브러리만 쓴다면 결코 좋은 코드가 만들어지지 않을 것입니다. 함수형 프로그래밍의 개념을 익히는 것은 좋은 코드를 볼 수 있는 새로운 관점을 얻게 되는 과정입니다. 그리고 그 관점으로부터 ‘이렇게 하면 좀 더 나은 프로그래밍을 작성할 수 있다’는 것을 알게 되는 것이 가장 중요한 성취일 것입니다.
서두에도 밝혔듯이 그렇다고 전부 100% 함수형으로 바꾸는 것이 더 나은 결과는 또 아니었기에 함수형 프로그래밍에 집착해서 더 나은 방식을 뒤로 하고 굳이 불편한 길을 걷지 않아도 됩니다. 언제나 이 밸런스가 중요합니다.
함수형 프로그래밍에 대한 이론 지식은 사실 운전면허 필기시험과도 같습니다. 아무리 많이 알아도 실제 운전실력은 이론이 아니라 경험에서 나오죠. 그래서 실제로 많이 코드를 작성해보면서 이러한 개념들이 체화되기를 바랍니다.
이 글을 통해 객체지향과는 다른 맛의 좋은 코드를 만들 수 있는 관점이 되는 함수형 프로그래밍에 대한 어떤 흥미와 그리고 도입 그리고 어떻게 익혀야 될지에 대한 방향성, 특히 당장 함수형 프로그래밍하지 않더라도 내 코드를 더 좋게 만드는 하나의 관점을 알게 된 터닝 포인트가 되는 시간이 되었기를 바랍니다.
함수형 프로그래밍에 대해 궁금한 내용이 있으면 댓글을 남겨주세요.
좋아요
댓글
공유
공유
좋아요
댓글
스크랩
공유
공유