(연재) m() – 가상노드와 컴포넌트 – Mithril

가상노드

Mithril은 기본적으로 가상 DOM 노드를 생성하고 이를 실제 웹페이지 내의 DOM에 마운트하거나, 특정 DOM내부에 가상 노드를 렌더링하는 일을 수행하는 것으로 UI를 구성한다. m()은 가상 노드를 표현하는 함수이다. 기본적인 모양은 다음과 같다.

m( selector, attributes, children )
  • selector : 특정 노드를 생성하기 위한 태그명 혹은 CSS 셀렉터이다. 클래스나 아이디를 사용하면 기본적으로 div 태그로 표현된다.
  • attributes : 키-값쌍으로 정의되는 객체이며, 해당 노드의 속성을 지정할 수 있다.
  • children : 해당 노드의 자식노드가 된다. 자식노드에 들어올 수 있는 것은 다음의 타입이 있다.
    • 텍스트 값 : 해당 노드의 텍스트 노드가 된다.
    • m()으로 생성한 단일 가상노드 : 해당 노드의 직속 하위 DOM 노드
    • 가상노드의 배열 : 여러 DOM 노드가 하위에 들어가게 된다.  하위 노드에 m() 대신 문자열이 들어가면 div 객체로 대체된다.

다음은 라이브 스크립트에서 m()을 사용해서 DOM을 표현하는 몇 가지 예이다.

#livescript


m \div {id: \box} \hello
# 가장 표준적인 폼
# <div id="box">hello</div>

m \h2.title \hello
# CSS셀렉터를 이용해서 id, class 속성을 넣을 수 있다.
# <h2 class="title">hello</h2>

m 'input[type=checkbox]' {checked: on}
# 체크된 체크 박스

# ul > li * 3 의 목록
m \ul, 
* m \li \hello
  m \li \world
  m \li \foobar

이렇게 만들어진 가상노드는 m.render() 함수를 통해서 특정 DOM 내에 렌더링할 수 있다. (m.mount()를 사용하기 위해서는 단순 가상 노드가 아닌 컴포넌트가 만들어져야 한다.)

m.render document.body, do
  m \.wrapper, m \ul, [1, 2, 3].map -> m \li it

컴포넌트

컴포넌트는 가상노드와 로직을 하나로 감싼 덩어리이다. 컴포넌트는 실제로 동작하는 앱을 구축하기 위한 가장 기본이 되는 단위이기도 하다. 유효한 컴포넌트는 view()라는 메소드를 가지고 있는 어떠한 객체도 될 수 있다. 컴포넌트를 가상 노드로 변환하기 위해서는 m() 함수에 인자로 전달하면 된다.

다음은 공식 문서에 소개되는 간단한 앱의 원형이다. m() 함수로 특정 컴포넌트를 전달할 때, 해당 컴포넌트의 속성값과 자식 노드를 추가 인자로 전달할 수 있다.  컴포넌트의 view() 함수는 vnode 라는 단일 인자를 받는데, m 함수에 전달되는 추가 인자는 각각 이 vnode의 attrs, children 이 된다.

Greeter = do
  view: (vnode) ->
    m \div, vnode.attrs, [\hello, vnode.children]

m.render document.body,
  m Greeter, {style: {color:\red}} \world
# <div style="color:red;">helloworld</div> 로 렌더링된다.

컴포넌트의 라이프사이클 메소드

각 컴포넌트는 view외에도 여러 라이프사이클 메소드를 갖는다. 이 메소드들은 가상 노드가 생성되거나 변경될 때 각각의 이벤트 핸들러처럼 호출된다.

메소드 설명
oninit(vnode) vnode가 DOM 트리에 렌더링 되기 직전에 호출된다.
oncreate(vnode) vnode가 DOM 트리에 추가될 때 호출된다.
onupdate(vnode) 마운트된 가상노드가 다시 그려지기 전에 호출된다.
onbeforeremove(vnode) 가상노드가 제거되기 직전 호출된다.
onrevmoe(vnode) 가상노드가 제거되기 전에 호출된다.
onbeforeupdate(vnode, old) onupdate() 호출 전에 호출된다. 만약 이 메소드가 false를 리턴하면 다시 그리기가 수행되지 않는다.

가상노드의 상태

Mithril은 1.0으로 업그레이드되면서 가상노드에서 컨트롤러 레벨을 완전히 분리했다. 따라서 데이터 모델은 컴포넌트 속으로 넣기보다는 가급적 외부 객체를 사용하는 것이 권장된다. 이전버전에서는 view 함수가 controller를 인자로 받아서 컨트롤러 내의 상태값을 참조하거나 메소드를 호출했는데, 이 패턴을 그대로 가져오려면 oninit에서도 vnode인자를 받고, 이 vnode가 그대로 view()에 전달된다는 점을 이용해서 다음과 같이 만들 수 있다.

## v.0.2.x
## 컨트롤러를 이용해서 app 컴포넌트 내에
## 상태를 저장하는 list라는 프로퍼티를 정의한다.
app = do
  controller: !->
    @list = []
  view: (ctrl) ->
    m \ul, ctrl.list.map -> m \li it.value

## v.1.0+
## oninit()을 통해서 vnode의 state에 리스트를 정의했다.
app = do
  oninit: (vnode) !->
    vnode.state.list = []
  view: (vnode) ->
    m \ul, vnode.list.map -> m \li it.value

## recommended
## 가장 권장되는 방식은 컴포넌트와 데이터모델을 완전히 분리하는 것이다.
Data = { list: [] }

app = do
  view: (vnode) ->
    m \ul, Data.list.map -> m \li it.value

다만, vnode.state를 과도하게 쓰는 것은 그리 권장되지 않는다. 특히 메소드를 정의하는 경우 this의 처리가 매우 곤란해지기 때문이다. 아예 외부 객체로 정의하는 것이 가장 정신건강에 이롭다.

data = do
  list: []
  desc: ''
  add: !~>
    if @desc.length > 0 then
      @list.push new Item @desc
      @desc = ''
## add 메소드는 바인드되어 있으므로 외부에서 이벤트핸들러로 바로 전달가능하다.

관련 글 목차

  1.  m() – 가상노드와 컴포넌트
  2.  m.render() – 가상 DOM 렌더링하기
  3. m.mount – 가상 노드를 마운트하기
  4. m.prop() – 양방향 바인딩을 위한 데이터 래퍼 – deprecated
  5. m.withAttr() 이벤트 핸들러 처리
  6. m.component – 가상노드를 컴포넌트로 사용하기
  7. Todo 앱 예제
  8. m.route() – 단일페이지 애플리케이션 및 라우팅 규칙
  9. m.request – 서버와의 통신

  1. 물론 mithril은 내부적으로 캐시를 사용하며, 변경된 부분만 새로 그리는 등의 성능 향상을 위한 내부적인 장치들을 가지고 있다.