본문 바로가기

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

함수형 프로그래밍2 - 프론트단에서 응용

https://www.inflearn.com/course/%ED%95%A8%EC%88%98%ED%98%95_ES6_%EC%9D%91%EC%9A%A9%ED%8E%B8f 를 수강하며 내용을 정리한 글입니다.

이미지 받아와서 화면에 뿌리기

서버에서 이미지를 받아와서 화면에 뿌려주는 코드 작성해본다.

const Images = {}

Images.fetch = () => new Promise(resolve => setTimeout(() => resolve([
    { name: "HEART", url: "https://s3.marpple.co/files/m2/t3/colored_images/45_1115570_1162087.png" },
    { name: "하트", url: "https://s3.marpple.co/f1/2019/1/1235206_1548918825999_78819.png" },
    { name: "2", url: "https://s3.marpple.co/f1/2018/1/1054966_1516076769146_28397.png" }, { name: "6", url: "https://s3.marpple.co/f1/2018/1/1054966_1516076919028_64501.png"},{"name":"도넛","url":"https://s3.marpple.co/f1/2019/1/1235206_1548918758054_55883.png"},{"name":"14","url":"https://s3.marpple.co/f1/2018/1/1054966_1516077199329_75954.png"},{"name":"15","url":"https://s3.marpple.co/f1/2018/1/1054966_1516077223857_39997.png"},{"name":"시계","url":"https://s3.marpple.co/f1/2019/1/1235206_1548918485881_30787.png"},{"name":"돈","url":"https://s3.marpple.co/f1/2019/1/1235206_1548918585512_77099.png"},{"name":"10","url":"https://s3.marpple.co/f1/2018/1/1054966_1516077029665_73411.png"}
  ]), 200));

Images.tmpl = imgs => `
  <div class="images">
    ${
        _.map(img => `<div class="image">
            <div class="box"><img src="${img.url}"></div>
            <div class="name">${img.name}</div>
        </div>`, imgs)
    }
  </div>
`;

Images.fetch 함수를 실행하면 서버로부터 데이터를 받아온다고 가정한다. 위의 Images.tmpl의 결과는 _.map의 결과로 배열이 출력될 것이므로 이를 합쳐줘야한다.

Images.tmpl = imgs => `
  <div class="images">
    ${
        string(_.map(img => `
        <div class="image">
            <div class="box"><img src="${img.url}"></div>
            <div class="name">${img.name}</div>
        </div>${'\n'}`, imgs))
    }
  </div>
`;

const string = iter => _.reduce((a,b) => `${a}${b}`, iter);

_.go(
    Images.fetch(),
    Images.tmpl,
    console.log
)

엘리먼트로 만들어주는 함수와 body에 붙여주는 함수를 만든다.

$.qs = (sel, pa) => document.querySelector(sel, pa);
//$.qs = document.querySelector.bind(document);
$.append = _.curry((p, c) => p.appendChild(c));
$.el = html => {
    const wrap = document.createElement('div');
    wrap.innerHTML = html;
    return wrap.children[0];
}



_.go(
    Images.fetch(),
    Images.tmpl,
    $.el,
    $.append($.qs('body')),
    console.log
)

화면에 이쁘게 렌더링 된다. 이제는 삭제하는 것을 만들어본다.

_.go(
    Images.fetch(),
    Images.tmpl,
    $.el,
    $.append($.qs('body')),
    $.findAll('.remove'),
    console.log
)

findAll이라는 함수를 통해 remove 클래스를 가진 엘리먼트를 모두 찾아올 것이다. findAll은 아래와 같이 구현한다.

$.qs = (sel, pa = document) => document.querySelector(sel, pa);
$.qsa = (sel, pa = document) => document.querySelectorAll(sel, pa);

$.find = _.curry($.qs);
$.findAll = _.curry($.qsa);

qs와 qsa는 가변적인 인자를 받으므로 쿼리를 적용하기 위해 따로 find와 findAll 함수를 만든다. 이제 찾아온 remove 엘리먼트들에 이벤트를 달아줄 것이다.

$.closest = _.curry((sel, el) => el.closest(sel));
$.removeIt = el => el.parentNode.removeChild(el)

_.go(
    Images.fetch(),
    Images.tmpl,
    $.el,
    $.append($.qs('body')),
    $.findAll('.remove'),
    _.each(el => el.addEventListener('click', (e) => _.go(
        e.currentTarget,
        $.closest('.image'),
        $.removeIt,
        console.log
    ))),
    console.log
)

그리고 자주사용될 법한 함수는 따로 빼서 만들어본다. _.each 함수를 한번 빼볼 것이다.

$.on = (event, f) => els => _.each(el => el.addEventListener(event, f), _.isIterable(els) ? els : [els]);

 

함수형을 통해 추상화 구현

우선 알림창을 만들어본다. 이미지에 삭제버튼을 눌렀을때 confirm 창과 삭제가 완료되었음을 알리는 alert 창을 만들것이다.

const UI = {};
UI.confirm = msg => new Promise(resolve => _.go(
   //알림창 만드는 코드,
	 //버튼 클릭시에 resolve
))

async function f() {
    await UI.confirm('??');
    console.log('hi!');
}

f();

윈도우가 제공하는 기본 confirm 창처럼 확인 버튼을 눌러야 다음 로직으로 넘어가는 것을 구현하려면 위와 같이 커스텀 confirm 창을 프로미스로 만들고 마지막에 resolve를 해서 넘어가도록 구현한다. 세부적인 코드는 아래와 같다.

UI.confirm = msg => new Promise(resolve => _.go(
    `
    <div class="confirm">
        <div class="body">
            <div class="msg">${msg}</div>
            <div class="button">
                <button type="button" class="cancel">취소</button>
                <button type="button" class="ok">확인</button>
            </div>
        </div>
    </div>
    `,
    $.el,
    $.append($.qs('body')),
    _.tap(
        $.find('.ok'),
        $.on('click', e => _.go(
            e.currentTarget,
            $.closest('.confirm'),
            $.removeIt,
            _ => resolve(true)
    ))),
    _.tap(
        $.find('.cancel'),
        $.on('click', e => _.go(
            e.currentTarget,
            $.closest('.confirm'),
            $.removeIt,
            _ => resolve(false)
    ))),
))

ok를 눌렀을 경우엔 프로미스가 true로 평가되고 cancel을 누르면 false로 평가된다. 이 함수는 기존의 go 함수에서 x 버튼을 클릭했을때 실행되도록 해야한다.

_.go(
    Images.fetch(),
    Images.tmpl,
    $.el,
    $.append($.qs('body')),
    $.findAll('.remove'),
    $.on('click', async e => {
	    if(await UI.confirm('?')) { // 내부에서 e = null
        _.go(
	        e.currentTarget,
	        $.closest('.image'),
	        $.removeIt)
	    }
	 }
))

하지만 이는 문제가 발생한다. if문 내부에서 e가 null로 변해서 currentTarget을 찾을 수 없다는 에러가 나올 것이다. 이를 방지하기 위해 구조분해를 사용한다.

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

이제 confirm창을 그대로 따와서 alert 창을 만들어보자

UI.alert = msg => new Promise(resolve => _.go(
    `
    <div class="confirm">
        <div class="body">
            <div class="msg">${msg}</div>
            <div class="button">
                <button type="button" class="ok">확인</button>
            </div>
        </div>
    </div>
    `,
    $.el,
    $.append($.qs('body')),
    _.tap(
        $.find('.ok'),
        $.on('click', e => _.go(
            e.currentTarget,
            $.closest('.confirm'),
            $.removeIt,
            _ => resolve(true)
    )))
))

이렇게 둘 다 만들어놓고 보니 confirm과 alert 간에는 중복이 정말 많이 있다. 객체지향 프로그래밍에서 클래스를 상속받아 하위 클래스를 구현하는 것처럼 상위 클래스의 역할을 하는 message 라는 함수를 만들고 이 message 함수에 적절한 인자를 넘겨서 confirm과 alert가 동작하도록 할 것이다. 

UI.message = (메세지, 버튼) => {}
UI.confirm = UI.messge(인자들 넘김)
UI.alert= UI.messge(인자들 넘김)

 

위와 같은 방식으로 클래스를 대신해서 함수로 추상화를 해낸 것이다. UI.message를 구현한 코드를 보면

UI.message = _.curry((btns, msg) => new Promise(resolve => _.go(
    `
    <div class="confirm">
        <div class="body">
            <div class="msg">${msg}</div>
            <div class="buttons">
                ${_.strMap(btn => `
                        <button type="button" class="${btn.type}">${btn.name}</button>
                    `, btns)}
            </div>
        </div>
    </div>
    `,
    $.el,
    $.append($.qs('body')),
    ..._.map(btn => _.tap(      //map 결과를 펼침
        $.find(`.${btn.type}`),
        $.on('click', e => _.go(
            e.currentTarget,
            $.closest('.confirm'),
            $.removeIt,
            _ => resolve(btn.value)
    ))), btns)
)))

이렇게 추상화를 이루어냈다. 나머지 코드는 기존의 confirm과 비슷하고 아래에 ...map 하는 부분을 주목해볼만 하다.

해당 map의 결과는 btn에 클릭 이벤트를 달아주는 '함수' 들의 배열이다. 그러므로 전개연산자를 통해 펼쳐서 각 함수들이 순차적으로 go 함수의 내부에서 실행되도록 해야한다. 이를 좀 더 쉽게 풀어보면

UI.message = btns => _.go(
    _.map(btn => _.tap(f), btns) // [_.tap(), _.tap(), _.tap(), _.tap(), ...]
)

//전개 연산자를 붙이면
UI.message = btns => _.go(
   _.tap(f),
	 _.tap(f), 
	 _.tap(f),
	 _.tap(f)
)
//_.tap 함수는 받은 인자를 내부의 로직을 수행한 후 그대로 다음 함수에 전달하는 함수이다.