자바스크립트에서 팩토리 함수란 무엇인가?


원문: https://www.sitepoint.com/factory-functions-javascript/

이 아티클은 Jeff Mott가 검토하였다. SitePoint 컨텐츠를 만들기 위해 최선을 다해준 모든 SitePoint 리뷰어들에게 감사한 마음을 전한다!

함수와 객체를 배우지 않고서는 자바스크립트 프로그래머로서 더 나아갈 수 없을 것이며, 이 두 가지가 함께 사용되는 경우 조합이라 불리는 강력한 객체 패러다임을 시작하는데 필요로 하는 빌딩 블록이 된다. 오늘 우리는 함수, 객체와 프라미스들을 조합하기 위해 사용되는 팩토리 함수의 몇 가지 관용적 패턴들을 살펴볼 것이다.

함수가 객체를 반환할 때 이것을 팩토리 함수라고 부른다.

간단한 예제를 보자.

function createJelly() {
  return {
    type: 'jelly',
    colour: 'red'
    scoops: 3
  };
}

이 팩토리 함수를 호출하면 매번 jelly 객체의 새로운 인스턴스를 반환할 것이다.

팩토리 함수명으로 create 접두사를 꼭 붙일 필요는 없지만, 명확하게 함수 의도를 전달할 수 있다는 점은 참고해두자. 마찬가지로 type 프로퍼티도 항상 사용할 필요는 없으며, 이것은 프로그램 상에서 흘러다니는 객체들을 구별할 수 있도록 도와준다.

파라미터를 받는 팩토리 함수

모든 함수들처럼, 파라미터로 객체를 변경하여 반환할 수 있는 팩토리를 정의할 수 있다.

function createIceCream(flavour='Vanilla') {
  return {
    type: 'icecream',
    scoops: 3,
    flavour
  }
}

이론상 다수의 인자값을 파라미터로 받아 특이하고 복잡하게 얽혀있는 객체를 반환하는 팩토리 함수를 사용할 수 있다. 하지만 앞으로 우리가 살펴볼 것처럼, 이것은 조합의 의도와 무관하다.

조합 가능한 팩토리 함수

다른 팩토리 함수를 사용해 팩토리 함수를 정의하면 더 작고 재사용 가능한 조각으로 복잡한 팩토리 함수들을 분리할 수 있다.

예를 들어, jellyicecream 팩토리로 생성된 객체를 반환하는 dessert 팩토리 함수를 만들 수 있다.

function createDessert() {
  return {
    type: 'dessert',
    bowl: [
      createJelly(),
      createIceCream()
    ]
  };
}

new 또는 this 없이 임의로 복잡한 객체를 생성하기 위해 팩토리 함수를 조합할 수 있다. is-a(동등)보다 has-a(포함) 관계로써 표현되는 객체는 상속 대신에 조합으로 구현될 수 있다.

상속의 예를 살펴 보자.

// A trifle *is a* dessert

function Trifle() {
  Dessert.apply(this, arguments);
}

Trifle.prototype = Dessert.prototype;

// or

class Trifle extends Dessert {
  constructor() {
    super();
  }
}

팩토리 조합으로 같은 개념을 표현할 수 있다.

// A trifle *has* layers of jelly, custard and cream. It also *has a* topping.

function createTrifle() {
  return {
    type: 'trifle',
    layers: [
      createJelly(),
      createCustard(),
      createCream()
    ],
    topping: createAlmonds()
  };
}

비동기 팩토리 함수

모든 팩토리 함수들이 즉시 데이터를 반환하도록 준비되진 않을 것이다. 예로 일부 팩토리 함수들은 처음에 데이터를 패치해야만 한다.

이 경우, 프라미스를 대신 반환하는 팩토리 함수를 정의할 수 있다.

function getMeal(menuUrl) {
  return new Promise((resolve, reject) => {
    fetch(menuUrl)
      .then(result => {
        resolve({
          type: 'meal',
          courses: result.json()
        });
      })
      .catch(reject);
  });
}

이런 종류의 복잡한 중첩 코드는 비동기 팩토리 함수를 읽고 테스트하기 어렵게 만든다. 이 코드는 2개의 팩토리 함수로 나눌 수 있으며 이를 조합해 보자.

function getMeal(menuUrl) {
  return fetch(menuUrl)
    .then(result => result.json())
    .then(json => createMeal(json));
}

function createMeal(courses=[]) {
  return {
    type: 'meal',
    courses
  };
}

콜백을 사용할 수도 있지만, 우리는 이미 프라미스 객체들을 반환하는 조합 팩토리 함수인 Promise.all을 가지고 있다.

function getWeeksMeals() {
  const menuUrl = 'jsfood.com/';

  return Promise.all([
    getMeal(`${menuUrl}/monday`),
    getMeal(`${menuUrl}/tuesday`),
    getMeal(`${menuUrl}/wednesday`),
    getMeal(`${menuUrl}/thursday`),
    getMeal(`${menuUrl}/friday`)
  ]);
}

비동기로 동작하면서 프라미스를 반환하는 팩토리 함수를 나타내기 위해 컨벤션으로 create 대신 get을 사용하고 있다.

함수와 메서드

지금까지 메서드를 가진 객체를 반환하는 그 어떤 팩토리 함수도 보지 못했을텐데 이것은 의도적이었다. 일반적으로 그럴 필요가 없기 때문이다.

팩토리 함수는 연산으로부터 데이터를 분리할 수 있다.

이는 객체가 세션 사이에서 유지되면서 HTTP 또는 웹소켓으로 전송되고 데이터로 저장될 때 중요한 JSON으로 직렬화될 수 있음을 의미한다.

예를 들어 jelly 객체에 eat 메서드를 정의하기보다, 파라미터로 jelly 객체를 받아 변경하여 반환하는 새 함수를 정의할 수 있다.

function eatJelly(jelly) {
  if(jelly.scoops > 0) {
    jelly.scoops -= 1;
  }
  return jelly;
}

변화되는 데이터 없이 프로그래밍 하기를 선호하는 사람들은 약간의 문법적인 도움을 사용해 다음 코드처럼 할 수 있다.

function eat(jelly) {
  if(jelly.scoops > 0) {
    return { ...jelly, scoops: jelly.scoops - 1 };
  } else {
    return jelly;
  }
}

이제 이렇게 쓰는 대신

import { createJelly } from './jelly';

createJelly().eat();

이렇게 쓸 수 있다.

import { createJelly, eatJelly } from './jelly';

eatJelly(createJelly());

마지막 결과는 객체를 받아 객체를 반환해주는 함수이다.

그래서 객체를 반환하는 함수를 무엇이라고 부르는가? 팩토리!

고차 함수

고차 함수로써 팩토리 함수를 주고받는 것은 많은 제어권을 준다. 예를 들어서 인헨서(enhancer)를 생성하기 위해 다음 개념을 사용할 수 있다.

function giveTimestamp(factory) {
  return (...args) => {
    const instance = factory(...args);
    const time = Date.now();
    return { time, instance };
  };
}

const createOrder = giveTimestamp(function(ingredients) {
  return {
    type: 'order',
    ingredients
  };
});

이 인헨서는 존재하는 팩토리 함수를 파라미터로 받고, 타임스탬프와 함께 인스턴스를 반환하는 팩토리를 생성하기 위해 해당 팩토리 함수를 감싼다. 만약 팩토리 함수가 불변 객체를 반환하길 원한다면 freezer로 개선할 수 있다.

function freezer(factory) {
  return (...args) => Object.freeze(factory(...args)));
}

const createImmutableIceCream = freezer(createIceCream);

createImmutableIceCream('strawberry').flavour = 'mint'; // Error!

결론

현명한 프로그래머가 말하길

잘못된 추상화보다 추상화가 없는 것이 복구하기에 훨씬 쉽다.

자바스크립트 프로젝트는 설계를 위해 자주 권장되는 복잡한 추상화 레이어들 때문에 테스트와 리팩토링이 어려워지는 경향이 있다.

프로토타입과 클래스는 자바스크립트에 추가된 이후로 여전히 혼란의 원인new, this와 같이 일반적이지 않은 도구를 사용해 단순한 아이디어를 구현한다.

객체와 함수는 대부분의 프로그래머들에게 자바스크립트 기본 형식으로 이해되서, 팩토리 함수가 추상화가 아니라는 논쟁거리가 될 수 있다.

팩토리 함수와 같은 단순한 빌딩 블록을 사용하는 것은 경험이 부족한 프로그래머들에게 우리가 짠 코드를 친숙하게 만들 수 있고, 우리는 이를 신경써야만 한다. 팩토리 함수는 고수준의 추상화에 이르는 것을 막지 않고, 조합 능력을 가진 기본 요소들과 함께 복잡하고 비동기적인 데이터를 모델링할 것을 권장한다. 자바스크립트는 단순함에 충실할 때 달콤하다!