(Javascript | mithril ) m.component – 앱을 컴포넌트화하기

본격 mithril 탐구. 가독성을 위해서 본 문서에서는 LiveScript로 예제를 작성합니다.

mithril 관련 글

  1. mithril 앱의 기본 구성 및 m()
  2. m.render – 가상 DOM 렌더링하기
  3. m.mountmithril을 이용한 양방향 바인딩 & 템플릿 렌더링
  4. m.prop 양방향 바인딩을 위한 데이터 래퍼
  5. m.withAttr 양방향 바인딩을 위한 이벤트 매퍼
  6. * m.componentmithril 앱을 컴포넌트화하기
  7. Todo
  8. m.route – 단일페이지 애플리케이션 및 라우팅 규칙
  9. m.request – 서버 API와 통신하기

m.componentcontroller + view로 이루어진 컴포넌트를 다른 컴포넌트 내에 임베드할 수 있게 하는 기능이다. 페이지마다 반복되거나, 페이지 내에서 반복되는 요소에 대해서 컴포넌트를 작성하여 재사용한다.

컴포넌트는 그 자체로 하나의 인스턴스가 아니라 컴포넌트를 사용할 때 그 사본이 생성되므로, 각각의 컴포넌트 인스턴스 간의 컨트롤러는 이벤트나 데이터가 간섭을 일으키지 않는다.

다음 예는 완전한 구성의 컴포넌트를 복제하여 사용하는 예로, 각각의 컴포넌트는 input 필드 하나와 버튼 하나를 가지고 있는데, 버튼을 클릭하면 해당 필드의 문자열을 대문자로 변경해주는 역할을 한다.

comp-upper = 
    controller = (word) ->
      word: m.prop word || 'sample'
    view: (ctrl) ->
      m \.row
      * m \input do
          onchange: m.with-attr \value, ctrl.word
          value: ctrl.word
        m \button do
          onclick: !~> ctrl.word ctrl.word!to-upper-case!
        , \upper

app = 
    view: (ctrl) ->
      m \.app
      * m \h1, \words
        [1 to 9].map -> m.component comp-upper

m.mount document.body, app

이 코드는 9개의 컴포넌트를 각각 렌더링하게 되는데, 각 컴포넌트는 개별적인 word값과 이벤트 핸들러를 처리한다. 따라서 각각의 버튼을 클릭하면 대응하는 필드의 텍스트만 변경된다.

재밌는 부분은 컴포넌트를 구성하는 프로퍼티들이 사실은 추가적인 인자를 받을 수 있다는 점이다. 1

컴포넌트를 구성하는 controllerview 는 추가적인 인자들을 받을 수 있다.

  1. controller()m.component가 호출될 때 콤포넌트 다음으로 받는 인자들을 넘겨 받는다.
  2. controller()m.component가 호출되는 시점에 단 한 번 호출받으며, 이후에는 호출되지 않는다.
  3. view는 첫번째 인자가 ctrl인 것을 제외하면, controller와 같이 m.component 호출 시 이후의 인자들을 넘겨받을 수 있다. (따라서 컨트롤러 부가 없어도 동적인 뷰 생성이 가능하다)

따라서, controller, view의 인자 처리 부분을 잘 다듬으면, 페이지 전체를 작은 단위의 컴포넌트들로 쪼개고 컴포넌트를 조합하는 것으로 페이지를 구성하는 것도 가능하다. 예를 들어 헤더를 렌더링해주는 컴포넌트, 현재 페이지의 식별자를 가지고 메뉴를 표시해주는 컴포넌트, 화면에 표시될 요소를 제어하는 컴포넌트2, 그리고 데이터를 가공하여 화면에 표시하는 컴포넌트 등으로 분리하는 것도 가능하다.

m.component() 함수는 m.component(컴포넌트, 인자들....) 이런 식으로 호출 하는데, 이는 뷰 단에서 임베드하는 것으로 m(컴포넌트, 인자...)로 축약하는 것도 가능하다.

stateless component

컴포넌트는 내부적으로 저장하는 데이터가 없다면 stateless하다고 말한다. 이는 순수한 함수에 가까우며, 따라서 특정 입력에 대해 동일 출력을 기대할 수 있기에 좀 더 예측이 쉽고 테스트나 디버그가 편리하다. 이러한 상태를 가지지 않는 컴포넌트들은 특정한 함수들만을 가지고 있으며, 내부에 데이터를 저장하지 않는 대신 (따라서 controller부는 저장 프로퍼티를 가지지 않는다.) 변환과 같은 처리를 담당하게 된다.

상태를 가지지 않기 때문에 컴포넌트 자체가 하나의 함수와 같이 행동하게 되므로, controller는 인자를 받지 않고, viewview(ctrl, args...)과 같은 식으로 인자를 받게 된다.

다음은 절대온도 값을 입력 받아 이를 화씨와 섭씨 온도로 변환하는 출력부이다. 변환과 출력 부분을 상태가 없는 컴포넌트로 정의했다.

my-app = 
  controller: -> { temp: m.prop 10 }
  view: (ctrl) ->
    m \div, 
    * m \input do
        onchange: m.with-attr \value, ctrl.temp
        value: ctrl.temp!
      "K",
      m \br
      "result:",
      m.component temp-converter, {value: ctrl.temp!}

temp-converter = 
  controller: -> do
    KtoC: (value) -> value - 273.15
    KtoF: (value) -> (9/5*(value - 273.15)) + 32
  view: (ctrl, arg) ->
    console.log \call
    m \div,
    * "celcius: #{ctrl.KtoC arg.value}"
      m \br,
      "fahrenheit: #{ctrl.KtoF arg.value}"

m.mount document.body, my-app

http://mithril.js.org/mithril.component.html 에 있는 코드를 라이브스크립트로 번역한 코드임.

비동기 컴포넌트

컨트롤러에서는 모델에 대한 메소드를 호출할 수 있기 때문에, 임베드된 컴포넌트가 비동기 동작을 하는 것이 허용된다. 네스팅되지 않은 컴포넌트(루트 앱 등)에 대해서는 미스릴은 비동기 동작을 동기화하기 위해 대기하지만, 네스팅된 컴포넌트에 대해서는 컴포넌트가 비동기 작업을 끝내기 전에 해당 컴포넌트의 부모 뷰를 먼저 렌더링한다. 컴포넌트의 존재는 템플릿이 렌더링 되는 시점에만 알 수 있다.

컴포넌트가 비동기 페이로드를 가지고 있고, 자동 리드로우 시스템의 큐에 들어가 있을 때, 뷰는 비동기 동작이 끝나기 전에는 렌더링되지 않는다. 그리고 이 비동기 작업이 완료되면 새로운 리드로우가 트리거되어 전체 템플릿 트리가 다시 평가된다. 이는 특정한 템플릿이 완전히 렌더링 될 때까지는 최소 두 번에서 그 이상의 redraw가 일어날 수 있다는 말이다.

컴포넌트 A가 컴포넌트 B를 포함하고 있고, 컴포넌트 B가 비동기 서비스와 연동할 때, 컴포넌트 A는 B대신에 대체차(<placeholder>)태그를 먼저 렌더링한 다음, B의 작업이 완료되면 플레이스 홀더를 B의 렌더링 결과물로 대체하게 된다.

주의 사항

임베드된 컴포넌트에서는 m.redraw를 호출할 수 없다. 또한 템플릿 내에서도 m.request를 사용할 수 없다. 이러한 동작들은 리드로우 사이클 중에 다른 리드로우를 생성해내고 이는 무한루프에 빠지게 되는 원인이 된다.

컴포넌트를 사용할 때는 다음 사항에 주의해야 한다.

  1. 임베드된 컴포넌트의 리턴은 반드시 가상 DOM 트리이거나 다른 컴포넌트여야 한다. 문자열, 배열, 숫자값들은 에러를 유발한다.
  2. 임베드된 컴포넌트의 controllerm.redraw.strategy를 변경할 수 없다. 대신 ctx.retain 플래그 값을 조정하는 것이 권장된다.
  3. 컴포넌트의 루트 DOM은 해당 컴포넌트의 라이프 사이클 내에서 변경되어서는 안된다. 예를 들어 return someCondition ? m('a') : m('b') 와 같은 동작을 해서는 안된다.
  4. 컴포넌트의 루트 요소가 첫 렌더링시에 {subtree: 'retain'} 여서는 안된다.

  1. 이는 m.comoponent를 통해서 호출할 때만 추가 인자를 넘겨주는게 가능하다. m.mount의 경우에는 추가 인자를 넘길 수 없으며, m.render의 경우에는 실질적으로 컴포넌트가 아닌 가상 URL을 받기 때문에 view()의 호출결과만을 필요로 하기 때문이다. 
  2. 전역 스코프에 데이터를 노출할 게 아니라면 다른 컴포넌트에 영향을 주는 데이터를 조작하기는 어렵지만, 앱 본체를 참조하는 식으로 제어는 가능할 것이다.