Mithril 1.0의 변경사항

일전에 mithril 관련한 내용을 총정리 겸해서 연재했었는데, 그 사이에 mithril 이 1.0으로 업데이트되었고 많은 내용이 변경됐다. 몇 가지 변경사항을 정리해보면 다음과 같다.

  1. m.prop 이 제거됐다. 코어에서 빠지고 별도의 stream 이라는 마이크로 라이브러리로 옮겨갔다.
  2. m.component() 함수도 제거된다. m() 함수가 vnode를 받아서 vnode를 리턴하는 형태로 변경되었으므로, m()으로 대체된다.
  3. config 함수가 제거됐다. 대신에 vnode마다 oninit, oncreate 등의 훅이 추가되었다.
  4. redraw 매커니즘이 변경됐다. 모든 다시 그리기 과정은 non-blocking으로 돌아가는 사이클이며, 이벤트 처리시에 리드로우를 금지하는 처리가 조금 더 간소화되었다.
  5. 동기식 리드로우는 완전히 제거되었다.
  6. 컴포넌트는 view() 함수만 가지며, controller 함수는 제거됐다. 컴포넌트 초기화는 oninit 훅에서 처리할 수 있다. view() 함수는 controller가 아닌 vnode를 인자로 받게 된다. 따라서 이전에 인자를 추가로 받던 기능은 모두 vnode.attrs를 참조하는 것으로 대체된다.

간단한 todo 앱을 mitrhil 1.0 으로 만드는 과정을 살펴보자. 이전 버전에서라면 하나의 앱 객체가 controller, view 함수를 각각 가지고 데이터 처리와 렌더링을 모두 수행하였다면, 지금은 앱 컴포넌트는 view 기능만을 가지게 되며, controller의 역할을 하는 객체를 따로 만들거나하는 식으로 뷰 외부로 빼내야 한다.

아이템 정의

각각의 할일을 나타내는 Task 클래스를 작성해보자. 각 할일은 이름과 완료 여부를 나타내는 값으로 구성한다. 이전 버전에서 사용되는 m.prop()이 없기 때문에 어떻게 양방향 바인딩이 구성될까 싶은데, Mithril 1.0부터는 사용자에 의한 UI 액션에 의해 데이터가 변경되면 자동으로 redraw가 이뤄지고, redraw에 따르는 부하를 최소한으로 줄이는 설계가 적용되어 있다.

참고로 코드는 LiveScript로 작성되었다.

class Task
  (@title) ->
    @done = no

컨트롤러

각각의 미스릴 앱 컴포넌트는 더 이상 비즈니스 로직을 내부에 담는 것을 지향하지 않는다. 따라서 데이터 컨트롤러 자체는 미스릴 범위 바깥의 순수한 자바스크립트 객체로 만들어진다.

controller = 
  desc : ''
  list : []
  add: !->
    if @desc != '' then
      t = new Task @desc
      @list.push t
      @desc = ''

컨트롤러의 경우 어떤 미스릴 관련 코드도 포함하지 않는다는 것을 볼 수 있다. 자 이제 본격적으로 뷰를 만드는 방법을 살펴보자.

뷰 Part 1 – 입력 컴포넌트

첫번째로 할일의 내용을 입력받고, 추가 버튼을 누를 수 있게해주는 입력 컴포넌트를 만드는 방법을 살펴보자. 이 컴포넌트는 텍스트 필드 하나와 버튼 하나를 가지게 된다. 텍스트 필드는 키가 눌려질 때마다 controller의 desc 속성을 변경하게 되고, add 버튼을 클릭하면 controller.add를 호출할 것이다.

comp-inputs = 
  view: (vnodes) ->
    m \.inputs,
    * m \input, do # 입력 필드
        onkeyup: m.with-attr \value (e)!-> controller.desc = e
        value: controller.desc
      # 다음은 버튼
      m \button, do
        onclick: controller.add.bind controller
      , \ADD

m.prop() 함수가 명시적으로 없어졌기 때문에, 입력 필드에서 키가 입력되면 조금 불편하지만, controller.desc값을 직접 제어하는 함수를 입력해야하는 불편함이 있을 수 있다. 다만 이 코드는 가독성 측면에서는 더 나은 형식을 보여주므로 나빠졌다고만은 할 수 없다.

m.prop()은 여전히 필요할 수 있는데, UI 상의 어떤 사용자 입력이나 액션 없이 프로그램적인 값의 변경을 감지하여 UI 갱신을 푸시해야 하는 경우에는 필요할 수 있다. 이를 위해서 미스릴에서는 이 기능이 완전히 제거된 것이 아니라, 코어로부터 분리되어 stream이라는 라이브러리로 독립되었다. 필요한 경우 해당 라이브러리를 프로젝트에 포함하여 사용하는 것도 여전히 가능하다.

뷰 Part 2 – 리스터

이제 각 할일의 목록을 표시하는 부분을 만들어보자. 각각의 할일은 하나의 리스트 아이템이며, 체크 박스 하나와 텍스트 표시요소 (span 등)로 이루어진다. 하나의 리스팅 항목을 컴포넌트로 정의하면 다음과 같다.

comp-lister = 
  view: (vnode) ->
    # {item: * } 의 형태로 데이터를 전달받을 수 있다.
    item = vnode.attrs.item
    m \li.row,
    * m 'input[type=checkbox], do
         onclick: m.with-attr \checked (v) !-> item.done = v
         checked: item.done
      m \span, do
        style:
          text-decoration: if item.done then 'line-through' else 'none'
      , item.desc

최종 조립

이제 끝으로 모든 컴포넌트를 조립하고 앱으로 실행할 수 있게 한다.

app = 
  view: (vnode) ->
    m \#app,
    * m comp-inputs
      m \ul.lister, controller.list.map (item) ->
        m comp-lister {item: item}

m.mount document.body, app

Mithril 을 사용한 앱을 구성하는 컴포넌트와 view 함수 구성 방식에서 변화가 있어서 비록 기존코드가 다 깨지는 정도의 큰 변경이 있었지만, 그 변화의 맥락이 충분히 합당하다고 생각되기에 이 변경을 따라가는 것 자체가 크게 어렵지는 않았다.

다만 버전이 올라가면서 Mithril을 설치하는 것이 개인적으로 별로 마음에 들지 않는 자바스크립트 패키지 빌더를 사용해야 한다는 점이 마음에 걸릴 뿐이다.