자바스크립트로 함수형 프로그래밍 배우기 :: JSUnconf 컨퍼런스

2020. 9. 3. 21:11React

필자는 자바 프로그래밍을 꽤 오랜 기간동안 해왔습니다. 때문에 객체 지향 프로그래밍 외의 코딩 스타일은 사실 거의 모른다 해도 과언이 아닙니다. 그런데 이번에 리액트를 공부하면서 거의 모든 곳에 함수형 프로그래밍이 쓰이고 있었고 때문에 이해는 커녕 따라치기에 급급한 수준임을 인정할 수 밖에 없었습니다. 물론 자주 보이는 함수들은 대충 뭐가 어떻게 굴러가는 구나 정도로 이해하고 넘어갔지만 리덕스와 미들웨어를 배우고 있는 요즘 자바스크립트, 특히 함수형 프로그래밍에 대한 기초지식이 상당히 부족함을 느끼고 있기에 자바스크립트의 함수형 프로그래밍에 대해 정리하고 넘어가려고 합니다.

 

함수형 프로그래밍은 말 그대로 '모든 걸 함수로 실행'하는 것입니다. 프로그램의 전부를 함수로 표현하는 거죠. 그럼 함수란 뭘까요? 함수는 '인풋을 받아 아웃풋을 내는 것'입니다. 이를 활용하려면 객체지향에서처럼 객체가 어떻게 상호작용하고 조작하는지 단계별(steps in recipe)로 생각하면 안됩니다. 함수형으로 표현하는 법을 고민해야 합니다. (인풋을 받고 아웃풋을 내는 것을요!)

 

예제를 하나 들어보겠습니다.

 

Non functional :

var name = "Hamin Song"
var greeting = "Hi, I'm";
console.log(greeting + name);
=> "Hi, I'm Hamin Song"

이것은 비함수형으로 화면에 "Hi, I'm Hamin Song" 을 띄우는 방법입니다.

변수를 지정해 'name'이라고 하고 'Hamin Song'을 저장하죠, 그리고 'greeting'이라는 변수를 만들고 거기에 'Hi, I'm'이라는 값을 저장합니다. 이런것이 명령형 스타일 코드입니다. 먼저 이거, 다음엔 이거! 순서가 있습니다. 인풋과 아웃풋의 개념이 전혀 없는거죠.

 

이제는 똑같은 내용을 함수형으로 풀어보겠습니다.

 

Functional:

function greet(name){
  return "Hi, I'm" + name;
}

greet("Hamin Song");
=> "Hi, I'm Hamin Song"

매개변수 name을 취하고 있는 greet 라는 함수를 하나 정의합니다. 이 함수는 "Hi, I'm"이라는 String을 반환하죠. 그래서 인풋으로 Hamin Song을 주게되면, Hi, I'm Hamin Song 이라는 문장을 아웃풋, 즉 출력합니다. 함수형으로는 이렇게 표현이 되는거죠

 

use Pure function

함수형 프로그래밍의 또 다른 특징은 "순수 함수 Pure function"만 쓴다는 것입니다. 순수 함수란 함수 인자(인풋)에 의지 하지 않는 다른 것을 가지고 함수의 결과에 영향을 주지 않는 함수를 의미합니다. 쉽게 말해 인풋만을 가지고 아웃풋을 계산하는 함수라고 할 수 있죠 

 

var name = "Hamin";
function greet() {
  console.log("Hi, I'm" + name);
}

예를 들어 위의 greet 함수는 전역 변수 name이 있고, 이 함수에 사용되고 있습니다. 이것은 순수 함수가 아닙니다. name이 이 함수의 인풋이 아니고 전역 상태에서 뭔가를 읽어오고 있는 상태이기 때문입니다. 또 greet 함수가 순수하지 않은 이유는, 함수의 반환 값이 없어서 입니다. 그저 console.log 라는 새로운 함수를 사용하고 있을 뿐, 반환하는 값이 전혀 없습니다.

 

greet 함수를 순수 함수로 사용하려면 다음처럼 써야합니다.

function greet(name) {
  return "Hi, I'm" + name;
}

현재 greet 함수의 아웃풋에 영향을 주는 것은 인풋(name) 뿐 입니다. 우리가 주는 매개변수는 아웃풋을 반환하게만 합니다. 

 

이것은 함수형 프로그래밍의 특징 중 하나가 최대한 순수하게 생각해야 하는 것이기 때문에 순수 함수는 굉장히 중요한 컨셉이라고 할 수 있습니다. 

 

use Higher-order functions

함수형 프로그래밍의 다른 특징으로는 "고차 함수 Higher-order functions" 가 있습니다. 이것은 다른 함수를 인풋으로 받거나 함수를 아웃풋으로 반환하는 함수입니다. 함수를 일종의 객체로 취급하는 것이죠. 함수를 다른 함수에 전달할 수 있어요. 즉, 함수 내에 함수가 있는 구조를 만들어 낼 수 있습니다. 

 

예시를 하나 들어보죠.

function makeAdjectifier(adjective) {
  return function (string) {
    return adjective + " " + string;
  };
}

var coolifier = makeAdjectifier("cool");
coolifier("conference");

=> "cool conference"

makeAdjectifier이라는 함수가 있다고 합시다. 인풋으로 adjective(형용사)를 주면, 함수를 반환합니다. String이나 번호가 아니라 주어진 String에 형용사를 붙히는 함수를 반환합니다. 예를 들어 makeAdjectifier 함수에 cool 이라는 인풋을 주면 coolifier 이라는 함수가 나옵니다. 이제 String을 coolifier 에 전달하면 이 String에 cool 이 붙어서 나오게 되죠. 그래서 conference를 전달하면 cool conference 가 나오게 되는 것입니다. 

 

 

Don't iterate

함수형 프로그래밍에서 피해야 할 버릇 중 하나는 "반복"입니다. 'for' 이나 'while' 같은 것들을 말이죠. 리스트에 있는 모든 항목을 이용하는 것은 다른 언어 패러다임에서 굉장히 익숙한 작업입니다. 하지만 함수형에서는 그러지 않고 map, reduce, filter 같은 것들을 씁니다. 인풋으로 원하는 방식의 리스트 뿐 아니라 함수도 받는 그런 구조입니다.

 

Map, Reduce, Filter은 다른 포스팅에서 자세하게 설명할 예정이지만 간략하게 그림으로 설명해보도록 하겠습니다.

 

 

여기 채소 목록이 있다고 합시다. 이 목록을 새로운 형태(썰고 자르는)로 바꾸고 싶다면, 함수형 프로그래밍이 아닌 경우 목록의 각 항목 항목에 '다지기' 라는 행동을 적용하겠지만 함수형 프로그래밍에서는 'Map'을 써서 재료 목록과 함수 '다지기'를 인풋으로 건네줍니다. 그러면 모든것이 다져진 새 목록이 나오겠죠?

 

여기서 'Reduce' 함수로 목록의 모든 항목을 일정 방식으로 결합합니다. 지금의 경우에는 맛있는 샌드위치가 되도록 순서에 맞춰 겹치는 것이 되겠죠. 

 

'Filter'은 만약에 오이가 싫다면 오이가 아닌 것들을 통과시키는 것입니다. 이렇게 고차함수를 써서 for 이나 while 반복을 피할 수 있습니다. 함수형 프로그래밍에서는 Map, Reduce, Filter 같은 고차함수에 함수를 주고 원하는 결과를 얻어냅니다. 

 

Avoid mutability

함수형 프로그래밍에서 또 피해야 할 것은 데이터의 변형입니다. 여기서 변형이란 객체 위치 변경을 뜻합니다. 데이터 불변성을 추구해야 한다는 건데, 이것은 위치를 변경할 수 없는 데이터를 뜻합니다. 일단 두면, 영구히 고정되는 것이죠.

 

예시를 보겠습니다.

 

Mutation (bad)

var rooms = ["H1","H2","H3"]; 
rooms[2] = "H4";
rooms;

=> ["H1","H2","H4"]; 

rooms 변수가 H1, H2, H3으로 이루어진 배열을 가지고 있습니다. 여기에서 H3이 맘에 들지 않아 이것을 H4로 변경하고자 합니다. 변경하고 났더니 rooms가 실제로 바뀌어진 것을 볼 수 있죠. 이것은 문제의 소지가 다분하기 때문에 함수형 프로그래밍에서는 피하는 방식입니다. 왜냐하면 이런식으로 객체 지향적으로 접근할 때 생기는 문제의 이유는 의도치 않게 뭔가를 바꿀 수 있기 때문입니다. (헷갈릴 수 있어요!)

 

만약에 제가 rooms는 H1, H2, H3이라고 생각하고 다른 코드 어딘가에서 rooms 배열이 바뀐 걸 몰랐다면 문제가 생길지 모릅니다. 그래서 코드에 버그가 생겨도 찾기가 정말 어려울 수 있습니다.

 

그래서 더 나은 방법은 모든 데이터를 불변 데이터로 생각하는 것입니다.

 

No Mutation (good)

var rooms = ["H1","H2","H3"];
var newRooms = rooms.map(function(rm) {
  if (rm === "H3") {
    return "H4";
  } else {
    return rm;
  }
}

newRooms; => ["H1","H2","H4"];
rooms; => ["H1","H2","H3"];

예를 들어 rooms의 H3을 대체하는 대신에 map 함수를 써서 새 rooms 함수를 만들고, 리스트의 각 요소(rm이라 명명)을 보고 H3가 있으면 그것을 H4로 반환하고 그것이 아니면 그대로 반환합니다.

 

예시를 완벽하게 이해할 필요는 없지만 중요한 것은 newRooms에는 H1,H2,H4가 있지만 rooms에는 H1,H2,H3이 그대로 완벽하게 유지되고 있습니다! rooms을 불변 데이터로 취급해서 변경하지 않은 것이죠. 이것은 함수형 프로그래밍에서 굉장히 중요한 부분입니다. 버그가 생기는 것을 원칙적으로 막을 수 있기 때문입니다.

 

여기서 생기는 문제는 배열 같은 것을 불변한다고 했을 때, 계속 해서 사본이 생긴다는 것입니다. 만약에 리스트의 요소 중 하나를 새로 바꾸고 싶다고 할지라도 원본 데이터에서 계속해서 복사를 해야합니다. 이것은 시간이 굉장히 많이 걸리고 메모리 공간도 많이 차지 할 수 밖에 없죠. 그래서 함수 프로그래밍에서 이러한 부분을 해결하고자 '영속 데이터 구조' 라는 것을 사용합니다.

 

Phil Bagwell이 처음 제안하고 Clojure을 개발한 Rich Hickey가 구현한 이것은 데이터 뭉치를 배열이 아니라 '트리 형태'로 만들어 데이터 요소들을 노드로 분할합니다. 그리고 여기에서 변경점이 있으면 기존의 변경할 노드에서 연결을 끊고 새로운 노드에 연결하는 식으로 해결하는 방식입니다.

 

 

이 포스팅은 2016년에 열린 JSUnconf 의 Learning Functional Programming with Javascript 강연을 참고했습니다 강의는 여기에 올려져 있습니다.

 

읽어주셔서 갑사합니다!