(Clean Frontend - 1) 함수형 프로그래밍

김정욱·2022년 11월 21일
0

JavaScript

목록 보기
10/10
post-thumbnail

함수형 프로그래밍 [week1]

"유지보수하기 좋은 코드로 개발하기 with Javascaript" 과정

함수형 프로그래밍 이해

[ imperative / declarative ]

  • imperative programming (OOP)
    • 목표기반이 아닌 알고리즘을 노출하고, 전역상태를 인정하며 이를 변경
    • 핵심은 객체이다. 속성과 메서드가 하나의 객체로 결합된 상태로 구현
  • declarative programming (FP)
    • 알고리즘을 숨기려고하고, 선언적으로 표현하면서 목표중심적으로 프로그램을 표현. 전역상태를 사용하지 않음
    • 목표중심의 실현체는 '함수'이다. 함수를 다양하게 표현해야 하는데 집중

[ FP 관련 키워드 ]

  • referential transparency (참조투명성, 순수성)
    • 동일합 입력에는 동일한 값을 반환
    • 외부 참조되는 것이 적어야 함
  • higher-order function (고차함수)
    • 함수를 '값' 으로 취급하며 함수의 인자로 전달할 수 있음
    • 함수가 함수를 반환할 수도 있음
    • 함수 합성 가능
  • stateless (무상태)
    • state를 유지하지 않는다. 필요하면 상태를 새롭게 만든다
    • immutability
    • 객체지향에서 this를 사용하지 않는다
  • immutability (불변성)
    • 불변은 데이터를 직접 변경하지 않음을 뜻함
    • 예) array.push(X) / array.concat(O)
 const myData = [ 
    {name:"crong", phoneNumber:"010-111-3333"},
    {name:"jk", phoneNumber:"010-001-3433"}
  ];

//data추가.
const newData = [...myData, {name:"honux", phoneNumber:"070-142-0000"}];
console.log(myData.length, newData.length); //2,3
console.log(myData === newData); //false

[ FRP(Functional Reactive Programming) ]

  • Reactive programming
    • 이벤트와 비동기코드의 확실한 개선을 위해서
    • 알고리즘을 숨기고, 비즈니스 로직에 좀더 집중
    • 이벤트와 비동기 기반 규모가 커지고 있는 Frontend 영역에서 각광
  • FRP는 FP와 RP의 부분집합
    • FP가 FE개발로 들어오면서 이벤트 중심의 개발 환경에 좀더 어울리기 위해서 Reactive Programming방법을 접목하는 것이 어울리다고 판단
    • 실제로 UI개발은 죄다 비동기에 이벤트 중심의 방식으로 동작되며, 이런 상황에서 FP의 도입의 해법으로 FRP의 패턴이 매력적으로 보임. 하지만 막상구현하려면 상상하는 것과 달리 이전 UI개발방식과의 차이로 인해 학습비용필요
  • 함수형 언어를 브라우저 세계에 쓸 때 좀더 유리
    • Collections을 Functional programming 방법으로 조작
    • 다양한 브라우저 비동기 Event도 Collections을 처리하는것과 동일하게 할 수 없을까?
    • Rx.observable 을 사용
var stopStream = Rx.Observable.fromEvent(document.querySelector('button'), 'click');
input.takeUntil(stopStream)
  .map(event => event.target.value)
  .subscribe(value => console.log(value)); // "hello" (click)

[ OOP vs FP ]

  • OOP
    • class를 통해 객체를 만들고 상속/캡슐화 등 OOP 개념 도입 충분
    • 컴포넌트단위 개발에서도 하나의 컴포넌트는 클래스 또는 함수
    • 상태를 변경하면서 데이터를 조작
  • FP
    • this 사용을 별로 하지 않음
    • 함수를 잘게 나누고 조합(composition)
  • OOP + FP
    • 컴포넌트 단위를 클래스의 형태로 만들고, 상태관리 방법을 FP로 처리
    • 프레임워크가 아닌 상태에서의 조합은, 멀티패러다임의 단점을 많이 볼 수 있다
    • 두 개의 장점을 이해하면서 함께 활용하는 방법을 찾아야 함

JS 함수형 프로그래밍 요소

[ first-class function ]

  • 1급 함수
  • 함수는 그저 하나의 '값' 이기 때문에 다른 값과 같이 사용할 수 있다
  • 대표적으로, 함수는 인자로 쓰일 수 있고, 반환될 수 있다
function foo(fn) {
    const val = 10;
    return fn(val);
}

foo( (value) => {
    return value * 10;
}) 
//100

[ Closure ]

  • 클로저는 함수 자신이 포함하는 스코프의 변수들을 추적하는 함수
  • 일반적으로 변수에 접근하려면, 함수의 실행이 종료되기 전이어야 한다(콜스택에서 실행중인 경우)
  • JS에서 특이한 부분은, 함수의 실행이 끝나고 나서 접근이 가능한 경우가 있다 => 클로저
  • 합성(composition)은 클로저(closure) 없이 불가능하다
function sum(a, b){
  return a+b;
}

function divide(a, b){
  return a/b;
}

 /* Currying */
function calculate(fn, prev) {
  return (param) => fn(prev, param);
}


const sum100 = calculate(sum, 100);
const divide100 = calculate(divide, 100);

console.log(sum100(20));
console.log(divide100(20));

[ Currying ]

  • 개념
    • 수학과 컴퓨터 과학에서 커링(currying)이란 다중 인수 (혹은 여러 인수의 튜플)을 갖는 함수를 단일 인수를 갖는 함수들의 함수열로 바꾸는 것을 말한다
    • 하나 이상의 인수의 기능을 하나의 인수의 기능으로 축소하는 과정
      => 다중 callable 프로세스 형태로 변환하는 기술 f(a, b, c) ==> f(a)(b)(c)
  • 특징
    • currying 을 통해서 더 가벼운 함수를 만들 수 있다
    • call() / apply() / bind() 다양하게 활용 가능
    • 인자를 1개씩 고정하는 partial application의 특수한 형태
      • partital application 함수는 다음 번 호출시 결과를 바로 반환, currying을 매개변수를 모두 받을 때 까지 새로운 함수 반환
      • refs : (zerocho blog) / (rhostem blog)
  • tip
    • 어떤 함수를 호출하는데 대부분의 매개변수가 비슷한 상황에서 유용하게 쓸 수 있다
    • 반복적으로 사용되는 매개변수를 내부적으로 저장하여, 매번 인자를 전달하지 않아도 된다
    • currying 시 매개변수의 순서는 매우 중요한데, 변동성이 적은 인자부터 앞에 위치하여 설계 필요
    • lodash 라이브러리의 curry를 이용해 일반적 or partial 한 호출이 가능한 함수 반환을 통해 손쉽게 currying을 사용 가능
/* 람다를 이용한 curry 함수 */
const curry = f => a => b => f(a, b)

/* 조금 더 복잡한 curry 함수 */
//code : https://www.gideonpyzer.com/blog/javascript-currying-and-partial-application/

let curry = (fn) => { 
    return function curryFn(...args1) {
        if (args1.length >= fn.length) {
            return fn(...args1);
        } else {
            return (...args2) => curryFn(...args1, ...args2);
        }
    }
}

function multiply(a, b, c){
    return a * b * c;
}

let curriedMultiply = curry(multiply);
let result = curriedMultiply(2,3)(4);
  • 커링 예제 1)
    • 기본적인 커링 함수의 형태
    • currying 함수의 단점은 코드가 지저분해보일 수 있다는 것이다
      => 파라미터 개수가 많아질수록 depth가 깊어지기 때문
/* 커링 함수 1) 기본 */ 
var greetDeeplyCurried = function(greeting) {
  return function(separator) {
    return function(emphasis) {
      return function(name) {
        console.log(greeting + separator + name + emphasis);
      };
    };
  };
};
/* 커링함수 2) lambda 형식 */
var greetDeeplyCurried = (greeting) => (separator) => (emphasis) => (name) => console.log(greeting + separator + name + emphasis);

/* greeting, separator, emphasis 변수를 바인딩해둔 함수
   => name 파라미터를 동적으로 활용 */
var greetAwkwardly = greetDeeplyCurried("Hello")("...")("?");
greetAwkwardly("Heidi"); //"Hello...Heidi?"
greetAwkwardly("Eddie"); //"Hello...Eddie?"


/* greeting, separator 변수를 바인딩해둔 함수
   => emphasis, name 파라미터를 동적으로 활용 */
var sayHello = greetDeeplyCurried("Hello")(", ");
sayHello(".")("Heidi"); //"Hello, Heidi."
sayHello(".")("Eddie"); //"Hello, Eddie."


/* emphasis 파라미터를 추가로 바인딩 한 함수
   => 커링함수에서 파라미터 순서가 중요한 이유가 바로 이것
   => 변동성이 적은 순서대로 파라미터를 위치해야 확장성이 좋다 */
var askHello = sayHello("?");
askHello("Heidi"); //"Hello, Heidi?"
askHello("Eddie"); //"Hello, Eddie?"
  • 커링 예제 2)
    • 다중 인수를 partial하고 유연하게 사용할 수 있도록 도와주는 curry 헬퍼함수 만들어보기
      => lodash에 이미 이러한 역할을 대신 해주는 함수 존재
function curry(func) {
  return function curried(...args) {
    // function.length 는 함수의 파라미터 개수를 의미
    if(args.length >= func.length) {
      return func.apply(this, args);
    }else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  }
}

var greetDeeplyCurried = curry((greeting, separator, emphasis, name) => console.log(greeting + separator + name + emphasis))


/* 파라미터의 개수가 충분하지 않으면 새로운 function을 반환하며,
   파라미터가 충분하면 function을 실행합니다 */
var greetAwkwardly = greetDeeplyCurried("Hello")("...")("?");
greetAwkwardly("Heidi"); //"Hello...Heidi?"

/* partial 하게 함수 실행 가능 */
greetDeeplyCurried("Hello")("...")("?")("Heidi");
greetDeeplyCurried("Hello", "...")("?")("Heidi");
greetDeeplyCurried("Hello", "...", "?")("Heidi");
greetDeeplyCurried("Hello", "...", "?", "Heidi");
  • 커링 예제 3)
    • lodashcurry 를 통한 손쉬운 currying 함수 생성
import _ from 'lodash'

/* lodash의 curry를 통해 생성한 커링함수  */
var greetDeeplyCurried = _.curry((greeting, separator, emphasis, name) => console.log(greeting + separator + name + emphasis))


/* 파라미터의 개수가 충분하지 않으면 새로운 function을 반환하며,
   파라미터가 충분하면 function을 실행합니다 */
var greetAwkwardly = greetDeeplyCurried("Hello")("...")("?");
greetAwkwardly("Heidi"); //"Hello...Heidi?"

/* partial 하게 함수 실행 가능 */
greetDeeplyCurried("Hello")("...")("?")("Heidi");
greetDeeplyCurried("Hello", "...")("?")("Heidi");
greetDeeplyCurried("Hello", "...", "?")("Heidi");
greetDeeplyCurried("Hello", "...", "?", "Heidi");

함수 합성(composition)

[ 개요 ]

  • 함수형 프로그래밍에서 함수를 (잘, 충분히) 활용하는 프로그래밍 방법
  • 함수를 다양한 문제해결 도구로 사용하려면, 결국 함수를 다양한 방식으로 조합(조립) 할 수 있어야 한다

[ function composition ]

  • 함수의 연속적인 연결. 함수를 조합해서 사용
  • composition 은 그저 입력과 반환값이 있는 함수라면 모두 연결할 수 있다
  • 나의 출력이 다른 함수의 입력으로 composition 된다

[ 필요성 & 용이한 구조 ]

  • 필요성 : 함수를 받아서 동작하는 함수는 상당한 이점 존재
    • 작은 함수들의 연결을 자연스럽게 돕는다
    • 복잡한 로직을 숨기고, 함수이름만으로 무엇을 하는지 비즈니스 로직을 빨리 알아챌 수 있다
  • Compose가 용이한 함수구조
    • 재사용가능한 크기(최대한 작은 단위)로 나눠져야 한다
    • 함수의 동작은 예측가능해야한다(참조투명성 보장)
    • 즉, 동일한 입력에 동일한 결과를 반환 => side effect를 제거

[ compose vs pipe ]

  • composepipe 는 하는 일은 동일하고, composition의 순서의 차이만 존재
  • 일반적으로 compose 가 수학적인 연산순서에 근거하여 더 많이 사용됨
  • pipe가 좌에서 우로 진행되어 사고의 순서와 동일하며, chaining에 익숙한 개발자에게 친숙
  • composepipe 를 통해서 더 보기좋게 표현할 수 있다
c(b(a(str)))
=>
  compose(c, b, a)(str)
  pipe(a, b, c)(str)

[ compose / pipe 구현해보기 ]

const trim = (str) => str.trim();
const toUpperCase = (str) => str.toUpperCase();


  /* 시행착오 1) */ 
// const pipe = (fn1, fn2, fn3) => (str) => fn3(fn2(fn1(str)))

  /* 시행착오 2) */
// const pipe = (fn1, fn2, fn3) => (str) => {
// 	const result1 = fn1(str);
//   const result2 = fn2(result1);
//   const result3 = fn3(result2);
//   return result3;
// }

  /* 완성된 pipe 와 compose */
const pipe = (...param) => (str) => param.reduce((result, fn) => fn(result), str);
const compose = (...param) => (str) => param.reduceRight((result, fn) => fn(result), str);

pipe(
  trim,
  toUpperCase,
  console.log
)(" pipe! "); // PIPE!

compose(
  console.log,
  toUpperCase,
  trim
)(" compose!  "); // COMPOSE!

[ composition 전략 ]

  • 1) 원하는 동작을 순서대로 설계
  • 2) 필요한 작은 함수 단위를 결정
  • 3) 입력과 출력을 결정하고, 함수간의 연결을 짓는다

추가로 알게된 것들

[ template 엘리먼트 ]

  • 추가되거나 복사될 수 있는 요소를 정의할 때 사용되는 element
  • http://www.tcpschool.com/html-tags/template
  • => template 엘리먼트를 사용하여 child 추가시 불필요한 div를 제거할 수 있다
  const parent = document.querySelector('#parent-element');
  const tpl = document.createElement('template');
  tpl.innerHTML = "<div>child</div>"; // htmlString
  // template.content 로 접근
  parent.innerHTML = tpl.content.innerHTML;
  parent.appendChild(tpl.content);

[ pipe / compose 다수 파라미터 대응 ]

  • pipe / compose 사용시 다수 파라미터 대응 방법
    • 1) currying으로 파라미터 개수를 1개로 줄이기
    • 2) currying 함수의 paramobject로 받기
    • 3) 파라미터 개수에 따라 분기처리해서 실행
/* 1) currying으로 파라미터 개수를 1개로 줄이기 */
const pipe = (...funcList: any) => (args: unknown) =>
             funcList.reduce((prev, func) => func(prev), args);
pipe(foo, bar, (arr)=> baz("hello", arr))([1,2,3])
=> const helloBindBaz = baz.bind(undefined, "hello");
=> pipe(foo, bar, helloBindBaz)([1,2,3])


/* 2) object로 currying 함수의 param 통일 */
const f1 = ({param1, param2}) => {...}
const f2 = (param1) => {...}
const f3 = ({param1, param2}) => {...} 
pipe(f1, f2, f3)({p1, p2});


/* 파라미터 개수에 따라 분기처리 해서 실행 */
export const pipe = (...fns) => (...args) => fns.reduce((result, fn) => 
             fn.length === 1 ? fn(result) : fn.apply(undefined, result), args);
const f4 = (param1, param2) => {...}
const f5 = (param1) => {...}
const f6 = (param1, param2, param3) => {...}
pipe(f4, f5, f6)(p1, p2);

[ pipe / compose 비동기 작업 대응 ]

  • pipeline 유틸로 사용되는 fn중 비동기 작업인 경우 Promise를 반환할 수 있다.
  • 비동기 작업을 처리해 주도록 pipe custom 필요
  • 추가 고려 사항
    • 비동기 로직의 error handling을 어떻게 할지 고민해볼 필요가 있다
const pipe = (...fns) => (value) => fns.reduce((acc, fn) => {
   if(toString.call(acc) === "[object Promise]") return acc.then(fn);
   else return fn(acc)
}, value);

[ Proxy / Reflect ]

  • https://ko.javascript.info/proxy#ref-363
  • 모던JS Proxy와 Reflect
  • Proxy를 통해서 특정 객체를 감싸고 정의된 작업(get, set, ...)을 가로채서 추가적인 작업을 하고 원래 객체로 그대로 전달할 수 있다.
  • 정의된 작업을 '트랩' 이라고 부른다.
    • get / set / delete / construct / apply / ownPropertyKeys ...
  • ReflectProxy트랩에 있는 모든 내부 메서드와 동일한 내장 메서드를 가진다
    => 원래 객체에 side effect 없이 전달 가능
  • Reflect를 통해 상속 관계에 있어도 안전하게 원래 객체의 값 접근을 할 수 있다
/* jotai를 모방하며 만든 store */
let atomMap = new WeakMap(); // 객체 자체가 키가되며, 참조가 없어지면 GC에 의해 제거되는 map

export const atom = (init) => {
    const atom = {init};
    const atomProxy = new Proxy({
        value: init,
        listeners: new Set()
    }, {
        // atom의 value가 변경되면 여기에 지정한 set 로직 수행
        set: (target, prop, newValue, ...args) => {
            if (prop !== ATOM_PROP.VALUE || target[prop] === newValue) return true;
            // Reflect.set 을 통해 안전하게 원래 객체로 전달
            const result = Reflect.set(target, prop, newValue, ...args);
            target.listeners.forEach(fn => fn());
            return result;
        }
    });
    atomMap.set(atom, atomProxy);
    return atom;
}
profile
Developer & PhotoGrapher

0개의 댓글