본문 바로가기

프론트엔드/자바스크립트

함수형 프로그래밍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

 

함수형 프로그래밍과 JavaScript ES6+ 응용편 - 인프런 | 강의

이 강좌는 함수형 프로그래밍과 JavaScript ES6+의 시리즈 강좌로 다양한 응용 사례를 다룹니다., 함수형 프로그래밍과 JavaScript ES6+ 응용편 이 강의는 '함수형 프로그래밍과 JavaScript ES6+'의 후속 강의

www.inflearn.com