프로그래밍/Web

자바스크립트와 함수형 프로그래밍.

BlaCk_Void 2020. 3. 7. 22:36

React Native에서 사용하는 React는 함수형 프로그래밍의 철학을 받아들여 만들어졌습니다.

React를 처음 접하는 이들을 위한 문서에서도 함수형 프로그래밍의 특징중 하나라는 불변성을 강조하고 있을 정도 입니다.

 

자습서: React 시작하기 - React

 

자습서: React 시작하기 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

따라서 React Native로 프로그래밍을 하기 위해서는 함수형 프로그래밍에 대해 알아두는 것이 좋습니다.

함수형 프로그래밍이란 패러다임은 람다대수에 기반하여 만들어졌고, 람다대수와 가장 가까운 프로그래밍 언어는 Lisp입니다.

따라서 함수형 프로그래밍 개념은 Lisp 계열로 이해하는 것이 가장 효율적이라 생각합니다.

 

Lisp는 전위 표현식으로 이루어진 언어로,

(함수 인자1 인자2)

와 같은 형태를 띄고 있습니다.

 

아래의 링크는 Lisp의 방언중 하나인 Racket이란 언어로 소개한 함수형 프로그래밍 입니다.

내 맘대로 프로그램 설계 7. - 함수형 프로그래밍.

 

내 맘대로 프로그램 설계 7. - 함수형 프로그래밍.

내 맘대로 하는 프로그램 설계 시리즈. Chapter1 - 간단한 데이터 처리(4섹션) 2017/12/27 - [프로그래밍/설계] - 내 맘대로 프로그램 설계 1. - 이유와 준비. 2018/01/11 - [프로그래밍/설계] - 내 맘대로 프로그..

black7375.tistory.com

 

매우 긴 내용이었지만 정리해봅시다.

  1. 함수형 프로그래밍은 결과값 말고 다른 상태를 변경시키지 않는 순수 함수를 사용한다.
    • 다른 상태를 변경시키는 것을 부작용(Side Effect)라 부르며 랜덤, I/O작업등도 포함된다.
    • 부작용이 있으면 상태에 따라 연산의 결과가 달라져, 최적화(예: 메모이제이션)와 버그추척이 힘들다.
    • 숨겨진 상태변화를 없애려면 명시적으로 매개변수와 출력값을 지정하면 되며, I/O 작업의 경우 모나드를 도입해 순수함수를 보장한다.
  2. 람다 대수는 튜링완전하며 표현식(Expression), 함수(Fuction), 적용(Application)으로 이루어진다.
    • 식별자의 이름(Name)은 자리 표시자(Place Holder)의 역할만 하며 치환(α-reduction)이 가능하다. 이 때문에 람다함수는 익명함수라 부르기도 한다.
    • 축약(β-reduction)되었을 때 축약대상은 종속변수, 남는 것은 자유변수로 생각할 수 있다.
  3. 함수형 프로그래밍(람다대수)의 함수는 일급 객체다.
    • 커링(Curring)을 사용하면 함수의 실행을 늦추거나(lazy evaluation), 재사용성을 늘릴 수 있다.
    • 열린 함수(자유변수가 존재)을 닫힌 함수로 만들어주는 것이 클로저며, 내부함수가 외부함수의 context에 접근할 수 있다.
    • 하나 이상의 함수를 인자로 받거나 결과로 반환하는 것이 고차함수며 map, reduce, filter, find 등이 존재하며 매우 편리하다.

 

결론적으로 보자면,

  • 객체지향 프로그래밍: '상태'를 나누어 관리
  • 함수형 프로그래밍: '상태'를 변경하지 않고, 드러내며 관리

자바스크립트의 화살표 함수는 람다 함수에 거부감을 줄이기 위해 만든 이름이다.

 

다음에서 람다나 클로저를 사용시의 강력한 예시를 보여주고자 한다.

 

람다의 장점

짧은 함수

const a = [
  "Hydrogen",
  "Helium",
  "Lithium",
  "Beryl­lium"
];

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);

 

참고

 

라이브러리

함수형 프로그래밍을 위한 라이브러리로 LodashRamda.js, Rambda.js가 유명하며 앞서 소개한 고차함수등을 사용할 수 있다.

단, 네이티브 함수와 비교를 해보고 사용하길 바란다.

Lodash의 대체재로서의 순수 자바스크립트 함수

 

Lodash의 대체재로서의 순수 자바스크립트 함수

이 포스트는 가장 널리 사용되고있는 라이브러리인 Lodash / Underscore.js 의 유틸리티 함수들을 순수 자바스크립트를 통해 어느 정도로 대체해 줄 수 있는지 이해를 돕기 위해 정리된 내용이다.

ui.toast.com

Ramda.js, 자바스크립트 함수형 프로그래밍 라이브러리 도입

 

Ramda.js, 자바스크립트 함수형 프로그래밍 라이브러리 도입

Ramda.js 적용기

medium.com

람다(Ramda)와 로다시(Lodash) 그리고 함수형 프로그래밍

 

람다(Ramda)와 로다시(Lodash) 그리고 함수형 프로그래밍

What is good code?

engineering.huiseoul.com

Rambda - Faster and Smaller Alternative to Ramda - Interview with Dejan Toteff (survivejs.com)

 

Rambda - Faster and Smaller Alternative to Ramda - Interview with Dejan Toteff

Even though you can get far with JavaScript's native functionality, eventually you'll find yourself…

survivejs.com

 

TS를 사용한다면 fp-ts를 사용해보자.

https://gcanti.github.io/fp-ts/

 

Introduction - fp-ts

Typed functional programming in TypeScript fp-ts provides developers with popular patterns and reliable abstractions from typed functional languages in TypeScript. Disclaimer. Teaching functional programming is out of scope of this project, so the document

gcanti.github.io

 

모나드??

모나드 이야기가 나온김에 아주 간단한 형태만 이해해보도록 한다.

모나드를 이해할때는 Haskell이 가장 효율적이므로 Haskell로 된 코드를 사용해보자.

자바스크립트로도 설명할 수 있지만 이해하기에 코드가 깔끔하진 않다.

 

다음은 하스켈의 간단한 입출력 예제다.

main = do
    putStrLn  "Input: "
    x <- getLine
    putStrLn ("The Input was " ++ x)

 

모나드가 적용되어 있지만 그리 어렵진 않다는 것을 알 수 있다.

 

간단한 모나드 설명과 예제

 

간단한 모나드 설명과 예제

리엑트 네이티브 스터디 때문에 시작한 글이었는데 생각보다 길어져서 분리하게 되었다. 자바스크립트 관련 코드는 해당 스터디쪽 문서에 올릴 예정. 역시 모나드를 이해할때는 Haskell이 가장 효율적이므로 Haske..

black7375.tistory.com

읽기 전에 꼭 알아두어야 할 것이 있다면 모나드의 설명의 returnbind는 자바스크립트의 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가지 이유

 

자바스크립트의 Async/Await 가 Promises를 사라지게 만들 수 있는 6가지 이유

이글은 6 Reasons Why JavaScript’s Async/Await Blows Promises Away (Tutorial)에 대한 번역입니다.

medium.com

 

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의 예와 비슷하게 느껴지지 않나요?

어렵다고 하는데, 엄청나게 어렵진 않은 것 같습니다.

 

 

참고.

 

 

React와 모나드.

리엑트의 hooks와도 깊은 연관성을 가지므로 읽어볼만 하다.

When to use React Suspense vs React Hooks

 

When to use React Suspense vs React Hooks

React Suspense is to a Monad as Hooks are to Applicative Notation Monads and Applicative Functors are extensively used in functional programming. There is a relationship between them and React Suspense for Data Fetching and React Hooks APIs. This is a quic

www.freecodecamp.org

 

참고로 Functor, Applicative, Monad에 대해 쉽게 설명하자면

  • Functor: 함수를 포장된 값에 적용
  • Applicative: 포장된 함수를 포장된 값에 적용
  • Monad: 포장된 값을 리턴하는 함수를 포장된 값에 적용

 

리엑티브 프로그래밍.

모나드 설명을 보면 알 수 있듯, Rx에서 쓰이는 Observable, Maybe등은 모나드의 한 예시이다.

 

특히 Observable의 경우 다음처럼 생각하면 이해하기 쉽다.

 

Rx가 세상에 나오게된 이유는 다음 글이 잘 소개하고 있다.

MS는 ReactiveX를 왜 만들었을까? (feat. RxJS)

 

MS는 ReactiveX를 왜 만들었을까? (feat. RxJS)

김코딩 님이 잘하고 싶어서 만든 블로그

huns.me

 

RxJS는 또 다른 웹 프레임워크인 Angular에서 통합되어 쓰이고 있다.

 

Angular RxJS | PoiemaWeb

리액티브(Reactive, 반응형) 프로그래밍은 비동기 데이터 스트림(Asynchronous data stream)에 기반을 둔 프로그래밍 패러다임이다. 데이터 스트림이란 연속된 데이터의 흐름을 말하며 리액티브 프로그래밍은 기본적으로 모든 것을 연속성을 갖는 데이터의 흐름인 스트림으로 본다.

poiemaweb.com

 

Angular 환경에서 RxJS 100% 활용하기

이번 블로그에서는 RxJS가 무엇이며, Angular 환경에서 RxJS를 최대한 활용할 수 있는 pattern을 공유하고자 합니다.

medium.com

 

React와 섞어쓰기 위해서라면 다음을,

 

React Hooks + RxJS or How React Is Meant to Be

In this article, we will explore how the RxJS and React combo allows for better readability and less boilerplate. Additionally, we will examine how they allow the the same functionality as the popular state management frameworks even though they aren't fra

blog.soshace.com

 

Reactive Redux state with RxJS

If you're like me, you're using Redux every day, to manage your state, probably with React, or any other view library or framework. You definitely know how hard is when it comes to handling async code and side effects. I know that very well, been there, tr

ivanjov.com

 

RxJS 자체를 배우고 싶다면 아래를 참고해보아도 좋다.

 

ReactiveX

CROSS-PLATFORM Available for idiomatic Java, Scala, C#, C++, Clojure, JavaScript, Python, Groovy, JRuby, and others

reactivex.io

 

Introduction

 

www.learnrxjs.io

 

RxJS 간단정리

RxJS의 개념을 간단하게 정리한다.

medium.com