명령형 자바스크립트 클래스를 함수형 선언적 코드로 바꾸는 8 단계

김진우 edited this page Oct 10, 2018 · 1 revision

명령형 자바스크립트 클래스를 함수형 선언적 코드로 바꾸는 8 단계

원문 : https://medium.com/front-end-hacking/8-steps-to-turn-imperative-javascript-class-to-a-functional-declarative-code-862964faf46c

functional-code

예를들어, 자바스크립트 코드를 사용하여 이벤트 리스너를 추가하고 이벤트를 전달하는 이벤트 관리자를 만들 것이다. 이 예제의 목적은 다중 패러다임 언어인 자바스크립트를 이용하여 명령형 코드선언적 코드를 함께 사용하는 두 가지 옵션을 보여줌으로써, 당신이 사용하고자 하는 언어의 특징을 스스로 결정할 수 있도록 하는 것이다.

선언적 코딩과 명령형 코딩의 차이점에 대해 더 자세히 알고 싶다면 이 링크를 살펴보기 바란다.

Class EventManager {
  construct (eventMap = new Map ()) {
    this.eventMap = eventMap;
  }
  addEventListener (event, handler) {
    if (this.eventMap.has (event)) {
      this.eventMap.set (event, this.eventMap.get (event).concat ([handler]));
    } else {
      this.eventMap.set (event, [handler]);
    }
  }
  dispatchEvent (event) {
    if (this.eventMap.has (event)) {
      const handlers = this.eventMap.get (event);
      for (const i in handlers) {
        handlers [i] ();
      }
    }
  }
}
const EM = new EventManager ();
EM.addEventListner ('hello', function () {
  console.log ('hi');
});
EM.dispatchEvent ('hello'); // hi

우리의 도전은...

  • 20 줄의 명령형 코드를 2가지 표현식을 가진 선언적 코드 7줄로 변환한다.
  • 중괄호 {}와 IF조건을 사용하지 않는다.
  • 명령형 코드가 포함되지 않아야 하며, 순수한 함수형 이어야 한다.
  • 함수는 정확히 하나의 인수만 가진 단항 함수만 작성하여야 한다.
  • 외부 데이터가 사용되거나 수정되지 않아야 한다, 사이드 이팩트가 없는 순수한 함수를 생성해라.
  • 합성 가능한 함수들을 만들어라.
  • 모든 것은 깨끗하고 느슨하게 결합되며 아름다워야 한다.

1 단계: 클래스를 함수로 바꾼다

const eventMap = new Map ();
function addEventListener (event, handler) {
  if (eventMap.has (event)) {
    eventMap.set (event, eventMap.get (event).concat ([handler]));
  } else {
    eventMap.set (event, [handler]);
  }
}
function dispatchEvent (event) {
  if (eventMap.has (event)) {
    const handlers = this.eventMap.get (event);
    for (const i in handlers) {
      handlers [i] ();
    }
  }
}

이 코드를 모듈로 사용하려면 코드 끝에 간단히 export 구문을 사용하기만 하면 된다.

// ./event-manager.js
export default {addEventListener, dispatchEvent};

그리고 나서 코드를 import하고 singleton으로 사용한다.

import * as EM from './event-manager.js';
EM.dispatchEvent ('event');

모듈은 singleton으로 동작한다. 모듈을 import 한 후 또 다른 위치에서 다시 import 하면 eventMap 변수는 공유 상태로 유지된다.

2 단계: 람다 화살표함수를 사용한다

화살표 함수 또는 람다 표현식을 아래처럼 쉽게 사용할 수 있다.

const eventMap = new Map ();
const addEventListener = (event, handler) => {
  if (eventMap.has (event)) {
    eventMap.set (event, eventMap.get (event).concat ([handler]));
  } else {
    eventMap.set (event, [handler]);
  }
}
const dispatchEvent = event => {
  if (eventMap.has (event)) {
    const handlers = eventMap.get (event);
    for (const i in handlers) {
      handlers [i] ();
    }
  }
}

function 키워드를 사용하는 대신 이제 람다를 통해 정의된 함수를 가진 상수를 갖는다. 그 밖의 변화는 없다, 하지만 화살표 함수새로 바인딩 되지 않은 this 를 사용한다는 것을 알아둬라. 처음에 클래스 내부에서 정의된 this처럼 사용할 수 없다.

3 단계: 사이드 이팩트를 제거하고 return 을 추가한다

eventMap을 함수의 인자와 리턴 값의 인자 중 하나로 만든다.

const addEventListener = (event, handler, eventMap) => {
  if (eventMap.has (event)) {
    return new Map (eventMap).set (event, eventMap.get (event).concat ([handler]));
  } else {
    return new Map (eventMap).set (event, [handler]);
  }
}
const dispatchEvent = (event, eventMap) => {
  if (eventMap.has (event)) {
    const handlers = eventMap.get (event);
    for (const i in handlers) {
      handlers [i] ();
    }
  }
  return eventMap;
}
const myMap =
  addEventListner ('hello', () => console.log ('hi'), new Map ());
dispatchEvent ('hello', myMap); // hi

이제 우리의 코드는 기능적으로 순수하며 합성이 가능한 상태로 변하고 있다. 새 맵을 추가하고 리턴 값 또한 새 맵을 리턴함으로써 데이터 변이로 인한 사이드 이팩트를 만들지 않게 된 것에 주목해라.

4 단계: for문 제거

for문을 forEach로 대체 할 것이다.

const addEventListener = (event, handler, eventMap) => {
  if (eventMap.has (event)) {
    return new Map (eventMap).set (event, eventMap.get (event).concat ([handler]));
  } else {
    return new Map (eventMap).set (event, [handler]);
  }
}
const dispatchEvent = (event, eventMap) => {
  if (eventMap.has (event)) {
    eventMap.get (event).forEach (a => a ());
  }
  return eventMap;
}

5 단계: 약간의 이진 논리를 적용

return 상태에 ||&& 를 올바르게 적용하려면 관련 함수의 리턴 값에 대해 정확히 이해하여야 한다. 만약 확실치 않으면 코드를 테스트해 보길 바란다.

const addEventListener = (event, handler, eventMap) => {
  if (eventMap.has (event)) {
    return new Map (eventMap).set (event, eventMap.get (event).concat ([handler]));
  } else {
    return new Map (eventMap).set (event, [handler]);
  }
}
const dispatchEvent = (event, eventMap) => {
  return (
    eventMap.has (event) &&
    eventMap.get (event).forEach (a => a ())
  ) || event;
}

6 단계: if를 3항 연산자로 바꾼다

if문 보다 더 쉽게 이해할 수 있으며, 심지어 삼항 연산자 내부에서 삼항 연산자를 사용하는 패턴으로 재미있게 사용할 수 있다.

const addEventListener = (event, handler, eventMap) => {
  return eventMap.has (event) ?
    new Map (eventMap).set (event, eventMap.get (event).concat ([handler])) :
    new Map (eventMap).set (event, [handler]);
}
const dispatchEvent = (event, eventMap) => {
  return (
    eventMap.has (event) &&
    eventMap.get (event).forEach (a => a ())
  ) || event;
}

7 단계: 이제 중괄호 {}는 필요없다

화살표 함수는 항상 표현식의 값을 리턴하기 때문에 {} 또는 return 문이 필요하지 않다.

const addEventListener = (event, handler, eventMap) =>
   eventMap.has (event) ?
     new Map (eventMap).set (event, eventMap.get (event).concat ([handler])) :
     new Map (eventMap).set (event, [handler]);
const dispatchEvent = (event, eventMap) =>
  (eventMap.has (event) && eventMap.get (event).forEach (a => a ()))
  || event;

8 단계: 커링

마지막 단계는 하나의 인수가 고차원 함수를 리턴하도록 커링하는 것이다. curry에 대한 자세한 내용은 필자의 또 다른 글 '자바스크립트 ES6 커리 기능과 실용적인 예제'에서 더 배울 수 있다. 여러분의 삶을 더 편하게 만들기 위해, 인수들을 일련의 화살표 함수로 바꾸는 (a, b, c) -> a => b => c 방법도 고려해 보면 좋을 것이다.

const addEventListener = handler => event => eventMap =>
   eventMap.has (event) ?
     new Map (eventMap).set (event, eventMap.get (event).concat ([handler])) :
     new Map (eventMap).set (event, [handler]);
const dispatchEvent = event => eventMap =>
  (eventMap.has (event) && eventMap.get (event).forEach (a => a ()))
  || event;

인수의 순서를 정하기 위해서는 애플리케이션의 부분인지 함수 합성인지 생각해볼 필요가 있다. 한번 해보면 아마도 어떤것을 선택해야 할지 쉽게 알 수 있을 것이다.

우리가 만든 두 가지의 표현식은 아래와 같이 여러 가지 방법으로 사용할 수 있다.

const log = x => console.log (x) || x;

// 있는 그대로의 사용
const myEventMap1 = addEventListener (() => log ('hi')) ('hello') (new Map ());
dispatchEvent ('hello') (myEventMap1); // hi

// 부분적용
let myEventMap2 = new Map ();
const onHello = handler => myEventMap2 = addEventListener (handler) ('hello') (myEventMap2);
const hello = () => dispatchEvent ('hello') (myEventMap2);
onHello (() => log ('hi'));
hello (); // hi

// 함수합성
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
const addEventListeners = compose (
  log,
  addEventListener (() => log ('hey')) ('hello'),
  addEventListener (() => log ('hi')) ('hello')
);
const myEventMap3 = addEventListeners (new Map ()); // myEventMap3
dispatchEvent ('hello') (myEventMap3); // hi hey

함수형 코드의 순수성과 관련한 다음 단계는 functors의 도입이다. 하지만 functor의 개념은 이제 막 함수형 프로그래밍을 시작하는 사람에게는 매우 복잡하므로 여기서 멈추도록 한다.

마지막 생각

각자의 스타일, 기술의 숙련도, 혹은 필요한정도에 때라 알맞은 단계에 맞춰 코드를 수정하면 된다. 코드의 사이드 이펙트와 테스트 용이성을 개선하고 싶다면 3단계 에서 멈춰라. 어떤 프로그래밍 패러다임이 당신과 가장 잘 맞는지 당신 스스로 결정할 수 있다. 행복한 코딩.

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.