자바스크립트

Cycle.js 소개

이 글은 팀 내에서 진행했던 “자바스크립트 프레임워크 스터디”의 결과물을 정리한 글이다. 다른 프레임워크 (React, Angular, Vue) 에 대한 설명은 Toast Meetup 에서 확인할 수 있고, 원문은 http://meetup.toast.com/posts/97 에서 확인할 수 있다.

소개

Cycle.js는 이벤트 스트림 방식을 기반으로 프론트 엔드 어플리케이션을 만들 수 있도록 해 주는 프레임워크이다. RxJS의 컨트리뷰터 중 한 명인 André Staltz가 만들었으며, RxJS를 기반으로 완전한 반응형(Reactive) 프로그래밍을 할 수 있게 해 준다. André Staltz는 Cycle.js를 소개하며 React가 이름과는 다르게 완전히 Reactive 하지 않다는 비판을 하기도 했는데, 그런 의미에서 Cycle.js는 React가 일부 도입한 반응적, 함수형 특징을 좀 더 극단까지 밀어붙인 프레임워크라고도 할 수 있을 것이다.

Cycle.js는 함수형, 반응형이라는 두 가지 프로그래밍 방식을 강조한다. 함수형 프로그래밍은 이제 자바스크립트에서 익숙한 개념이 되었지만, 반응형 프로그래밍이라는 용어는 생소한 분들도 있을 텐데, 그런 분들은 이 글을 읽어보시기 바란다. 사실 반응형 프로그래밍의 정의는 경우에 따라 조금씩 다른데, 저자가 말하는 반응형 프로그래밍이란 결국 비동기 데이터 스트림을 이용해 프로그래밍하는 것을 말하며, 이를 위해 RxJS와 같은 이벤트 스트림 라이브러리를 적극 활용한다. 이런 방식의 프로그래밍을 함수 반응형 프로그래밍 (Functional Reactive Programming) 이라고 부르기도 하는데, 이런 특징으로 인해 Cycle.js 로 만들어진 코드는 this 키워드가 없고, setState()foo.update() 와 같은 명령형 호출을 하지 않는다.

사실 일명 함수 반응형 프로그래밍(FRP)이라는 용어에 대해서도 여러 이견이 있다. 여기에 대해서도 André Staltz가 자신의 의견을 정리한 글이 있으니 관심 있으신 분들은 참고하길 바란다.

RxJS

사실 RxJS를 설명하지 않고 Cycle.js를 설명하기는 힘들 것 같다. RxJS는 ReactiveX의 Javascript 구현 라이브러리이며, Observable 이라는 데이터 모델을 사용해 이벤트 기반의 비동기 코드를 쉽게 다룰 수 있게 해 준다. ReactiveX는 MS나 Netflix같은 회사에서 적극 사용해온 것으로 유명하며, 특히 최근에 정식 발표된 Angular2 에서도 RxJS를 도입하는 등 점점 사용층이 두터워지고 있다.
추가로 ReactiveX에 대해 한글로 아주 잘 정리된 글도 있으니 관심있는 분들은 참고하면 좋을 것 같다.

사실 RxJS와 같은 스트림 기반의 반응형 프로그래밍에 대한 이해가 없이는 Cycle.js를 제대로 이해하기가 힘들다. 하지만 이 글에서 상세한 내용을 다루기는 힘들기 때문에, 관심있는 분들은 앞에서 언급된 링크들을 꼭 먼저 읽어보길 바란다.

개발/구동 환경

의존성

Cycle.js는 단 하나의 외부 의존성을 가지는데, 위에서 말한 것처럼 특정한 이벤트 스트림 라이브러리를 필요로 한다. RxJS를 기반으로 만들어졌기 때문에 기본적으로는 RxJS에 의존성이 있었는데, 최근에 별도의 스트림 라이브러리가 필요하다는 판단하에 xstream 이라는 라이브러리를 만들었으며, RxJS 대신 사용하기를 권장하고 있다. xstream은 RxJS 보다 작고, 빠르며 hot stream만 지원하는 등 Cycle.js에 특화되어 있다. 하지만 여전히 RxJS (v4)를 사용할 수 있으며, 뿐만 아니라 RxJS (v5)most.js 등의 라이브러리로 대체할 수도 있다.

이 외에 Cycle.js는 내부적으로 Virtual-DOM을 위해 Snabbdom 이라는 라이브러리를 사용하는데, 외부 의존성으로 분리되어 있지 않기 때문에 별도로 다운로드 받을 필요는 없다.

개발 환경

꼭 필요한 것은 아니지만, Cycle.js의 튜토리얼 문서를 보면 Babel 이나 TypeScript 와 같은 트랜스파일러와 함께 browserify나 webpack 와 같은 번들링 툴을 사용하기를 권장하고 있다. 이는 라이브러리 자체가 npm과 ES6의 모듈을 활용해서 의존성 관리, 번들링 등을 하기 쉽도록 구성되어 있을 뿐만 아니라, 함수형의 코드를 작성함에 있어서 ES6의 문법이 많은 도움이 되기 때문이다. 또한 Babel 플러그인과 snabbdom-jsx를 활용하면, JSX 문법을 사용해서 Virtual-DOM을 만들어낼 수도 있다.

단, Cycle.js는 7.0.0(Diversity)에서부터 TypeScript로 완전히 재작성되었기 때문에 Babel 보다는 TypeScript와의 궁합이 더 좋을 수도 있을 것 같다.

구동 환경

사실 Cycle.js의 약점이라고 할 수도 있는 부분인데, 브라우저 지원 범위가 그리 넓지 않다. Cycle DOM 리파지토리에 명시된 바에 의하면 정식으로 지원하는 IE 버전은 Window7의 IE10과 IE11 뿐이다.

screen shot 2016-09-27 at 12 34 34 pm

(출처: cycleDOM)

물론 위의 표에서 제외된 브라우저에서도 실행가능할 수는 있지만, 공식 지원범위에 포함되지 않기 때문에 100% 정상 동작을 보장할 수는 없을 것이다.

아키텍처

기본 Data-Flow

Cycle.js은 기본적으로 사용자와 컴퓨터가 스트림을 기반으로 입력(Input)과 출력(Output)을 주고받는 함수라고 가정한다. 코드로 표현하면 다음과 같을 것이다.

function computer(inputDevices) {
  return outputDevices;
}

function human(senses) {
  return actuators;
}

이를 그림으로 표현하면 다음과 같다.

screen shot 2016-09-27 at 10 19 35 am

(출처: cycle.js.org)

즉, 컴퓨터는 마우스나 키보드와 같은 디바이스를 사용해 입력을 받고, 모니터 화면, 스피커 등을 사용해 출력을 내보낸다. 사용자는 눈이나 귀 등의 감각기관으로 컴퓨터의 출력을 입력받은 다음에 특정 행위를 함으로써 출력을 내보낼 것이다. 이 행위들은 위해 컴퓨터의 디바이스들을 사용하기 때문에 이는 다시 컴퓨터의 입력이 된다.

이러한 입력/출력 데이터를 일련의 스트림으로 처리할 수 있도록 도와주는 것이 RxJS와 같은 라이브러리이며, 이렇게 컴퓨터와 사용자의 입력과 출력을 연결하여 일종의 순환 싸이클을 만들어주는 것이 바로 Cycle.js의 역할이다. (이제 왜 이름이 Cycle인지 이해할 수 있을 것이다.)

André Staltz의 만약 사용자가 하나의 함수라면?라는 발표를 보면 이러한 기본 개념을 아주 잘 설명하고 있으니 꼭 확인해 보기 바란다.

Cycle.run(main, drivers)

이러한 순환 싸이클을 만들기 위해 Cycle.js 에서는 두 개의 파라미터를 받는 run 함수를 제공한다. 첫 번째 파라미터는 보통 main 이라고 불리는 함수이며, 위에서 computer로 표현된 함수처럼 입력 디바이스로부터의 데이터 스트림을 파라미터로 받고 출력 디바이스로 내보낼 데이터 스트림을 반환하는 함수이다. 두 번째 인자는 드라이버라는 개념의 함수들인데, main 함수에서 출력된 이벤트 스트림을 받아서 사용자에게 보여줄 수 있는 형태로 변환하고, 반대로 사용자로부터 이벤트를 받아서 스트림으로 반환할 수 있는 API를 제공해 주기도 하는 역할을 한다. 대표적인 드라이버로 DOM Driver가 있으며, 이는 main 함수에서 반환되는 Virtual-DOM 구조의 스트림을 실제 DOM으로 변환해주고 실제 DOM에서 사용자의 이벤트를 읽어 들일 수 있는 API를 제공해 준다.

이 드라이버라는 이름은 실제로 운영체제에서 사용되는 드라이버의 개념과 유사하다. 운영체제에서 특정 하드웨어와의 연결을 위해 중간에서 어댑터의 역할을 하는 것을 드라이버라고 하듯이, Cycle.js의 드라이버는 main 함수와 외부 환경 (DOM, Browser API, HTTP통신 등)을 연결해 주는 어댑터의 역할을 한다고 할 수 있을 것이다.

Cycle.js에서 특별히 유념할 점은 모든 부작용(Side Effect)이 드라이버에서 처리된다는 점이다. 예를 들어 DOM Driver는 DOM 변경과 같은 실제 브라우저에 종속적인 모든 부작용들을 다 내부에서 처리한다. 이로 인해 main 함수를 단순히 입력/출력을 스트림으로 받는 순수 함수로 유지할 수 있으며, 단위 테스트나 유지보수에 있어 큰 이점이 될 수 있다.

screen shot 2016-09-27 at 12 25 56 pm

(출처: cycle.js.org)

위의 그림에 있는 하단의 박스에서 DOM, HTTP 등의 부작용을 처리하는 것이 드라이버이다. main 함수의 출력 스트림을 받아서 부작용을 처리하고 main 함수에게 입력 스트림을 제공하는 역할을 하는 것이다. run 함수는 여기서 main 함수와 드라이버들의 입출력 스트림을 서로 연결해 주는 역할을 한다.

그럼 아주 간단한 예제를 한번 살펴보자. 아래 코드는 체크박스의 상태가 변경될 때마다 p 태그 내부의 텍스트를 ON 혹은 off 로 변경한다.

import xs from 'xstream';
import {run} from '@cycle/xstream-run';
import {div, input, p, makeDOMDriver} from '@cycle/dom';

function main(sources) {
  return {
    DOM: sources.DOM
      .select('input').events('change')
      .map(ev => ev.target.checked)
      .startWith(false)
      .map(toggled =>
        div([
          input({attrs: {type: 'checkbox'}}), 'Toggle me',
          p(`${toggled ? 'ON' : 'off'}`)
        ])
      )
  };
}

Cycle.run(main, {
  DOM: makeDOMDriver('#app')
});

위의 코드의 run 함수에서 두 번째 파라미터로 넘긴 객체의 DOM 프라퍼티가 바로 DOM Driver 이다. makeDOMDriver 함수를 이용해 주어진 CSS 선택자로 DOM Driver를 만들 수 있으며, 이렇게 넘겨진 DOM Driver는 출력값으로 DOMSource 객체를 만들어 반환한다. 이 DOMSource 객체는 main 함수의 입력값인 sources.DOM 으로 넘겨지며, 이 DOMSource에서 제공되는 selectevents API를 사용하면 실제 DOM에서 발생하는 이벤트를 스트림으로 만들어 낼 수 있다. 그리고 이러한 이벤트 스트림을 변환하여 main 함수에서 최종적으로 반환되는 Virtual-DOM 스트림은 다시 DOM Driver의 입력값으로 들어가게 되며, 실제 DOM으로 변환되어 화면에 표시된다.

Model-View-Intent

위에서 살펴보았듯이 Cycle.js의 API는 사실 run 함수가 전부라고 할 수 있고, 이를 위해 구현해야 할 어플리케이션 코드는 사실 main 함수가 전부이다. main 함수를 어떤 식으로 구현할 지는 코드를 작성하는 사람이 임의대로 결정하면 되며, Angular나 React/Redux 처럼 따라야만 하는 특정한 API가 없다.
하지만 main 함수가 너무 복잡해지는 것을 대비해 Cycle.js에서 권장하는 구조가 있는데, 이름하여 MVI 즉, Model-View-Intent이다.

screen shot 2016-09-27 at 10 22 11 am

(출처: cycle.js.org)

위와 같이 main 함수에서 하는 일을 순서대로 Intent -> View -> Model 의 순서로 나누어 작성할 수 있다. 앞서 보았던 코드를 다시 작성하면 다음과 같이 만들 수 있을 것이다.

function main(sources) {
  const actions = intent(sources.DOM);
  const state$ = model(actions);
  const vdom$ = view(state$);

  return {
    DOM: vdom$
  }
}

세부 내용은 잠시 후 살펴보기로 하고 일단 여기서는 몇 가지 주요 특징들을 살펴보자.

  • intentmodelview 모두 순수 함수이다.
  • state$vdom$ 마지막의 $는 일종의 컨벤션이며 이 변수가 스트림임을 의미한다.
  • intent 함수는 DOMSource로부터 이벤트 스트림들을 만들어 actions 객체로 묶어 반환한다.
  • model 함수는 intent로부터 받은 이벤트 스트림들을 변환해서 실제 어플리케이션의 상태(State) 변화를 나타내는 하나의 스트림을 만들어 반환한다.
  • view 함수는 model로부터 받은 상태 스트림을 변환해서 VNode(Virtual Node: Virtual-DOM을 구성하는 Node) 트리의 스트림을 만들어 반환한다.

Intent

Intent는 번역하자면 의도 정도가 될 것 같은데, 말 그대로 사용자의 의도를 이벤트 스트림을 이용해 정의한다고 이해하면 될 것 같다. 먼저 위의 예제에 맞는 intent 함수를 만들어보자.

function intent(domSource) {
  const change$ = domSource.select('input')
      .events('change')
      .map(ev => ev.target.checked);

  return {change$};
}

domSource는 위에서 설명했듯이 DOM Driver가 반환하는 객체이며, select 메소드는 CSS 셀렉터를 사용해 스코프를 제한해 주고, events 메소드는 주어진 DOM 이벤트가 발생할 때마다 이벤트 데이터가 발생하는 스트림을 반환해준다. 마지막에 있는 map 함수는 RxJS나 xstream의 스트림 객체에서 제공하는 API이며 해당 스트림을 변환해 새로운 스트림으로 반환한다. 즉 위의 intent 함수는 체크박스의 상태가 변경될 때마다 true/false 데이터가 발생하는 스트림을 만들어 반환되는 객체의 change$ 라는 속성으로 할당하는 것이다.

Model

Model은 어플리케이션의 상태를 관리한다는 점에서 우리가 흔히 알고 있는 MVC 패턴의 Model과 유사하다고 볼 수 있다. 중요한 다른 점은 아마 스트림을 입력받아 스트림을 반환한다는 점일 것이다. 정확히는 여러 개의 이벤트 스트림을 입력받아 하나의 상태 스트림을 반환한다고 할 수 있다. 코드를 살펴보자.

function model(actions) {
  const toggled$ = actions.change$.startWith(false);

  return toggled$;
}

코드가 엄청 간단한데, 실제로 여기서는 Intent에서 반환된 이벤트 스트림이 하나밖에 없고, Model이 단지 초기값을 할당하는 정도의 역할만을 할 뿐이기 때문이다. 만약 Intent에서 반환되는 객체가 change$ 외에 다른 이벤트 스트림을 가진다면 Model은 이들 스트림들을 합쳐서 하나의 상태 스트림으로 반환해야만 한다. 체크박스 외에 별도의 인풋 요소가 존재하고 키가 입력될 때마다 텍스트가 추가되는 이벤트 스트림을 actions.keydown$ 이라고 가정해보자.

function model(actions) {
  const toggled$ = actions.change$.startWith(false);
  const text$ = actions.keydown$.startWith('');

  return xs.combine(toggled$, text$)
    .map(([toggled, text]) => ({
      toggled, text, 
    }));
}

위의 xs.combine은 xstream의 API이며 여러개의 스트림을 합쳐서 하나의 스트림으로 반환해준다. 이처럼 Model에서 반환되는 스트림의 데이터는 어플리케이션의 전체 상태를 나타내는 객체이며 이는 스트림인 것만 빼면 Redux에서 사용하는 단일 상태 (Single State)의 개념과 유사하다고 볼 수 있다. 실제로 이렇게 스트림을 합치는 부분을 제외한 나머지 부분을 Redux 처럼 상태 객체를 변화하는 Reducer 함수로 분리해서 사용할 수 있다.

View

View역시 MVC 패턴의 View와 유사하다고 볼 수 있다. 차이점은 Model과 마찬가지로 스트림을 입력받아 스트림을 반환한다는 점이다. 코드를 살펴보자.

function view(state$) {
  return state$.map(toggled => 
    div([
      input({attrs: {type: 'checkbox'}}), 'Toggle me',
      p(`${toggled ? 'ON' : 'off'}`)
    ])
  );
}

이때 입력값은 Model에서 넘어온 상태 스트림이고, 반환하는 값은 VNode 트리의 스트림이다. 앞에서 언급했듯이 Cycle.js는 내부적으로 Snabbdom 라이브러리를 사용하는데, Snabbdom은 기본적으로 VNode 트리를 생성하기 위해 hyperscript 문법을 사용한다. 코드에 보이는 divpinput과 같은 함수들은 hyperscript 를 좀더 쉽게 사용할 수 있도록 cycleDOM 에서 제공해주는 헬퍼 함수들이다. 자세한 API는 Snabbdom 문서 에서 확인할 수 있다.

React 와의 비교

사실 스트림 기반의 구조라는 것을 제외하면 Virtual-DOM을 사용하거나 단일 상태 객체를 사용하는 점 등은 React/Redux 구조와 유사하다고 볼 수 있을 것이다. 한가지 명확하게 다른 점은 바로 Intent인데, React에서 <button onclick={handler}> 와 같은 식으로 Virtual-DOM 구조에 직접 이벤트 핸들러를 정의하는 것과는 반대의 접근방법을 취하고 있다. 오히려 예전에 jQuery와 같은 라이브러리에서 직접 셀렉터를 사용해 이벤트 핸들러를 할당하던 방식에 좀 더 가깝다고 할 수 있을 것 같다.

이는 View의 역할을 단순하게 Model의 상태 변경에 반응하여 화면을 그려주는 역할로 한정시켜 단일 책임의 원칙에 좀 더 충실하게 하고, 좀 더 반응적(Reactive)으로 만들기 위함이다. 또한 이렇게 함으로써 유저의 의도(Intent)를 추가하는 작업이 View에 영향을 끼치지 않게 되어 두 모듈의 역할을 명확하게 분리할 수 있게 된다.

사실 이벤트 핸들링이 필연적으로 View의 구조에 영향을 받을 수밖에 없다는 점을 생각해 보면, 두 가지를 분리하는 이러한 방식은 오히려 코드 관리를 힘들게 할 수가 있다. Cycle.js 에서는 이러한 단점을 최소화할 수 있도록 사용자의 Intent를 정의할 때에 DOM 구조에 종속적이기 보다는 className을 적극 활용할 수 있도록 isolate()와 같은 헬퍼 함수들을 제공해 주고 있다.

컴포넌트

Cycle.js는 모든 것을 스트림으로 다루기 때문에, 컴포넌트 단위로 구조화를 할 때도 스트림을 기반으로 작성해야 한다. 단순히 모듈 단위로 나누는 작업과는 차이가 있어서 간단히 이해하기가 쉽지 않은데, 일단 아래 그림을 보자.

screen shot 2016-09-27 at 10 23 51 am

(출처: cycle.js.org)

외부의 큰 박스가 main 함수라면 내부에 있는 작은 박스가 컴포넌트라고 할 수 있을 것이다. 혹은 이런 식으로 특정 컴포넌트가 다른 컴포넌트를 포함할 수도 있다. 이렇게 외부 컴포넌트(혹은 main)로 들어온 스트림에서 내부 컴포넌트에 필요한 부분만 분리해서 넘겨주고, 내부 컴포넌트의 출력으로 나온 스트림을 외부 컴포넌트의 최종 출력 스트림과 조합해서 반환하면 된다. 이 때 내부 컴포넌트는 이벤트 외에 Model의 데이터도 함께 스트림으로 받아야 할 것이고, 처리된 데이터도 VNode 트리와 함께 스트림으로 반환해야 할 것이다.

screen shot 2016-09-27 at 10 27 37 am

(출처: cycle.js.org)

위의 그림을 보면 컴포넌트가 유저 이벤트 스트림 외에 필요한 데이터를 props$ 스트림으로 입력 받고, 출력 값으로 vtree$ 스트림 뿐만 아니라 처리된 값에 대한 value$ 스트림을 함께 내보내는 것을 확인할 수 있을 것이다.

이렇게 컴포넌트를 만들 때 전체 DOM 영역이 아닌 컴포넌트에 필요한 DOM 영역만 스코프를 한정하기 위해서는 일일이 특정한 클래스명을 지정하는 등의 처리를 해 주어야 하는데, 이러한 작업을 돕기 위해 Cycle.js 에서는 isolate() 함수를 제공한다.

const ComponentA = isolate(MyComponent, 'comp-a');
const ComponentB = isolate(MyComponent, 'comp-b');

MyComponent는 main 함수와 같이 입력 스트림을 받아서 출력 스트림을 반환하는 순수 함수이다. 위와 같이 isolate를 사용하면 MyComponent를 각각 comp-acomp-b 클래스 내부로 스코프를 한정하는 두 개의 독립된 슬라이더로 만들어서 사용할 수 있다. 또한 두 번째 인자를 사용하지 않으면 내부적으로 랜덤한 클래스명을 할당해 주기 때문에, CSS에 영향을 받는 경우가 아니라면 굳이 클래스명을 명시적으로 지정할 필요도 없이 사용할 수 있다.

그림으로 보면 개념적으로는 단순하지만, 실제로 스트림을 나누고 합치는 과정은 RxJS 등을 이용한 FRP에 익숙하지 않다면 이해하기가 어렵다. 여기서 이러한 내용을 모두 다루기엔 너무 길어질 것 같으니 자세한 내용은 Cycle의 컴포넌트 설명 문서를 참고하기 바란다.

테스트

Cycle.js의 어플리케이션은 대부분이 순수 함수로 만들어지기 때문에 테스트하기가 굉장히 쉽다. 객체를 생성해서 상태를 관리할 필요가 없고, 함수별로 입력/출력에 대한 테스트만 작성하면 된다.

다만 이벤트 스트림에 대한 의존도가 굉장히 높고, 이들 스트림을 합치거나 분리하는 작업이 많은데 이러한 작업은 테스트를 작성하기가 간단하지만은 않으며, 해당 스트림 라이브러리가 테스트를 지원하는 방식의 영향을 많이 받는다. 예를 들면 RxJS 5부터는 Marble Test를 지원하고, xstream 에서는 fromDiagram 함수를 제공하는데, 이러한 기능을 사용하면 스트림에 대한 테스트를 다음과 같이 Marble Diagram 형태로 작성할 수 있다.

var e1 = hot('----a--^--b-------c--|');
var e2 = hot(  '---d-^--e---------f-----|');
var expected =      '---(be)----c-f-----|';

expectObservable(e1.merge(e2)).toBe(expected);

이처럼 Cycle.js에서는 이벤트 스트림 자체를 다루는 부분과 실제 데이터를 다루는 부분을 분리해서 프로그램을 작성해야 좀더 테스트하기 쉬운 코드를 만들어낼 수 있을 것이다.

또한 DOM과 같은 외부 환경과 관련된 부작용은 모두 드라이버 내부에서 다루어지기 때문에 외부 환경을 모킹(Mocking)하여 테스트를 작성하기도 용이하다. 단 해당 드라이버가 모킹을 도와주는 API를 제공해 주어야 하는데, 예를 들어 DOM Driver 에서는 mockDOMSource 함수를 제공해서 DOMSource를 모킹할 수 있도록 해 준다. 이를 활용하면 다음과 같이 테스트를 작성할 수 있다.

const eventDummy = {
  target: {
    parentNode: {
      dataset: {
        id: 5
      }
    }
  }
}

it('removeSong: ', function() {
  const domSource = mockDOMSource({
    '.btn-remove': {
      'click': Observable.of(eventDummy)
    }
  })

  removeSong(domSource).subscribe(id => {
    expect(id).toBe(5)
  })
})

removeSong 함수는 Intent 내부에서 삭제 버튼을 클릭했을 때 해당 버튼과 관련된 ID를 반환하는 스트림을 만들어서 반환해주는 함수이다. 위와 같이 mockDOMSource를 사용하면 .btn-remove의 click에 대한 이벤트 스트림을 직접 만들어 설정할 수 있으며, 이렇게 만들어진 domSource를 사용해서 removeSong을 호출하면 새로운 스트림을 반환하게 되고 이 스트림을 subscribe 하여 테스트 코드를 작성할 수 있다.

성능

Cycle.js 모든 것을 스트림으로 처리한다는 특징 때문에 다른 라이브러리에 비해 약간의 오버헤드가 있으며, 스트림 처리를 위해 어떤 라이브러리를 사용하느냐에 따라 성능에 영향을 많이 받는다. 사실 버전 7.0.0 이전에는 RxJS 자체의 성능 문제와 더불어 느리다는 비판이 꽤 있었다. 하지만 버전 7.0.0 부터 xstream과 Snabbdom 기반으로 전체 코드 베이스가 변경되면서 많은 성능 향상이 있었다. 실제로 최근에 자바스크립트 프레임워크의 성능을 비교한 글을 보면 몇몇 테스트에서 React 보다도 빠른 성능을 보여주는 부분도 꽤 많다. 특히 메모리 사용량에 있어서 좋은 결과를 보이고 있는데, 함수형 특징으로 인해 불필요한 인스턴스화가 많이 없기 때문(특히 View가 순수 함수이므로)이라고 유추할 수 있을 것 같지만, 명확한 이유는 사실 좀 더 살펴봐야 알 수 있을 것 같다.

Virtual-DOM을 사용한다는 특징 또한 성능에 영향을 미치는데, 이벤트가 발생할 때마다 전체 VNode가 변경되는 구조상 특정한 경우에 성능이 많이 느려질 수 있다. 이러한 경우에는 Snabbdom의 Thunk 함수를 활용하여 VNode를 캐싱하여 사용한다면 성능을 개선시킬 수 있을 것이다.

정리

사실 Cycle.js는 어렵다. 스트림 기반의 반응형 프로그래밍에 익숙하지 않다면 제대로 사용할 수가 없는데, 이러한 함수 반응형 프로그래밍(FRP) 방식은 이해하기가 쉽지 않아서 많은 공부와 연습을 필요로 한다. 하지만 한번 이러한 방식을 잘 이해하고 나면 비동기 방식의 코드를 다루기가 아주 용이하며, 최근에 곳곳에서 관심이 커지고 있는 만큼 공부할만한 가치는 충분하다고 생각한다.

또한 Cycle.js는 잘 설계된 아키텍처를 갖고 있다. André Staltz와 Cycle.js의 컨트리뷰터들은 일관된 철학을 갖고 몇 년 동안 꾸준히 설계를 발전시켜 왔으며, Elm이나 Haskell 등의 함수형 언어가 가진 여러 가지 장점들을 공유하고 있다. 특히 순수함수와 부작용을 확실하게 구분하여 다룰 수 있기 때문에, 어플리케이션의 상태를 좀 더 단순하고 명확하게 관리할 수 있으며 테스트하기 쉬운 코드를 만들어낼 수 있다.

Cycle.js의 홈페이지에 가 보면 이러한 설계 철학에 대해 자세한 설명을 볼 수 있으며, 무료 동영상 강의도 시청할 수 있다. 이렇게 정말 공들여서 관리되고 있는 것에 비해 여전히 사용자층이 많지 않은 것은 아쉽지만, 만약 함수 반응형 프로그래밍이나 ReactiveX 등에 관심이 있다면 꼭 한번 사용해 보길 권한다.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s