[프론트엔드] 일단 한번 해보세요! 함수형 프로그래밍.

teo.yu·2021년 11월 5일
54
post-thumbnail

주의! 함수형 프로그래밍은 아주 방대하며, 해당 글에서 소개하는 내용은 아주 아주 극히 일부분일 뿐입니다.
javascript 혹은 typescript로 개발을 배우고 있는 분들에서 도움이 될 만한 아주 가벼운 이야기 입니다.

프론트엔드 개발자가 왜 함수형 프로그래밍을 배우고 써야 하는지 이유에 대해서 말해볼까 합니다.

그리고 글 말미에는 더 clean하고 safe한 코드를 작성하기 위해 당장 시작할 수 있는 간단한 팁들을 준비했습니다.

왜 우리는 함수형 프로그래밍을 배워야 할까요?

- 우리는 javascript를 써야 하는 운명인 것이에요!

프로그래밍 방법에는 여러가지 패러다임이 존재합니다. 절차식 프로그래밍, 객체지향 프로그래밍, 함수형 프로그래밍 이런것들은 한번씩은 접해봤을법한 용어들입니다. (더 이상의 자세한 설명은 생략한다.)

함수형 프로그래밍이 객체지향 프로그래밍보다 더 낫기 때문에 함수형 프로그래밍을 배워야 할까요? 아닙니다.
웹 개발을 하기 위해서 우리는 (현재로썬) 언어의 선택권이 없이 javascript를 해야만 하기 때문입니다.

- javascript 탄생비화

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

결국 javascript에는 언어의 태생부터 함수형 프로그래밍의 개념들이 녹아있고 동시에 객체지향의 가치는 다소 희석이 되어 있는 형태의 언어였습니다.

당시 javascript는 사람들에게 이도저도 아닌 언어라며 놀림을 받았지만 지금은 가장 인기 있는 언어에 당당히 1위를 한 적도 있는 (요새는 AI때문에 python이 다시 치고 올라오고 있어서...) 언어가 되었습니다.

- 그래서 javascript는 멀티 패러다임 랭귀지다!

javascript의 설명을 보면 multi-paradigm language 라고 되었습니다. 즉, 함수형과 객체지향이 혼종된 형태란 말이죠.

결국 우리는 javascript를 쓸 수 밖에 없기에 객체지향이냐 함수형이냐 패러다임을 선택해서 깊게 파야 하는 것이 아니라

javascript 그 자체를 잘 하기 위해서 javascript의 함수형과 객체지향을 둘 다 알아야 합니다.

Scope 관점에서 이해해보는 프로그래밍 패러다임

개념 이해를 돕기 위해서 조금더 빌드업을 해보도록 하겠습니다.

- 프로그래밍은 곧 값의 변화이다.

프로그램이라는 게 뭘까요? 여러 정의가 있겠지만 단순하게 컴퓨터 입장에서 본다면 0과 1로 이루어진 메모리 공간의 배치가 외부 요인으로 인해 변경되는 일련의 과정을 기술한 것을 프로그래밍이라고 볼 수 있습니다.

결국 프로그래밍의 가장 큰 핵심은 값을 바꾸는 것이죠.

이 관점에서 프로그램의 오류라는 것은 원하지 않는 값으로 변경된 것이라고 볼 수 있습니다.

우리는 개발하는 기간보다 수정하는 기간이 더 길기 때문에 가급적 오류가 발생했을때 빨리 찾을 수 있기를 원합니다.

그리고 scope라 함은 어떤 변수에 접근을 할 수 있는 범위를 말합니다.

- 왜 전역 변수가 나쁠까?

개발을 처음 배울때 듣는 원칙 중 하나인 "전역 변수를 가급적 쓰지 말라." 말은 들어보셨을 겁니다.
javascript가 전역변수 기반으로 되어 있다는 문제점도 들어봤을테구요.

근데 왜 전역 변수는 나쁜걸까요?

앞서 언급했듯이 오류는 곧 원하지 않는 값의 대입이며 이를 찾아야지 오류를 수정할 수 있습니다. 전역 변수는 프로그램의 어디서든 접근이 가능하고 수정이 가능한 변수입니다. 곧 문제가 발생했을 경우에 확인해야 할 코드 범위가 코드 전체라는 얘기가 됩니다. 그만큼 오류를 찾기가 어려워지겠죠. 또한 프로그래밍의 곳곳에서 수정될 수 있는 만큼 흐름을 따라가기 위해서는 많은 곳들을 찾아다니게 만듭니다.

- 객체 지향 프로그래밍의 탄생

그래서 이러한 변수들의 접근할 수 있는 범위의 제한이 필요했습니다. 하지만 공통으로 같이 써야 하는 변수를 완전히 없앨 수는 없습니다. (전역변수는 나쁜거지 아예 쓸 수 없어야 하는 것은 아니니까요.)

그래서 덩치가 큰 프로그램을 작은 프로그램 덩어리로 나누고 각자 공통적으로 쓰는 변수들은 자기들끼리만 사용하도록 고립된 형태로 만들어서 관리를 하면 어떨까? 하는 생각을 합니다.

이러한 생각의 바탕위에 여러가지 개념들이 추가해져 만들어진 것이 객체 지향 프로그래밍 패러다임입니다.

이제 scope가 프로그램 전체가 아니라 class 단위로 좁아졌습니다.

- 함수형 프로그래밍은요?

함수형 프로그래밍은 scope의 범위를 함수 범위까지 좁히며, 공통으로 같이 써야 하는 변수를 아예 사용하지 않도록 하는 방식입니다.

함수형 프로그래밍에서 오류의 발생은 값을 변경하는데서 온다고 믿기에 극단적으로 값을 변화시키지 않는 프로그램을 추구합니다.

프로그램의 정의가 곧 값을 변화시키는 것인데 값을 변화를 안 시킨다면 프로그램이라 할 수 없겠죠. 정확히는 값을 변화를 해야 하는 곳과 그렇지 않은 곳을 분리하여 전역변수, 공통변수의 문제를 해결하고자 합니다.

주의! 위 내용들은 개념 이해를 돕기 위해 작성되었으며 명백히 맞는 말은 아닙니다. 함수형 프로그래밍과 객체지향 프로그래밍은 각자 태생이 다르며, 또한 절차지향 > 객체지향 > 함수형 이런 식으로 선형적으로 발전해오지 않았습니다.
또한 각자의 프로그래밍 패러다임은 javascript에만 국한되는 것이 아니기에 정확한 설명이 아니므로 정확한 팩트를 원하시는 분들은 알아서 더 공부해보시길 바랍니다. :)

javascript에서 함수형 프로그래밍이 주목받기 시작한 이유

Lodash, Ramda 등 정통 함수형 라이브러리가 존재했지만 대중에게 함수형 프로그래밍이 유명해진 이유는 따로 있었다.

- Redux의 순수함수

지금은 아니지만 Redux가 대세론이 자리잡던 시기가 있었습니다. 그리고 빠짐없이 나왔던 키워드가 바로 "순수 함수"입니다. (후에 순수함수에 대해 자세히 기술하겠습니다.)

불변성과 순수함수는 함수형 프로그래밍에서 아주 중요한 키워드이며 많은 사람들이 함수형 프로그래밍은 잘 몰라도 순수 함수나 불변성에 대해서는 다들 알게된 계기입니다.

- React의 함수형 컴포넌트

React가 16.8.0 함수형 컴포넌트와 hook을 출시하면서 class 컴포넌트의 시대가 막을 내리고 함수형 컴포넌트 시대가 시작합니다. 이때부터 함수형이라는 키워드와 함께 HoC와 같은 함수형 프로그래밍의 용어들이 주요 키워드가 되면서 자연스레 프론트엔드에서 함수형 프로그래밍의 키워드 주목도가 올라가게 됩니다.

새삼 프론트엔드에서 React의 영향력을 느낍니다.

그래서 함수형 프로그래밍이 뭔가요?

이미 충분이 글이 길어졌기에 학문적인 얘기는 다른 좋은 글들을 참고해보세요.
여기서는 간략하게 개념만 짚고 넘어가겠습니다.

- 목적과 방향성

깔끔하고 예측가능하며 테스트 하기 쉽고 안전한 코드를 만들기 위해서,
부수 효과(Side-effect)가 있는 함수와 그렇지 않은 순수 함수(Pure Function)를 구분하여 문제를 최대한 작게 만들고 이를 조립하여 해결하는 방식

부수효과(Side-Effect)란, 함수 내부에서 동작하는 행동들이 함수 외부로 영향을 끼치는 것을 의미합니다.
대표적으로 1) 외부 값의 변화 2) API를 이용한 I/O 등이 있습니다.

- 순수함수와 불변성

부수효과와 그렇지 않은 것을 구분해야 합니다. 순수함수란 부수효과가 없는 함수를 의미합니다.
순수함수가 되려면 다음 조건을 만족해야 합니다.

  1. 동일한 인자를 넣을 경우 항상 같은 값을 반환해야 한다.
  2. 함수가 호출되고 나서 아무런 변화가 없어야 한다.

그래서 조금더 자세히 적어보자면,

1-1. 함수의 인자가 아닌 외부 변수를 사용하지 않아야 합니다. (상수는 OK)
1-2. 함수 내부에서 Math.random()이나 file I/O등 호출때마다 달라지는 값이 없어야 합니다.
2-1. 외부 변수의 값을 수정하지 않아야 합니다.
2-2. 인자로 넘어온 Object나 Array, Date와 같은 값들의 필드를 내부에서 변경하지 않아야 합니다.
2-2. 콘솔, 네트워크, 기타 DOM API등을 사용하지 않아야 합니다.
2-3. try ~ catch등 같은 에러 처리 로직을 사용하지 않아야 한다.

이런 것들을 다 안하면 어떻게 프로그램이 될까 싶지만 다시 읽어보면 목적은 이 둘의 구분입니다.

순수함수는 항상 같은 값을 반환하기 때문에 테스트하기가 쉽고, 순수함수 + 순수함수 = 순수함수 가 되므로 순수함수로 조립된 최종값만 부수효과가 있는 함수를 통해 처리하고 다시 그 결과를 순수함수를 통해 조립을 하는 방식으로 개발을 하면 우리 목적을 달성할 수 있게 됩니다.

함수형 프로그래밍의 개념도
Input => (순수 함수 => 순수 함수 => 순수 함수 => 순수 함수) => 부수 효과 => Output1
Output1 => Input => (순수 함수 => 순수 함수) => 부수 효과 => Output2

그래서 이렇게 순수함수를 통해 데이터가 지나가는 경우에는 외부에 영향을 주지 않기 때문에 무한한 조립의 가지수를 다 테스트하지 않고 각 함수만 잘 동작한다면 저 구간은 문제 없을 거라는 것을 확신할 수 있게 됩니다. 이렇게 값이 변하지 않는 상태를 유지하는 것을 불변성이라고 합니다.

순수함수에 대한 기술적인 정의는 더 좋은 자료가 많습니다. 더 알고 싶다면 아래 링크를 통해 확인해 보세요.
https://velog.io/search?q=%EC%88%9C%EC%88%98%ED%95%A8%EC%88%98

🤨 흠.. 그래서 뭘 어떻게 하라는 거죠?

원래 글을 쓰기 전 생각은, '함수형 프로그래밍에 대한 기술적인 얘기들이 있는 글들은 많은데 정작 그래서 어떻게 개발하라는 거지 알려주는 게 별로 없네. 그런 글을 써볼까?'

라는 생각으로 적기 시작했지만, 함수형 프로그래밍이 워낙 방대하고 최소한의 개념은 기술을 하다보니 분량 조절을 실패했다는 생각이 드네요.

글의 본래 목적인 "좋은 javascript 코드를 작성하기 위한 함수형 프로그래밍 패러다임의 좋은 코딩 습관"을 적어 보려합니다.

javascript가 태생이 함수형이다보니 아직은 함수형 프로그래밍의 모든 개념을 아직은 다 알지 못하더라도, 모든 코드가 함수형 프로그래밍이 아니어도 좋으니 함수형 프로그래밍이 추구하는 목적과 형태를 따라하다보면 자연스레 좋은 코드의 모양을 하고 있게됩니다.

좋은 javascript 코드를 작성하기 위한 코딩 습관

- let보다 가급적 const를 사용해라.

함수형 프로그래밍은 불변성의 유지로 부터 시작합니다. 코드에서 let의 사용을 줄이고 const로 개발하는 습관을 가져보세요. 수정된 값이 필요하면 새로운 변수를 만들어서 사용해 보세요.

Do Not This:

let foo = 100
...
foo = somthing(foo, "bar")

Do This:

const foo = 100
...
const new_foo = somthing(foo, "bar")

- Array, Date의 Mutaion Method를 가급적 사용하지 마세요.

push, pop, shift, sort, reverse 등 객체를 변하는 Method를 가급적 spread operator로 대체하거나 값을 복사해서 사용하세요.

Do Not This:

const example = (arr:number[], date:Date) => {
  arr.push(4)
  arr.sort()
  date.setMonth(10)
}

Do This:

const example = (arr:number[], date:Date) => {
  const new_arr = [...arr, 4]
  const sorted_new_arr = [...new_arr].sort()
  const new_date = new Date(date).setMonth(10)
  return [new_arr, new_date]
}

- 가급적 Object의 필드에 대입 연산자를 쓰지 마세요.

Do Not This:

const example = (obj:Object) => {
  obj.foo = 200
}

Do This:

const example = (obj:Object) => {
  return {...obj, foo: 200}
}

- '가급적' 이라는 의미를 잘 생각하고 값을 변경할 때에는 항상 scope에 대해 생각하며 코딩하세요.

본말을 전도해서 극단적으로 let을 쓰지 않고 Array push를 쓰지 않고 Object의 값을 변경할때 항상 복사를 하지는 마세요.

값을 변경할때 변수가 선언된 위치에서 부터 3~7줄 범위내에서 수정되며 선언된 함수의 scope를 벗어나지 않는 값이라면 복사해서 값을 옮기는 행위는 리소스 낭비입니다.

- pure function과 effect를 확실하게 구분해서 작성하세요.

일부만 Pure하면 Pure한게 아니다.

const와 spread operator를 통해서 불변성을 지키는듯 보이는 코드들 안에 부수효과를 유발하는 코드가 있다면 pure하지 않습니다. 분리할 수 있는 방법을 고민하세요. pure function은 크지 않아도 좋습니다.

- 그래도 요구사항대로 구현해서 코드가 작동하는게 제일이다!

위에서 저렇게 말은 했지만 javascript는 멀티 패러다임 랭귀지이기에 함수형을 쓰던 객체지향으로 개발하던 잘 돌아가기만 한다면 일단 OK입니다. 언젠가는 차츰차츰 각 영역의 좋은점만 찾아서 영리하게 잘 이용하시리라 믿어요.

마치며

과거 객체지향을 공부하면서 특히 디자인 패턴을 배우고 나서는 모든 구현 과제에 어떤 패턴으로 만들어야 맞는 것인지 모든 것이 패턴으로 풀어보려는 디자인 패턴병에 걸렸던 적이 생각이 납니다.

그리고 함수형 프로그래밍도 배우기 시작하고 다 함수형으로 만들어보려는 함수형 병도 잠깐 앓고 지나 갔었습니다.

여기서 이제 한발자국만 더 나가면 함수와 함수를 어떻게 결합할 수 있는지를 시작으로 객체지향의 디자인 패턴과 같은 것들이 나오기 시작하고 그런 것들을 익히다 보면 가끔 본질을 까먹을 때가 있습니다.

우리가 함수형 프로그래밍을 해야 하는 이유는 깔끔하고 예측가능하며 테스트 하기 쉽고 안전한 코드를 만들기 위해서이며
javascript가 태생이 완전한 객체지향도 완전한 함수형도 아닌 밸런스를 가지고 있는 언어이기 때문입니다.

그래서 기법 수준의 내용이 들어가기 전 내용들을 모아 일단 정리해보았습니다.
언제가 될진 모르겠지만 다음 글에서는 함수형 프로그래밍의 본격적인 기법들에 대해서 한번 정리를 해볼까 합니다.

끝으로 드리고 싶은 말은, 프론트엔드에서 패러다임의 논쟁은 객체지향이 좋다 vs 함수형이 좋다 vs 그냥 취향 차이 아닌가? 가 아니라

javascript의 특성에 맞는 객체지향의 좋은 점, 함수형 프로그래밍의 좋은 점 골라서 둘 다 내 것으로 해야 한다.
라고 생각합니다.

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

6개의 댓글

comment-user-thumbnail
2021년 12월 2일

좋아요!

1개의 답글
comment-user-thumbnail
2022년 2월 25일

우연히 링크를 타고 들어와서 읽게 되었는데 좋은 글 감사합니다!

1개의 답글
comment-user-thumbnail
2022년 6월 5일

학문적, 기술적으로 엄밀히 접근, 이해하지 않더라도 함수형 프로그래밍이 추구하는 가치에 공감하고 코드에 반영하려고 하면 좋은 코드들이 나오는 것 같습니다.

무겁지 않는 방식으로 설명해주시고 바로 적용해볼 수 있는 팁을 제공해주셔서 많은 분들이 좋은 코드를 짜는데 많은 도움을 받을 것 같습니다.

감사합니다!

1개의 답글