함수형 프로그래밍2 - 추상화 레벨 높이기

함수형 프로그래밍2 - 추상화 레벨 높이기

이미지 로딩

이전의 실습에서 이미지가 로딩 될때 굉장히 버벅거렸는데 이러한 부분들을 개선해볼 것이다.

이미지가 로딩이 다 되면 화면에 렌더링되도록 코드를 작성해보자.

_.go( Images.fetch(), Images.tmpl, $.el, $.append($.qs('body')), _.tap( $.findAll('img'), L.map(img => new Promise(resolve => { img.onload = () => resolve(img); // onload img.src = img.getAttribute('lazy-src'); })), _.each($.addClass('fade-in')) ), $.findAll('.remove'), $.on('click', async ({currentTarget : ct}) => { if(await UI.confirm('?')) { _.go( ct, $.closest('.image'), $.removeIt, _ => UI.alert('삭제 완료')) } }))

findAll('remove') 전에 tap 함수를 넣어서 해당 함수에서 이미지들을 깔끔하게 렌더링 해주는 코드를 넣는다. each에서 한번에 평가를 시도하면서 순차적으로 렌더링 될 것이다.

순차적으로 렌더링 됨

만약 한번에 렌더링하고 싶다면 어떻게 해야할까? (1) 프로미스이고 시간이 걸리는 작업이니 병렬적으로 처리해야하고 (2) 모든 값을 가져와야하니 정답은 C.takeAll을 사용하는 것이다.

_.go( Images.fetch(), Images.tmpl, $.el, $.append($.qs('body')), _.tap( $.findAll('img'), L.map(img => new Promise(resolve => { img.onload = () => resolve(img); img.src = img.getAttribute('lazy-src'); })), C.takeAll, // 이 부분 추가 _.each($.addClass('fade-in')) ), $.findAll('.remove'), $.on('click', async ({currentTarget : ct}) => { if(await UI.confirm('?')) { _.go( ct, $.closest('.image'), $.removeIt, _ => UI.alert('삭제 완료')) } }))

C.takeAll을 사용한 결과

이번엔 부하를 조절하기 위해 그룹을 나눠서 4개씩 렌더링 해보자 C.takeAll과 addClass 해주는 부분 대신에 아래의 코드를 넣으면 된다.

lazy => { //그룹을 지어줌으로써 부하를 조절함 let r = L.range(Infinity); return _.go( lazy, _.groupBy(_ => Math.floor(r.next().value / 4)), // 인덱스 : [배열] 형태의 객체 L.values, //value인 배열만 뽑아냄 L.map(L.map(f => f())), L.map(C.takeAll), _.each(_.each($.addClass('fade-in'))) ) }

_.groupBy 라는 함수를 이용한다. n 개씩 묶어내어 2차원 배열을 만들어냈으므로 L.map을 두개 중첩해서 사용하도록 한다. 하지만 이렇게만 해두면 제대로 동작하지 않는다. 이유는 groupBy 내부에서 프로미스가 풀려버리기 때문이다. 풀린다면 L.map(f⇒f()) 에서 f가 함수가 아니라는 에러가 나올 것이다. 이를 방지하기 위해 다음과 같이 코드를 변경한다.

Images.loader = limit => _.tap( $.findAll('img'), L.map(img => _ => new Promise(resolve => { // 여기서 함수를 한층 더 쌓는다 img.onload = () => resolve(img); img.src = img.getAttribute('lazy-src'); })), lazy => { let r = L.range(Infinity); return _.go( lazy, _.groupBy(_ => { return Math.floor(r.next().value / limit) }), L.values, L.map(L.map(f => f())), L.map(C.takeAll), _.each(_.each($.addClass('fade-in'))) ) } ); _.go( Images.fetch(), Images.tmpl, $.el, $.append($.qs('body')), Images.loader(4), $.findAll('.remove'), $.on('click', async ({currentTarget : ct}) => { if(await UI.confirm('?')) { _.go( ct, $.closest('.image'), $.removeIt, _ => UI.alert('삭제 완료')) } }))

위와 같이 함수를 한단계 더 추가해도 자바스크립트의 클로저에 의해 img 변수가 저장된 상태로 프로미스가 선언된다. loader 함수도 따로 만들어서 limit 값을 받도록 했기때문에 원하는 그룹 사이즈를 조정해서 한번에 렌더링 되는 갯수를 조절할 수 있게 되었다.

함수들의 추상화 레벨 높이기

고차함수들을 사용하면 추상화레벨이 높은 함수들을 쉽게 만들 수 있다. 위의 Image.loader 함수를 더 작게 나누는 연습을 해보자. 먼저 알아둬야 할 점은 고차함수의 특징은 도메인, 데이터 형이 없다는 점이다. 그러므로 데이터형과 상관이 없는 추상화된 부분을 찾아야한다.

Images.loader = limit => _.tap( $.findAll('img'), L.map(img => _ => new Promise(resolve => { img.onload = () => resolve(img); img.src = img.getAttribute('lazy-src'); })), lazy => { //그룹을 지어줌으로써 부하를 조절함 let r = L.range(Infinity); return _.go( lazy, _.groupBy(_ => { return Math.floor(r.next().value / limit) }), L.values, L.map(L.map(f => f())), L.map(C.takeAll) ) }, _.each(_.each($.addClass('fade-in'))) );

addClass를 해주는 부분은 특정 데이터와 연관이 있으니 밖으로 빼줘보면 lazy ⇒ f 는 모두 추상화 레벨이 높은 섹션이다. 이 부분을 따로 함수로 빼낸다.

Images.loader = limit => _.tap( $.findAll('img'), L.map(img => _ => new Promise(resolve => { img.onload = () => resolve(img); img.src = img.getAttribute('lazy-src'); })), C.takeAllWithLimit(limit), _.each(_.each($.addClass('fade-in'))) ); C.takeAllWithLimit = _.curry((limit, iter) => { let r = L.range(Infinity); return _.go( iter, _.groupBy(_ => { return Math.floor(r.next().value / limit) }), L.values, L.map(L.map(f => f())), L.map(C.takeAll) ) })

이렇게 빼면 C.takeAllWithLimit은 평가를 기다리는 여러개의 값들을 그룹으로 나눠서 한번에 처리하도록하는 추상화레벨이 높은 함수가 된다. 여기서 한번 더 나눠볼 수 있는데 어떻게 해야할까??

먼저 변수 r 에 주목해야한다. r 은 평가가 지연된 상태로 무한하게 평가될 수 있는 range이다. 해당 변수는 _.groupBy 내부에서만 사용되므로 저 둘을 밖으로 빼서 함수로 만들 수 있다.

_.groupBySize = _.curry((size, iter) => { let r = L.range(Infinity); return _.groupBy(_ => Math.floor(r.next().value / size), iter) })

위와 같이 가능하다. 이 groupBySize 함수를 사용하는 C.takeAllWithLimit 함수는 보다 더 간단해질 것이다.

C.takeAllWithLimit = _.curry((limit, iter) => _.go( iter, _.groupBySize(limit), L.values, L.map(L.map(f => f())), L.map(C.takeAll) ))

함수들의 추상화 레벨 높이기 연습

_.go( Images.fetch(), Images.tmpl, $.el, $.append($.qs('body')), Images.loader(4), $.findAll('.remove'), $.on('click', async ({currentTarget : ct}) => { if(await UI.confirm('?')) { _.go( ct, $.closest('.image'), $.removeIt, _ => UI.alert('삭제 완료')) } }))

이미지를 fetch하고 그룹으로 나눠서 로딩하고 삭제버튼 등록하는 등의 작업을 하는 함수이다. 이 함수에서도 고차함수를 뽑아낼 수 있다.

UI.remover = (btnSel, targetSel, parent) => _.go( parent, $.findAll(btnSel), $.on('click', async ({currentTarget : ct}) => { await UI.confirm('?') && _.go( ct, $.closest(targetSel), $.removeIt, _ => UI.alert('삭제 완료')) }))

parent 의 자식노드 중 btnSel을 찾아서 클릭됐을 경우 targetSel을 삭제하도록하는 함수이다. 클로저를 이용해서 꼭 3개의 인자를 한번에 다 받을 필요없이 parent만 나중에 따로 받도록 할 수 있다.

UI.remover = (btnSel, targetSel) => _.pipe( $.findAll(btnSel), $.on('click', async ({currentTarget : ct}) => { await UI.confirm('?') && _.go( ct, $.closest(targetSel), $.removeIt, _ => UI.alert('삭제 완료')) }))

이제 어떤 목록이든 지울 수 있는 보다 더 추상화레벨이 높은 함수가 되었다!!

마무리

고차함수를 만드는 사례들을 통해 함수를 분리하는 것을 실습해봤다. 함수형 프로그래밍에서는 이렇듯 일어나야되는 일을 이터러블로 바라보고 이 이터러블한 대상을 여러 보조함수를 사용해서 다형성 및 추상화레벨을 높여준다.

수업에서 실습해온 것처럼 잘게 나누는 연습을 하고 이름을 잘 붙여나가면 생산성을 많이 높일 수 있을 것이다.

관련글

https://www.inflearn.com/course/%ED%95%A8%EC%88%98%ED%98%95_ES6_%EC%9D%91%EC%9A%A9%ED%8E%B8/dashboard

from http://mvmvm.tistory.com/75 by ccl(A) rewrite - 2021-08-11 00:00:17