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

teo·6일 전
85

테오의 프론트엔드

목록 보기
24/24
post-thumbnail

프롤로그

참 좋은데 어떻게 표현할 방법이 없네...

오랜 기간 개발을 공부하게 되면서 여러가지 패러다임의 변화를 겪었는데 그 중 인상깊었던 것 중 하나가 객체지향 패러다임에서 함수형 패러다임으로 넘어오는 것이었습니다.

홍대병 힙스터 기질이 있던 저에게 함수형 프로그래밍은 굉장히 좋은 소재였습니다. 좋다고는 하는데 아직까지는 비주류인 나만 알고 싶은 그런 것들을 탐구하고 공부하는 것을 저는 참 좋아합니다.

함수형 프로그래밍을 배우는 것은 험난한 여정이었습니다. 이렇다 할 지침서가 있는 것도 아니고 저마다 다른 방식으로 함수형 프로그래밍을 설명하고 있었기 때문이죠. 프론트엔드 개발자로써 JS에서 그리고 실전에서 사용할 수 있는 함수형 프로그래밍 체계를 잡기까지에는 오랜 시간이 걸렸습니다.

그렇게 함수형 프로그래밍을 깨닫고 나서 알게 된 것은 실제 함수형 프로그래밍의 본질은 사실 그렇게 어려운게 아닌데 이걸 대단히 어렵게 설명을 하고 있다는 것이었습니다. 함수형 프로그래밍이 실전보다는 학술적인 이론을 바탕으로 먼저 성장을 했기에 실전에 적용하는 방법보다는 모호한 이론 설명이 더 많았기 때문입니다.

저 역시 함수형 프로그래밍에 대해서는 잘 설명을 해 보기 위해서 글쓰기를 시도해보았지만 순수함수나 파이프, Array Method의 용어 설명 이상의 제대로 된 설명을 잘 못하고 있었습니다.

그러던 중 정말 좋은 함수형 코딩과 관련된 책을 알게되었습니다. 어느 분 요청으로 책 내용을 검토하면서 목차를 보고서는 바로 구매를 하였습니다. 그 동안 실전과 관련없거나, 함수형 라이브러리를 설명하고 있거나, 함수합성, 체인, 커링 등 함수형 프로그래밍 테크닉만 설명하는 책과는 달랐습니다.

이 책을 읽으면서 그간 함수형 프로그래밍에 대해서 알려주고 싶었던 것은 함수형 프로그래밍의 용어 설명이나 테크닉이 아니라 코드를 함수형으로 생각하는 힘인 함수형 사고 패러다임이었다는 것을 다시 한번 깨달았습니다.

그래서 다시 한번 함수형 프로그래밍에 대해 글을 써보기로 하였습니다. 이 책에서 얘기하고 있는 멘탈 모델을 좀 따라가면서 제 생각을 바탕으로 함수형 프로그래밍에 대한 이야기를 다시 시작해봅니다.

함수형 프로그래밍을 알아야 이유는 뭘까요?

우리들은 언제나 자신이 짜는 코드가 좋은 코드이기를 바랍니다. 그리고 끊임없이 고민을 하게 되죠.

'내가 짠 코드는 좋은 코드일까?'

좋은 코드에 대해서는 명확한 기준이라는 없지만 적어도 구조적으로 좋은 설계를 가지고 있을수록 좋은 코드가 된다는 것을 우리는 알고 있습니다.

좋은 코드와 나쁜 코드의 기준은 기존에 만들어 둔 코드가 내 발목을 잡는지 아니면 도와주는지의 차이!

프로그램의 덩치가 작을 때에는 좋은 코드와 나쁜 코드에 대해서 구분을 잘 하지 못합니다. 오히려 그렇지 않은 코드의 생산성이 더 높기 때문이죠. 그러나 프로그램이 일정 이상의 크기를 가지게 되면 설계가 없는 코드에서는 점점 더 생산성이 떨어지게 됩니다. 그렇기에 우리는 좋은 설계를 유지하려는 노력이 필요하죠.

좋은 설계를 유지한다는 표현을 쓴 까닭은 좋은 설계라는 것은 한번의 작업이 아니라 코드 전반에 걸쳐 일관적인 원칙과 규칙으로 작성이 되어야 하기 때문입니다. 이러한 원칙과 방법이 되는 관점을 우리는 패러다임이라고 부릅니다.

우리가 익히 들어 알고 있는 객체지향 프로그래밍 패러다임은 객체를 중심으로 사고하고 프로그램을 작성하는 것입니다.

반면 데이터를 함수로 연결하는 것을 중심으로 사고하고 프로그래밍을 하는 것을 함수형 프로그래밍 (패러다임) 이라고 부릅니다.

함수형 프로그래밍 패러다임은 프로그램을 이해하는 새로운 관점을 제공한다.

패러다임과 관련해서는 우리가 잘 아는 천동설과 지동설이 있습니다. 흔히들 지동설은 맞고 천동설을 틀리다고 알고 있지만 사실 천동설은 틀린게 아니라 조금 더 복잡할 뿐 충분히 우주의 이동을 설명할 수 있다고 합니다. 단지 지구를 중심에 두는 것보다 태양을 중심으로 둘 때 훨씬 더 간결하고 단순하게 설명을 할 수 있기 때문에 과학은 더 단순한 것을 채택했을 뿐입니다.

제가 쓴 다른 글에서 천동설 지동설을 포함한 프로그래밍 패러다임에 대한 이야기를 읽어 보실 수 있어요.

프로그래밍 패러다임과 반응형 프로그래밍 그리고 Rx
https://velog.io/@teo/reactive-programming

함수형 프로그래밍은 객체지향 프로그래밍에 더 단순하게 그리고 간결하게 프로그램을 바라볼 수 있도록 도와줍니다. 그러나 천동설과 지동설과는 달리 함수형 프로그래밍이 객체지향보다 반드시 더 나은 것은 아닙니다. 오히려 대부분의 언어가 객체지향으로 되어 있고 순수 함수형 언어가 극소수에 인지도가 낮다는 점이 이를 반증하고 있지요.

그러나 자바스크립트가 쏘아올린 작은 공은 프로그래밍 패러다임의 변화를 야기하였습니다. 자바스크립트는 함수형 프로그래밍 기반 위에 객체지향 언어의 껍데기를 씌운 언어입니다. 이렇게 다소 실험적으로 탄생한 이 언어는 객체지향에 함수형 프로그래밍을 적당히 섞으면 훨씬 더 좋다는 것을 개발자들에게 알려주었고 이후 탄생한 다른 언어에도 영향을 끼치며 객체지향 언어에 함수형을 결합하는 형태의 멀티 패러다임의 근간을 마련해 주었습니다.

우리가 하는 자바스크립트 언어는 멀티 패러다임 언어

javascript를 창시한 Brendan Erich는 언어를 개발할 당시 유행하던 객체지향에 한계를 느끼고 LISP, scheme등 함수형 프로그래밍에 관심을 가지고 있었기에 함수형 프로그래밍의 형태로 언어를 만들고 싶어 했습니다. 하지만 Netscape의 그의 상사는 당시 개발자들이 제일 많이 쓰던 Java와 같은 문법으로 만들기를 요구했기에 결국 둘의 혼종의 형태로 세상에 나오게 되었습니다. :)

제 글의 전반에서 나오는 얘기인데, 자바스크립트는 함수형 패러다임을 기반으로 하면서 객체지향의 문법을 쓰는 독특한 언어입니다. 결국 우리가 쓰는 자바스크립트를 가장 잘 쓰기 위해서는 객체지향스럽게 작성을 하면서도 함수형 프로그래밍 패러다임으로 개발하는 것이 가장 좋다는 것이겠지요.

자바스크립트에서 객체지향 프로그래밍이 궁금하다면 이 글을 한번 읽어 보세요 :)

객체지향 프로그래밍과 javascript (약간의 역사를 곁들인...)
https://velog.io/@teo/oop

함수형 프로그래밍을 제대로 배운적이 없더라도 Array Method, Promise, Event Listener, setTimeout, React의 hook이나 Redux와 같이 우리가 쓰고 있는 대부분의 JS 라이브러리나 API에서 이 함수형 패러다임이 고스란히 녹아 있기에 너무 멀리 있는 개념이 아닙니다.

자바스크립트는 완전히 함수형 언어가 아니고 완전히 객체지향 언어도 아닌 멀티 패러다임의 매력적인 언어입니다. 어느 개념이든 원하는 대로 가져다가 만들 수 있고 심지어는 이러한 체계없이도 어쨌든 돌아가는 코드를 만들기 너무 좋은 언어입니다. 이 말은 반대로 언제든 나쁜 코드 역시 쉽게(!) 작성할 수 있다는 얘기이기도 합니다.

결국 이 둘의 패러다임을 잘 구분해서 잘 섞어서 쓸 수 있어야 Javascript로 좋은 코드를 작성할 수 있다는 의미이며 프론트엔드 개발을 잘 하기 위해서는 함수형으로 사고하는 패러다임을 잘 이해할 필요가 있습니다.

📕 쏙쏙 들어오는 함수형 코딩

재밌는 것은 함수형 프로그래밍은 Javascript의 근간이 되는 하나의 큰 축인데 반해 이에 대한 개념적인 내용들이 잘 정리된 내용들은 그리 많지 않습니다. Redux로 인해 프론트엔드 씬에서 함수형 프로그래밍이 한 차례 유행을 했음에도 함수형 프로그래밍에 대한 개념과 이론들은 아직까지 파편화되어 있습니다.

제가 이 책이 마음에 들었던 이유는 함수형 프로그래밍을 굉장히 이론적으로 복잡하게 만들어 설명하거나 함수형 프로그래밍의 테크닉을 다루는 것이 아니라 함수형 사고의 본질과 개념을 중심으로 쉽게 설명을 하고 있다는 점입니다.

특히 함수형 프로그래밍 학습의 초기 진입장벽인 순수함수, 불변성, 1급객체, 고차함수, 커링과 다소 난해한 용어들을 쓰지 않고 저자만의 철학을 통해 조금 더 쉽게 재정의한 액션, 계산, 데이터, Copy on Write, 방어적 복사, 함수 계층 구조, 추상화 벽, 명시적 입출력, 타임라인, 커팅 등의 새로운 용어를 통해서 실용적인 측면에서의 함수형 프로그래밍의 본질을 조금 더 쉽게 소개를 하고 있다는 점이 인상적이었습니다.

이 글에서는 이 책의 저자인 에릭 노먼드가 다시 만들어낸 용어들에 대한 개념들을 소개하면서 제가 함수형 프로그래밍에 대해 하고 싶었던 이야기들을 다시 정리해보자 합니다. 앞으로 드릴 이야기들은 책에서 다시 정의한 용어는 빌려오고 있으나 책에 있는 내용과 동일하지 않으며 주관적인 해석을 바탕으로 정리한 내용이라는 점 알려드립니다.

함수형 프로그래밍 용어 다시쓰기

함수형 프로그래밍에서는 순수함수, 불변성, 선언적 패턴 이라는 3가지 요소가 굉장히 중요합니다.책의 저자 에릭 노먼드는 학문적인 입장에서 만든 정의는 실용적인 개발 입장에서 혼란을 가중한다고 말하고 있습니다. 그래서 같은 개념을 다음과 같은 방식으로 이야기합니다.

  1. 순수함수 → 코드를 액션과 계산, 데이터로 분리하자. 특히 액션에서 계산을 분리하는 코드를 작성하자.
  2. 불변성카피온라이트방어적복사를 이용하여 불변성을 유지하자.
  3. 선언적 패턴계층형 설계추상화 벽을 이용하여 무엇과 어떻게를 구분하여 좋은 설계를 유지하자.

다시 풀어낸 문장 역시 바로 이해되지 않겠지만 함수형 프로그래밍에 관심이 있던 사람이었다면 이러한 용어들이 실무적으로 이러한 뜻이었구나 하고 알게 되는 계기가 되었으면 좋겠습니다.


1부. 액션, 계산, 데이터

일반적인(?) 함수형 프로그래밍 첫 경험기

함수형 프로그래밍은 대개 순수함수(Pure-Function)와 부수효과(Side-effect)의 정의로 시작을 하며 부수효과를 멀리하고 순수함수의 합성을 통해서 프로그래밍을 하자라고 합니다.

'음.. 좋은 말이군...' 이라고 생각하며 조금 더 부수효과에 대해 알아보다보면 부수효과의 예시로, DOM의 변화, 로그, 파일에 읽고 쓰기, 서버와의 통신, 메일 보내기 등을 설명을 합니다.

그러면 당연히 이어지는 생각은 '아니? DOM 처리나 파일 읽고 쓰기, 서버와의 통신을 안하고 어떻게 프로그래밍을 하라는 거지?' 라는 생각이 들면서 점점 더 복잡해지는 함수형 프로그래밍의 이론과 함께 실전성이라고는 하나도 보이지 않는 예시코드들을 보면서 아... 함수형 프로그래밍은 나중에 공부해도 괜찮겠다는 생각과 함께 이 정도선에서 보통 마무리를 짓게 됩니다.

저만 이런거 아니죠? +_+

순수함수 vs 부수효과 가 아니라 액션, 계산, 데이터로 분리하자

함수형 프로그래밍에서는 부수효과와 가변된 상태를 멀리하고 순수함수로 프로그래밍을 하자고 하지만 대부분의 프로그램의 목적은 부수효과에 있습니다. 서버에서 데이터를 조회하고 화면을 변경하고 로그를 남기고 파일을 읽고 쓰는 행위들을 하지 않는 프로그램은 의미가 없겠죠. 순수함수만 가지고는 우리가 만들고자 하는 응용프로그램이 되지 않습니다.

그렇기에 잘못된 오해를 불러 일으키는 용어 대신 함수형 프로그래밍은 프로그램은 크게 액션, 계산, 데이터 이 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>
}

함수형 프로그래밍이라더니 어딘가 많이 익숙한 코드의 모양이네요... 사실 함수형 프로그래밍은 현대 UI 프로그래밍에 잘 맞으며 알게 모르게 우리가 쓰고 있는 중입니다.

함수형 프로그래밍은 멀리 있지 않았습니다. 우리가 현재 쓰고 있는 이러한 패턴은 이미 함수형 패러다임이 충분히 반영된 코드였습니다. 우리가 여기서 주목을 해야할 코드는 바로 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)) // 외부에서 필요한 모든 입력을 넣어줍니다.
}

이렇게 만들어진 계산은 이제 독립적입니다. 언제든 재사용이 가능하며 테스트하기에도 용이합니다. 계산은 조립을 해도 언제나 같은 결과를 만들어내기 때문에 조합에 의한 폭발적인 테스트 시나리오를 만들지 않도록 도와줍니다.

계산은 명시적인 입력과 출력만을 가지며 어떠한 부수효과도 만들어내지 않습니다. 같은 입력에 대해서는 언제나 같은 결과만을 만들어내야 합니다.

그렇습니다. 계산이 바로 순수함수였습니다.

액션 - 계산 - 데이터 정리

함수형 프로그래밍을 순수함수와 부수효과가 아니라 액션 - 계산 - 데이터의 관점으로 이해를 해보면 조금 더 쉽게 받아들일수 있습니다.

프로그램은 곧 데이터의 변화이며 다시 써보자면 프로그램은 액션에 의해 변하는 데이터입니다. 데이터가 변하는 방법은 따로 계산으로 독립적으로 만들어두어 액션과 계산과 데이터를 함수를 통해 연결하여 작성하는 개념이 바로 함수형 프로그래밍 패러다임 인 것입니다.

특히 실행시점과 회수에 의존적인 액션에서 독립적인 계산을 분리해내어 복잡한 코드를 간단하게 만들고 테스트를 용이하도록 만들도록 하는 것이 중요합니다.

  1. 순수함수 → 코드를 액션과 계산, 데이터로 분리하자. 특히 액션에서 계산을 분리하는 코드를 작성하자.

2부. 불변성 - 카피온라이트, 방어적복사

계산은 여러 번을 실행을 해도 외부의 영향에 값을 변경하지 않아야 된다고 했습니다. 하지만 함수에서 숫자나 문자열이 아닌 객체나 배열을 사용한다면 자바스크립트는 기본적으로 pass by reference 방식을 사용하기에 언제든 외부에서 값이 수정되거나 함수 내부에서 외부에 영향을 미칠 수 있다는 사실을 알아야합니다. 그리고 그렇지 않기 위해서 객체나 배열을 pass by value의 형태로 변경하는 방식을 알아야 합니다.

카피 온 라이트: Copy on Write

앞서 만든 카운터 프로그램에서 요구사항을 살짝 변경해보겠습니다. 버튼을 클릭할때마다 숫자를 더하는게 아니라 배열을 만들어두고 배열의 값을 하나씩 하나씩 늘려가는 형태로 만들어간다고 상상해봅시다.

그러면 기존의 코드는 어떻게 바뀌어야 할까요?

// 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 기능이 되었기에 이 API를 사용하면 됩니다.

https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

IE에서는 아직 지원하지 않는 API이므로 필요할 경우에는 polyfill을 사용할 수도 있습니다. https://github.com/zloirock/core-js#structuredclone

이렇듯 외부 세계의 API중 mutaion한 함수를 해야하는 경우에는 방어적 복사 혹은 깊은 복사를 통해서 액션을 계산으로 만들 수 있습니다.

  1. 불변성카피온라이트방어적복사를 이용하여 불변성을 유지하자.

3부. 선언적 패턴과 계층형 구조

함수형 프로그래밍은 함수를 통해서 관심사를 분리할 수 있다.

설계는 엉켜있는 코드를 푸는 것이다!

지금까지의 내용을 통해서 함수형 프로그래밍은 액션 - 계산 - 데이터로 구분하고 불변성을 이용해서 액션에서 최대한 계산을 분리하자고 하였습니다. 이렇게 각 영역을 함수로 구분을 짓다보면 자연스럽게 좋은 구조를 만들어 낼 수가 있습니다.

코드를 작게 분리하면 좋은 점

  1. 재사용하기 쉽다.
  2. 유지보수하기 쉽다
  3. 테스트하기 쉽다.

그리고 우리는 이렇게 분리한 코드를 조합을 하는 과정에서 자연스레 함수간의 계층이 생긴다는 것을 알 수 있게 됩니다.

계층적 구조

최초 예를 들었던 코드를 다시 가져와 보았습니다. 그리고 한번 함수들이 어떠한 계층을 가지는지 그림으로 표현을 해보았습니다.

위와 같이 함수들이 이러한 계층을 이루고 있으며 각 계층별로는 어떠한 유형의 함수들이 추가될지도 한번 예상해보면서 그림을 추가해보았습니다. 어떤가요? 새로운 기능을 구현하려고 한다면 각 코드가 어느 계층에 들어가야할지 상상이 잘 되시나요?

이렇게 액션, 계산, 데이터로 코드를 구분하고 계층을 만들고, 계층을 넘나들지 않는 코드를 짜다보면 자연스럽게 좋은 코드의 구조를 만들 수 있고 계산의 비중을 높여가고 계층을 넘나들지 않도록 코드를 쪼개다보면 좋은 설계과 리팩토링에 대한 좋은 근거가 될 수 있습니다.

액션으로 갈 수록 코드의 형태는 무엇을 하는 것인지 행동을 기반한 기획서에 가까운 코드가 만들어지며 데이터 구조를 몰라도 되는 형태의 코드를 작성하게 됩니다. 반면 계산과 데이터에 가까워 질수록 데이터 중심적인 코드를 작성하게 되고 상대적으로 재사용성이 높고 테스트를 하기 좋은 코드형태를 갖추게 됩니다.

이런식으로 계층을 나누고 각 계층을 침범하지 않도록 코드를 작성하다보면 자연스럽게 추상화 벽이라는 것이 만들어지게 되면서 벽 상단으로의 코드 변화가 하단에 영향을 미치지 않고 하위의 코드 변화가 상위에 영향을 주지 않도록 할 수 있습니다.

우리가 마치 API를 mock으로 작성을 하던 실제로 연결을 하던 dev서버에 연결을 하던 동일한 API콜을 호출하는 것과 유사한 맥락입니다.

이렇게 계층이 견고해지는 구조로 작성을 하게 되면 유연하면서도 변화에 국지적인 형태의 좋은 설계를 가져가게 됩니다. 상위에는 기획서에 가까운 무엇을 해야할지만 기술을 하는 선언적 패턴으로 코드를 가져갈 수 있게 되고 하위에는 태스트가 쉬운 코드 조각들로 구성이 되는 좋은 설계방향의 코드가 만들어지게 됩니다.

  1. 선언적 패턴계층형 설계추상화 벽을 이용하여 무엇과 어떻게를 구분하여 좋은 설계를 유지하자.

주관적으로 써보는 함수형 프로그래밍이 널리 쓰이지 못하는 까닭

지금까지 함수형 프로그래밍이 지향하는 방향과 함수형 사고에 대해서 알아보았습니다. 프로그램을 액션 - 계산 - 데이터의 영역으로 구분하고 각 코드를 부수효과가 있는 함수 - 순수함수 - 데이터로 분리하여 작성을 하면 엉키지 않고 계층별로 잘 정리된 코드를 만들 수 있다고 하였습니다. 특히 액션에서 계산과 데이터를 많이 분리해낼수록 테스트가 쉬워지고 재사용과 조합을 하기 좋은 코드가 된다고 하였습니다.

분명 간결해보이면 좋아보이는 방식처럼 느껴집니다. 하지만 이러한 함수형 프로그래밍이 현재 널리 쓰이지 않는 이유는 무엇일까요? 개인적으로 제가 느끼는 이유를 한번 공유해보고자 합니다.

예시 - 중첩된 객체에 대한 카피 온 라이트

함수형 프로그래밍 세계가 아닌 곳에는 원본을 넘기고 원본의 특정값을 바로 수정을 한다고 하였습니다. 그 편이 메모리 관점에서는 훨씬 더 효율적이기 때문입니다. 자바스크립트는 순수 함수형 프로그래밍 언어는 아니기 때문에 자바스크립트에서 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")          

이런식으로 간단하고 유용한 함수들을 쓰다보면 언젠가 내가 분명히 귀찮게 작성했던 코드들을 훨씬 간결하고 풍성한 문법으로 코드를 작성을 할 수 있게 됩니다. 마치 알고 있는 단어가 많아질수록 말을 더 잘 하는 것처럼 말이죠.

그래서 결국 함수형 라이브러리를 택하게 되는데...

저도 함수형 패러다임을 시작으로 설명을 하기 시작했지만 조금 더 함수형 프로그래밍을 배우다보면 결국 함수를 다루는 테크닉을 배우게 되고 이러한 테크닉과 함수형 패러다임이 만나게 되면 비슷한 형태의 함수형 도구들이 많아지게 됩니다. 그러다보면 대부분 실전에서 필요한 코드들의 형태가 비슷해지고 ex) map, filter, reduce, find, pluck, groupBy, distint 이러한 유틸리티들을 새롭게 구현하기 보다는 이미 만들어진 함수형 도구들을 사용하는 편이 낫다는 결론에 이르게 됩니다.

그렇게 발전한 lodash와 ramda와 같은 함수형 라이브러리들은 처음에는 인기를 끌었지만 점점 이렇게 방대해지는 형태로 가다보니 오히려 더 어려워지는 효과를 가져왔습니다.

Ramda에서 제공하는 수 많은 함수 어휘들. 분명 내 코드를 더 간결하게 만들고 코드 어휘력을 늘려줄테지만 공부할 엄두가 안나는 분량이다.

누군가는 예시로 든 이 코드가 무슨의미인지 기존보다 훨씬 더 쉽게 이해가 되는 한편 다른 누군가는 복잡하게 짠 코드보다 더 어렵게 해석을 해야할수도 있다.

이러한 이유로 이러한 유틸리성 함수형 프로그래밍 라이브러리등은 더 발전을 하거나 다채로운 라이브러리들이 만들어지지지 않고 호불호가 갈리는 방식이 되었습니다.

목적을 위해서 함수형 개념만 접목시키는 라이브러리들

앞서 소개했듯이 결국 순수함수형을 깊게 파고 발전을 하는 것은 더욱 더 함수형 프로그래밍을 어렵게 만드는 방향이 되었습니다.

그리하여 나온 것들은 함수형 프로그래밍 개념을 이용해서 만들어진 상태관리를 위한 Redux라던가 반응형 프로그래밍에 함수형 프로그래밍 개념을 엮어서 만든 rxjs, 불변성 관리를 위해서 만들어진 immutable.js, 날짜만 다루는 함수형 라이브러리 date-fns 등 특정 목적성을 가지고 여러가지 라이브러리들이 함수형 패러다임을 적절히 결합하는 방식으로 발전을 하고 있습니다.

그렇기에 함수형 프로그래밍은 함수형 라이브러리를 배우는 것이 아니다.

종착지는 같은데 아무튼 아니다.

자바스크립트는 함수형 패러다임은 지향하지만 언어레벨에서는 순수 함수형 기능등을 문법적으로 제공하고 있지는 않습니다. 그러다보니 함수형 프로그래밍을 하기 위해서는 특정한 라이브러리나 혹은 그에 준하는 유틸리티 함수를 직접 만들어서 사용을 해야하는 문제가 있습니다.

그러다보면 내가 함수형 라이브러리를 다 만들바에 함수형 라이브러리를 써야겠다하게 되면서 기존의 함수형 프로그래밍을 배우다 보면 순수함수, 부수효과 -> 1급객체로써 함수를 다루는 방법 -> 체이닝과 컬렉션 -> 특정 함수형 라이브러리를 다루는 방법과 같은 형태로 학습을 하다가 함수형 프로그래밍이 실무와는 점점 멀어지게 되는 것 같아요.

그래서 이번 글에서의 목표와 분량 역시 함수형 프로그래밍에 대한 용어 설명이나 테크닉이 아닌 함수형 패러다임과 함수형 사고에 관련된 이야기에 많이 집중을 하였습니다.

실전에서 적용하기 좋은 함수형 사고

  1. 프로그램을 액션 - 계산 - 데이터로 구분하여 생각하자.
  2. 계산은 가급적 명시적 입력과 명시적 출력으로 만들어서 테스트 가능하게 만들자.
  3. 코드의 계층적 구조를 그려보고 같은 계층의 레벨에 맞는 위치에 코드를 작성하자.
  4. 계층을 뛰어넘는 코드를 작성하지 않도록 하여 좋은 구조를 계속 유지하자.

함수형 라이브러리를 이용하여 온전히 모든 프로그래밍을 함수형 프로그래밍을 하지 않더라도 일부 이러한 패러다임을 조금씩 코드에 적용을 시켜본다면 훨씬 더 좋은 코드를 작성하는데 도움이 될 거라고 생각합니다.

제가 전달하는 함수형 패러다임을 알고 나서 알게되었으면 하는 바람들...

이렇게 나눴더니 코드 리뷰를 할 때 원칙이 생겼어요.
이렇게 나눴더니 가독성이 높아졌어요.
이렇게 했더니 코드를 재사용하기 쉬워졌어요.
이렇게 했더니 코드를 이해하기 쉬워졌어요.
이렇게 했더니 설명하기 쉬워졌어요.

TMI: JS도 함수형 프로그래밍을 지원할 생각은 있다.

StateOfJS 설문조사에서 실시한 내용 중 Javascript에서 부족하다고 느끼는 영역 중에 무려 3가지나(하위권 입니다만) 함수형 프로그래밍과 관련된 내용입니다. 불변성에 대한 지원과 Pipe Operator가 정식 문법이 된다면 훨씬 더 함수형 프로그래밍을 하기 좋아질거라고 생각합니다.

TC39 Pipe operator
https://github.com/tc39/proposal-pipeline-operator

TC39 Observable
https://github.com/tc39/proposal-observable

Observable이 표준이 되면 제가 좋아하는 rxjs도 JS의 표준이 될거라는 기대를 하며 기다리고 있습니다.

사실 논의가 시작된지 7년도 더 지났지만 기약없는 기다림을 하는 중입니다. 😂
하지만 정말 그날이 온다면? 함수형 프로그래밍을 해야겠죠? 😆

끝으로

자바스크립트는 함수형 프로그래밍을 할 수 있도록 만들어졌지만 함수형 프로그래밍을 위해서 만들어진 언어는 아닙니다. 그래서 온전한 함수형 프로그래밍을 하기 위해서는 반드시 별도의 코딩이 필요하며 이는 곧 관련 라이브러리가 필요하다는 의미입니다.

결국 그러한 라이브러리를 만들기 위해서는 이러한 함수형 프로그래밍등의 기초지식들을 바탕으로 라이브러리를 사용하거나 제작하는 방법을 배우는 것이 종착지가 됩니다.

대부분의 함수형 프로그래밍의 책들이 특정한 함수형 프로그래밍 라이브러리를 사용하거나 만드는 방법의 책들로 되어 있는 것은 이러한 이유입니다. 저 역시 rxjs를 실무에서 잘 사용하는 것이 함수형 프로그래밍의 종착지라고 생각을 했으니까요.

그렇지만 함수형 프로그래밍을 배운다는 것은 lodash나 Ramda, 혹으 rxjs를 익히기 위한 수단이라거나 함수형 라이브러리를 만들기 위한 것은 아닙니다. 함수형 사고개념이 없는 상태에서 함수형 라이브러리만 쓴다면 결코 좋은 코드가 만들어지지 않을 것입니다.

함수형 프로그래밍의 개념을 익히는 것은 좋은 코드를 볼 수 있는 새로운 관점을 얻게 되는 과정입니다. 그리고 그 관점으로부터 이렇게 하면 좀 더 나은 프로그래밍을 작성할 수 있다는 것을 알게 되는 것이 가장 중요한 성취일 것입니다.

단순히 책 1권을 읽고서 바로 실무에 함수형 프로그래밍을 쓴다는 것은 쉽지 않습니다. 함수형 프로그래밍을 한다는 것은 기존 레거시에서 함수형 프로그래밍 기반으로 교체를 한다는 의미가 되기 때문입니다.

그렇지만 프로그램을 액션과 데이터와 계산으로 구분을 하고 암묵적인 입력과 출력들을 명시적인 입력으로 바꾸고 카피 온 라이트를 통해서 액션에서 계산을 빼내는 하는 방향으로 조금씩 리팩토링을 한다면 장기적으로 더 좋은 코드로 만들 수 있는 명확한 방법과 기준이 됩니다.

이러한 기준은 좋은 코드가 무엇인지 그리고 어떻게 코드 리뷰를 하고 어떻게 리팩토링을 하면 좋을지 고민을 하는 개발자들에게 좋은 지침이 되어 줄 것입니다.

서두에도 밝혔듯이 그렇다고 전부 100% 함수형으로 바꾸는 것이 더 나은 결과는 또 아니었기에 함수형 프로그래밍에 집착해서 더 나은 방식을 뒤로 하고 굳이 불편한 길을 걸어서도 안됩니다. 언젠나 이 밸런스가 중요합니다.

이 글이 객체지향과는 다른 맛의 좋은 코드를 만들 수 있는 관점이 되는 함수형 프로그래밍에 대한 어떤 흥미와 그리고 도입 그리고 어떻게 익혀야 될지에 대한 어떤 방향성, 특히 당장 함수형 프로그래밍을 하지 않더라도 내 코드를 더 좋은 코드로 만들 수 있기 위한 하나의 관점을 알게 되는 터닝 포인트가 되는 시간이 되었길 바랍니다.

함수형 프로그래밍에 대한 이론지식은 사실 운전면허 필기시험과도 같습니다. 아무리 많이 알아도 실제 운전실력은 이론이 아니라 경험에서 나오죠. 그래서 실제로 많이 코드를 작성 해보면서 이러한 개념들이 체화되기를 바랍니다. 하다보면 이 글들이 무슨 말인지 깨닫는 순간이 올거에요!

긴 글 읽어주셔서 감사합니다.
함수형 프로그래밍에 대해서 궁금한 내용이 있다면 댓글을 물어봐주세요 :)

profile
Svelte, rxjs, vite, AdorableCSS를 좋아하는 시니어 프론트엔드 개발자입니다. 궁금한 점이 있다면 아래 홈페이지 버튼을 클릭해서 언제든지 오픈채팅에 글 남겨주시면 즐겁게 답변드리고 있습니다.

10개의 댓글

comment-user-thumbnail
6일 전

홍대병 힙스터 기질.. 이거 참 굉장히 공감이 되네요 ㅋㅋㅋ

1개의 답글
comment-user-thumbnail
6일 전

계산을 통해서 순수함수를 더욱 쉽게 설명해주셔서 좋았던거 같습니다.
불변성에 대한 이야기, 암묵적 입출력에 대한 이야기도 그 동안 작성 했던 함수를 생각해보는 계기가 되었습니다.

좋은 글 감사합니다 ㅎㅎ

1개의 답글

엇.. 카톡방에서 궁금해서 여쭈어보았던 것이 벌써 글로 완성이 되었군요! 까매오로 출연한거 같아서 재밌네요! 함수형 개념을 알고 코딩하는것과 모르고 코딩하는것에는 코드에 큰 영향을 미칠 수도 있겠다는 생각이 들었습니다. 좋은 글 감사합니다.

1개의 답글
comment-user-thumbnail
5일 전

오 간만에 올라온 테오님의 신상이네요...!

저자의 정보를 찾아보니 함수형에 관심이 많고 활동도 활발하게 하시네요!
https://github.com/ericnormand/grokking-simplicity-code
예제 코드도 있고 덕분에 저도 찾아봤습니다 ㅎㅎ

(번외이지만... 번역하면서 왜 책 제목을 이상하게 바꾸는건지 ㅠㅠ
Grokking Simplicity 자체로 나왔으면 참 좋았을텐데 싶네요 ㅎㅎ)

테오님이 추천하신 책이니까 시간내서 꼭 한번 읽어봐야겠군요!
감사합니다

1개의 답글
comment-user-thumbnail
3일 전

요즘 함수형 프로그래밍 학습에 대해 고민이 있었는데 좋은 글 감사합니다!

1개의 답글