"유지보수하기 좋은 코드로 개발하기 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로 처리
- 프레임워크가 아닌 상태에서의 조합은, 멀티패러다임의 단점을 많이 볼 수 있다
- 두 개의 장점을 이해하면서 함께 활용하는 방법을 찾아야 함
- 1급 함수
- 함수는 그저 하나의 '값' 이기 때문에 다른 값과 같이 사용할 수 있다
- 대표적으로, 함수는 인자로 쓰일 수 있고, 반환될 수 있다
function foo(fn) { const val = 10; return fn(val); } foo( (value) => { return value * 10; }) //100
- 클로저는 함수 자신이 포함하는 스코프의 변수들을 추적하는 함수
- 일반적으로 변수에 접근하려면, 함수의 실행이 종료되기 전이어야 한다(콜스택에서 실행중인 경우)
- 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
)이란 다중 인수 (혹은 여러 인수의 튜플)을 갖는 함수를 단일 인수를 갖는 함수들의 함수열로 바꾸는 것을 말한다- 하나 이상의 인수의 기능을 하나의 인수의 기능으로 축소하는 과정
=> 다중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)
lodash
의curry
를 통한 손쉬운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
은 그저 입력과 반환값이 있는 함수라면 모두 연결할 수 있다- 나의 출력이 다른 함수의 입력으로
composition
된다
- 필요성 : 함수를 받아서 동작하는 함수는 상당한 이점 존재
- 작은 함수들의 연결을 자연스럽게 돕는다
- 복잡한 로직을 숨기고, 함수이름만으로 무엇을 하는지 비즈니스 로직을 빨리 알아챌 수 있다
Compose
가 용이한 함수구조
- 재사용가능한 크기(
최대한 작은 단위
)로 나눠져야 한다- 함수의 동작은 예측가능해야한다(
참조투명성 보장
)- 즉, 동일한 입력에 동일한 결과를 반환 =>
side effect
를 제거
compose
와pipe
는 하는 일은 동일하고,composition
의 순서의 차이만 존재- 일반적으로
compose
가 수학적인 연산순서에 근거하여 더 많이 사용됨pipe
가 좌에서 우로 진행되어 사고의 순서와 동일하며,chaining
에 익숙한 개발자에게 친숙compose
나pipe
를 통해서 더 보기좋게 표현할 수 있다c(b(a(str))) => compose(c, b, a)(str) pipe(a, b, c)(str)
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!
- 1) 원하는 동작을 순서대로 설계
- 2) 필요한 작은 함수 단위를 결정
- 3) 입력과 출력을 결정하고, 함수간의 연결을 짓는다
- 추가되거나 복사될 수 있는 요소를 정의할 때 사용되는 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 사용시 다수 파라미터 대응 방법
- 1)
currying
으로 파라미터 개수를 1개로 줄이기- 2)
currying
함수의param
을object
로 받기- 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);
- 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);
- https://ko.javascript.info/proxy#ref-363
- 모던JS Proxy와 Reflect
- Proxy를 통해서 특정 객체를 감싸고 정의된 작업(
get
,set
, ...)을 가로채서 추가적인 작업을 하고 원래 객체로 그대로 전달할 수 있다.- 정의된 작업을
'트랩'
이라고 부른다.
get
/set
/delete
/construct
/apply
/ownPropertyKeys
...Reflect
는Proxy
의트랩
에 있는 모든 내부 메서드와 동일한 내장 메서드를 가진다
=> 원래 객체에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; }