-
자바스크립트와 함수형 프로그래밍.프로그래밍/Web 2020. 3. 7. 22:36
React Native에서 사용하는 React는 함수형 프로그래밍의 철학을 받아들여 만들어졌습니다.
React를 처음 접하는 이들을 위한 문서에서도 함수형 프로그래밍의 특징중 하나라는 불변성을 강조하고 있을 정도 입니다.
따라서 React Native로 프로그래밍을 하기 위해서는 함수형 프로그래밍에 대해 알아두는 것이 좋습니다.
함수형 프로그래밍이란 패러다임은 람다대수에 기반하여 만들어졌고, 람다대수와 가장 가까운 프로그래밍 언어는 Lisp입니다.
따라서 함수형 프로그래밍 개념은 Lisp 계열로 이해하는 것이 가장 효율적이라 생각합니다.
Lisp는 전위 표현식으로 이루어진 언어로,
(함수 인자1 인자2)
와 같은 형태를 띄고 있습니다.
아래의 링크는 Lisp의 방언중 하나인 Racket이란 언어로 소개한 함수형 프로그래밍 입니다.
매우 긴 내용이었지만 정리해봅시다.
- 함수형 프로그래밍은 결과값 말고 다른 상태를 변경시키지 않는 순수 함수를 사용한다.
- 다른 상태를 변경시키는 것을 부작용(Side Effect)라 부르며 랜덤, I/O작업등도 포함된다.
- 부작용이 있으면 상태에 따라 연산의 결과가 달라져, 최적화(예: 메모이제이션)와 버그추척이 힘들다.
- 숨겨진 상태변화를 없애려면 명시적으로 매개변수와 출력값을 지정하면 되며, I/O 작업의 경우 모나드를 도입해 순수함수를 보장한다.
- 람다 대수는 튜링완전하며 표현식(Expression), 함수(Fuction), 적용(Application)으로 이루어진다.
- 식별자의 이름(Name)은 자리 표시자(Place Holder)의 역할만 하며 치환(α-reduction)이 가능하다. 이 때문에 람다함수는 익명함수라 부르기도 한다.
- 축약(β-reduction)되었을 때 축약대상은 종속변수, 남는 것은 자유변수로 생각할 수 있다.
- 함수형 프로그래밍(람다대수)의 함수는 일급 객체다.
- 커링(Curring)을 사용하면 함수의 실행을 늦추거나(lazy evaluation), 재사용성을 늘릴 수 있다.
- 열린 함수(자유변수가 존재)을 닫힌 함수로 만들어주는 것이 클로저며, 내부함수가 외부함수의 context에 접근할 수 있다.
- 하나 이상의 함수를 인자로 받거나 결과로 반환하는 것이 고차함수며
map
,reduce
,filter
,find
등이 존재하며 매우 편리하다.
결론적으로 보자면,
- 객체지향 프로그래밍: '상태'를 나누어 관리
- 함수형 프로그래밍: '상태'를 변경하지 않고, 드러내며 관리
자바스크립트의 화살표 함수는 람다 함수에 거부감을 줄이기 위해 만든 이름이다.
다음에서 람다나 클로저를 사용시의 강력한 예시를 보여주고자 한다.
람다의 장점
짧은 함수
const a = [ "Hydrogen", "Helium", "Lithium", "Beryllium" ]; const a2 = a.map(function(s) { return s.length; }); //일반 const a3 = a.map( s => s.length ); //람다
일반적인 함수보다 람다를 사용한 코드가 간결하다는 것을 알 수 있다.
까다로운
this
자바스크립트의 객체를 다룰때 사용하는 문법인 this는 Lexical Scope(정의하는 곳에 따라 결정)이 아니라 Dynamic Scope(불려지는 곳에 따라 결정)되며
"use strict"
(엄격모드) 사용여부에 따라 달라지기도 한다.따라서 사용하는 입장에서는 까다롭게 느껴진다.
예를 들어 다음 코드는 동작하지 않는다.
첫번째 this는 Person, 두번째 this는 전역이 대상이기 때문이다.
function Person1() { this.age = 0; setInterval(function growUp() { this.age++; }, 1000); } const p1 = new Person1();
이 코드를 동작 가능하게 고쳐보자.
변수에 할당
self
란 변수에this
를 대입하여 사용할 수 있다.function Person2() { var self = this; self.age = 0; setInterval(function growUp() { self.age++; }, 1000); } const p2 = new Person2();
bind
사용this
값이growUp
함수에 전달되도록 바인딩을 할 수도 있다.function Person3() { this.age = 0; setInterval(function growUp() { this.age++; }.bind(this), 1000); } const p3 = new Person3();
람다
람다를 사용하면 Lexical Scope로 처리되기 때문에 골머리를 앓을 필요가 없다.
function Person4() { this.age = 0; setInterval(() => { this.age++; }, 1000); } const p4 = new Person4();
다른 언어
다른 언어로는 역시 OOP의 대표적인 언어인 자바를 살펴보자.
일반적인 클래스와 인터페이스
인터페이스의 함수를 오버라이드한 후 사용한다.
public class JButtonTest_General extends JFrame implements ActionListener { ... JButtonTest_General() { ... //Set Button button = new JButton("Button"); button.setActionCommand(button.getText()); button.addActionListener(this); add(button); ... } ... @Override public void actionPerformed(java.awt.event.ActionEvent e) { if (e.getActionCommand().equals(button.getText())) { label.setText(button.getText()); } } }
익명 클래스
클래스 단위로 인터페이스를 상속받는 대신, 함수내부에서 인터페이스를 사용한다.
public class JButtonTest_Anonymous extends JFrame { ... JButtonTest_Anonymous() { ... //Set Button button = new JButton("Input"); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { label.setText(button.getText()); } }); add(button); ... } ... }
람다
인터페이스의 인스턴스화, 함수 오버라이드 표시 없이 바로 사용할 수 있다.
람다는 '익명'함수이기 가능한 일이다.
public class JButtonTest_Lambda extends JFrame { ... JButtonTest_Lambda() { ... //Set Button button = new JButton("Input"); button.addActionListener((e) -> { label.setText(button.getText()); }); add(button); ... } ... }
몇가지 예제를 보며 람다식의 장점은 물론, 기존의 객체지향과 함수형이 반목하는 것이 아니라는 것까지도 알 수 있었다.
클로저의 활용
오직 하나의 메소드를 가지고 있는 객체를 일반적으로 사용하는 모든 곳에 클로저를 사용할 수 있다.
특히 웹은 사용자의 이벤트에 의한 콜백으로 사용하는 경우가 많기 때문에 클로저는 유용하다.
기초 예시.
글씨 크기를 사용자의 클릭에 따라 바꾸고 싶다고 생각해보자.
우선 다음과 같은 HTML의 링크를 준비했다.
<a href="#" id="size-12">12</a> <a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
클로저를 이용하면 간단하게 각 이벤트에 해당하는 콜백함수를 만들어서 적용할 수 있다.
function makeSizer(size) { return () => { document.body.style.fontSize = size + 'px'; }; } const size12 = makeSizer(12); const size14 = makeSizer(14); const size16 = makeSizer(16); document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16;
객체지향 따라하기.
객체지향에서 중요한 개념중 하나는 은닉화/캡슐화를 하는 것이다.
자바스크립트는 태생적으로 은닉화를 지원하지 않지만 클로저를 활용한다면
private
메소드를 만들 수 있다!!const counter = (() => { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment() { changeBy(1); }, decrement() { changeBy(-1); }, value() { return privateCounter; } }; })(); console.log(counter.value()); // logs 0 counter.increment(); counter.increment(); console.log(counter.value()); // logs 2 counter.decrement(); console.log(counter.value()); // logs 1
위 코드에서 privateCounter과 changeBy는 private의 형태로 쓰이고 있다는 것을 알 수 있습니다.
클로저와 성능
새로운 객체/클래스를 생성 할 때, 메소드는 일반적으로 객체 생성자에 정의되기보다는 객체의 프로토타입에 연결되어야 좋다.
생성자가 호출(개체가 생성) 될 때마다 메서드가 다시 할당되어 성능과 메모리를 낭비할 수 있기 때문이다.
따라서 이 코드는
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
다음과 같이 고쳐볼 수 있다.
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } (function() { this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }).call(MyObject.prototype);
참고
- MDN
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Closures - 자바 익명클래스와 람다식
https://black7375.tumblr.com/post/168099931290/자바-익명클래스와-람다식
라이브러리
함수형 프로그래밍을 위한 라이브러리로 Lodash나 Ramda.js, Rambda.js가 유명하며 앞서 소개한 고차함수등을 사용할 수 있다.
단, 네이티브 함수와 비교를 해보고 사용하길 바란다.
Ramda.js, 자바스크립트 함수형 프로그래밍 라이브러리 도입
람다(Ramda)와 로다시(Lodash) 그리고 함수형 프로그래밍
Rambda - Faster and Smaller Alternative to Ramda - Interview with Dejan Toteff (survivejs.com)
TS를 사용한다면 fp-ts를 사용해보자.
https://gcanti.github.io/fp-ts/
모나드??
모나드 이야기가 나온김에 아주 간단한 형태만 이해해보도록 한다.
모나드를 이해할때는 Haskell이 가장 효율적이므로 Haskell로 된 코드를 사용해보자.
자바스크립트로도 설명할 수 있지만 이해하기에 코드가 깔끔하진 않다.
다음은 하스켈의 간단한 입출력 예제다.
main = do putStrLn "Input: " x <- getLine putStrLn ("The Input was " ++ x)
모나드가 적용되어 있지만 그리 어렵진 않다는 것을 알 수 있다.
읽기 전에 꼭 알아두어야 할 것이 있다면 모나드의 설명의
return
과bind
는 자바스크립트의return
,Fuction.prototype.bind
와 다르다.Javascript에서의 모나드.
링크의 글을 읽어보면 알겠지만, Promise와 Async/Await가 대표적인 예이다.
Promise
비동기 작업의 최종 완료 또는 실패를 나타낸다.
프로미스의 상태는 3가지로 나눌 수 있다.
-
대기(Pending): 이행, 거부가 일어나지 않은 초기 상태.
-
이행(Fulfilled): 연산이 성공적으로 완료됨.
-
거부(Rejected): 연산이 실패함.
function imgLoad(url) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); request.open('GET', url); request.responseType = 'blob'; request.onload = function() { if (request.status === 200) { resolve(request.response); } else { reject(Error('Image didn\'t load successfully; error code:' + request.statusText)); } }; request.onerror = function() { reject(Error('There was a network error.')); }; request.send(); }); }
현대적인 함수나 API들은 프로미스를 통해 콜스택이 완료됨을 보장해준다.
그리고 다음과 같이 고전적인 API를 Wrapping 해볼 수도 있겠다.
function successCallback(result) { console.log("Audio file ready at URL: " + result); } function failureCallback(error) { console.log("Error generating audio file: " + error); } // 옛날: 콜백을 전달 createAudioFileAsync(audioSettings, successCallback, failureCallback); // 프로미스: 콜백 첨부 const createAudioFilePromise = (audioSettings) => new Promise( (resolve, reject) => { createAudioFileAsync(audioSettings, resolve, reject); } ); createAudioFilePromise(audioSettings) .then(successCallback, failureCallback);
모나드 링크에서 봤던 것처럼 콜백을 개선해볼 수도 있다.
.then
과.catch
메서드 반환 값은 프로미스이므로 Chaining이 가능하기 때문.그리고 Async/Await는 이를 더 간단하게 만드는 것이 가능하다.
Async/Await
Async/Await는 Promise를 명령형처럼 사용하게 만들어주며 Promise로 반환한다.
다음은 콜백, 프로미스를 Async/Await로 바꾼 예.
function callbackHell() { doSomething(function(result) { doSomethingElse(result, function(newResult) { doThirdThing(newResult, function(finalResult) { console.log('Got the final result: ' + finalResult); }, failureCallback); }, failureCallback); }, failureCallback); } function usePromise() { return doSomething() .then(result => doSomethingElse(result)) .then(newResult => doThirdThing(newResult)) .then(finalResult => { console.log(`Got the final result: ${finalResult}`); }) .catch(failureCallback); } async function useAsync() { try { const result = await doSomething(); const newResult = await doSomethingElse(result); const finalResult = await doThirdThing(newResult); console.log(`Got the final result: ${finalResult}`); } catch (err) { failureCallback(err); } }
기타 프로미스와 비교할 만한 장점은 아래 링크에서 확인 가능하다.
자바스크립트의 Async/Await 가 Promises를 사라지게 만들 수 있는 6가지 이유
Do / Binding
아직도 모나드가 어렵나요?
그렇다면 JS에 바인딩 연산자와 Do Notation, 커링을 지원하는 함수가 있다고 가정하고 일반 함수와 비교해봅시다.
// 일반 함수 getFoo("/api/foo").chain(foo => { return getBar("/api/bar").chain(bar => { const lol = bar.name; return getBaz("/api/baz/" + lol).map(baz => { return foo + bar + baz; }); }); }); // 바인딩 연산자 getFoo("/api/foo") >>= (foo) => getBar("/api/bar") >>= (bar) => { const lol = bar.name; return map(getBaz("/api/baz/" + lol)) >>= (baz) => foo + bar + baz; }; // Do 문법 do { foo << getFoo("/api/foo"); bar << getBar("/api/bar"); const lol = bar.name; baz << getBaz("/api/baz/" + lol); foo + bar + baz; }
방금전에 봤던 Callback -> Promise -> Async/Await의 예와 비슷하게 느껴지지 않나요?
어렵다고 하는데, 엄청나게 어렵진 않은 것 같습니다.
참고.
- MDN
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function - Github
https://github.com/mdn/js-examples/tree/master/promises-test
https://github.com/pfgray/babel-plugin-monadic-do
https://github.com/gajus/babel-plugin-transform-function-composition - Developers Google
https://developers.google.com/web/fundamentals/primers/async-functions?hl=ko - Faster async funtions and promises(V8 Blog)
https://v8.dev/blog/fast-async - Awesomely Descriptive Javascript with Monads
https://www.slideshare.net/wicherrr/awesomely-descriptive-javascript-with-monads
React와 모나드.
리엑트의
hooks
와도 깊은 연관성을 가지므로 읽어볼만 하다.When to use React Suspense vs React Hooks
참고로 Functor, Applicative, Monad에 대해 쉽게 설명하자면
- Functor: 함수를
포장된
값에 적용 - Applicative:
포장된
함수를포장된
값에 적용 - Monad:
포장된
값을 리턴하는 함수를포장된
값에 적용
리엑티브 프로그래밍.
모나드 설명을 보면 알 수 있듯, Rx에서 쓰이는
Observable
,Maybe
등은 모나드의 한 예시이다.특히 Observable의 경우 다음처럼 생각하면 이해하기 쉽다.
Rx가 세상에 나오게된 이유는 다음 글이 잘 소개하고 있다.
MS는 ReactiveX를 왜 만들었을까? (feat. RxJS)
RxJS는 또 다른 웹 프레임워크인 Angular에서 통합되어 쓰이고 있다.
React와 섞어쓰기 위해서라면 다음을,
RxJS 자체를 배우고 싶다면 아래를 참고해보아도 좋다.
댓글
- 함수형 프로그래밍은 결과값 말고 다른 상태를 변경시키지 않는 순수 함수를 사용한다.