(연재) m.render – 가상 DOM 렌더링하기 – Mithril

m.render vs m.mount

미스릴은 virtual dom node를 생성하고, 이 가상 노드를 문서내의 특정 요소에 렌더링하거나 마운트한다. 가상 노드를 생성하는 컴포넌트를 작성하고, 해당 컴포넌트의 내용을 화면에 표시하기 위해서는 이미 존재하는 HTML 문서 내의 특정 DOM의 내부를 가상 노드로 채워주게 된다. 이 때 사용하는 함수로 m.render()m.mount()가 있다. m.render()는 1회성으로 가상 노드를 렌더링하며, m.mount()는 특정 DOM에 가상 노드 컴포넌트를 마운트하는데, mithril은 마운트된 DOM 내에서 발생하는 이벤트를 체크하여 내부적으로 m.render()를 다시 호출해서 이벤트에 의해서 변경된 데이터가 UI에 반영될 수 있도록 자동으로 다시 그리기를 수행한다. 

다음 코드에서 links는 두 개의 링크 정보(연결주소와 타이틀)를 담은 객체 배열인데, 이 정보를 리스트로 렌더링하는 예를 보여준다. (이들 데이터가 고정적이라고 한다면 마운트하는 것보다 m.render()를 써서 그리기만 하는 것이 훨씬 가벼울 것이다.)

See the Pen simply render lists. by Stripe_Q (@stripe-q) on CodePen.0

하지만 실질적으로 m.render()는 2회 이상 호출이 가능하다. m.redner()는 미스릴의 UI 렌더링 기능을 모두 포함하고 있고, 마운트 되지 않은 가상노드라하더라도 이미 그려진 상태와 그릴 상태를 비교해서 지능적으로 다시 그리기 성능을 극대화한다.

관련 글 목차

  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() – 서버 API와 통신하기

 

(Javascript | mithril ) m.mount – 양방향 바인딩 & 템플릿 렌더링

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와 통신하기

마운팅은 mithril 내 앱 컴포넌트가 생성한 결과물(가상 DOM)을 HTML DOM내에 렌더링하는 작업을 의미한다. m.renderm()으로 생성한 가상 DOM을 한 번 처리하여 렌더링하는데 있지만, m.mount는 내부에 controllerview를 모두 가지고 있는 앱 컴포넌트를 렌더링하는 것이며, 앱이 UI에 대한 이벤트를 걸어두었을 때 이벤트가 발생하는 것에 따라 모델의 요소값이 변경되면 뷰를 자동으로 redraw한다는데 있다.

Object mount(DOMElement rootElement, Component component)

where
    Component :: Object{ Controller, View }
    Controller :: SimpleController | UnloadableController
        SimpleController :: void controller([Object attributes [, any, ..args]])
        UnloadbleController :: void controller ([Object attributes [, any ...args]]) { prototype: void unload(UnloadEvent e)}
            UnloadEvent :: Object{ void preventDefault() }
    View :: void view(Object controllerInstance [, Object attributes [, any ...args]])

컴포넌트에 대한 보다 자세한 내용은 다음 글을 찾아볼 것. :: m.componentmithril 앱을 컴포넌트 화하기

(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로 이루어진 컴포넌트를 다른 컴포넌트 내에 임베드할 수 있게 하는 기능이다. 페이지마다 반복되거나, 페이지 내에서 반복되는 요소에 대해서 컴포넌트를 작성하여 재사용한다.

컴포넌트는 그 자체로 하나의 인스턴스가 아니라 컴포넌트를 사용할 때 그 사본이 생성되므로, 각각의 컴포넌트 인스턴스 간의 컨트롤러는 이벤트나 데이터가 간섭을 일으키지 않는다.
(Javascript | mithril ) m.component – 앱을 컴포넌트화하기 더보기

스도쿠 문제 풀이 작성하기 (LiveScript)

스도쿠 문제를 풀어주는 프로그램을 만들어보자. 스도쿠 풀이 방법은 여러 접근법이 있지만, 여기서는 가장 간단하게 임의의 한 빈칸에서 출발해서 ‘쓸 수 있는 숫자 후보’들을 하나씩 넣으면서 다음 빈칸으로 이동하는 식으로 진행한다. 여기서 사용할 언어는 Livescript이다.

노드

스도쿠 문제풀이에 사용하고자 하는 방식은 일종의 깊이 우선 탐색이다. 각 칸에서 사용가능한 후보 숫자 중 하나를 적용하고 다른 칸으로 이동한다. 이 때 적용할 수 있는 후보 숫자가 없으면 어딘가 잘못된 것이므로 이전 노드에서 다른 후보 값을 사용하고 재탐색한다.

또한 노드는 1)처음부터 값이 주어진 칸, 2)공란, 3)문제를 푸는 과정에서 값을 쓴 칸으로 구분하게 되는데 이는 ‘색’이라는 속성으로 사용하도록 하겠다. 또 한가지 중요한 것은 노드는 자신의 위치(인덱스)도 저장해야 한다는 점이다. 이를 바탕으로 각 칸을 표현할 노드 클래스를 아래와 같이 정의한다.

class Node
    (@value, @order) ->
        @color = if @value == 0 then \white else \black

유틸리티 함수

스도쿠 문제를 푸는 과정에서의 핵심은 특정한 칸의 위치에서 사용할 수 있는 후보 숫자들을 뽑아내는 것인데 이는 1~9의 숫자에서 다음에 해당되는 숫자를 뺀 나머지가 된다.

  • 같은 세로 줄에 있는 숫자
  • 같은 가로 줄에 있는 숫자
  • 같은 3X3 격자에 있는 숫자

따라서 다음과 같은 유틸리티 함수를 만들어야 한다.

  • 인덱스로 좌표(x, y)를 산출하는 함수
  • 인덱스로 zone 값을 구하는 함수
  • 특정 zone 내의 칸의 인덱스들을 구하는 함수
  • 특정 인덱스와 같은 열의 인덱스를 구하는 함수
  • 특정 인덱스와 같은 행의 인덱스를 구하는 함수

인덱스를 좌표로 변환

인덱스를 좌표로 변환하는 것은 간단하다. 스도쿠는 9×9 영역에서 이루어지는 게임이므로 인덱스값 i를 9로 나눈 몫이 행이되고, 9로 나눈 나머지가 열이 된다.

i2c = (i) -> [i % 9, Math.floor i / 9]

인덱스를 zone으로 변환

인덱스를 좌표로 변환하고 나면, zone을 구하는 것도 간단하다. zone은 다음과 같이 위치한다고 할 때,

  0   1   2
  3   4   5
  6   7   8

y 값을 3으로 나눈 몫이 행의 위치가 되고, x 값을 3으로 나눈 나머지가 열의 위치가 된다. 따라서 아래와 같은 코드로 zone의 번호를 구할 수 있다.

zone = (i) -> 
  [x, y] = i2c i
  (Math.floor y / 3) * 3 + (Math.floor x / 3)

인덱스와 동일한 zone에 속한 인덱스 구하기

반대로 zone의 번호를 가지고 해당 zone의 모든 인덱스를 구하는 방법을 생각해보자. 이는 각 zone의 가장 앞 인덱스를 구할 수 있다면 그 위치로부터 가로, 세로로 3개의 9개 인덱스 값을 얻으면 된다.

zone의 첫 칸의 위치는 zone 번호를 z라 할 때 다음과 같이 계산된다.

  • y : zone의 행 * 27
  • x : zone의 열 * 3

따라서 i 와 같은 zone에 위치한 인덱스들은 다음과 같이 계산한다.

in-zone = (i) ->
    z = zone i
    zi = (z % 3) * 3 + (Math.floor z / 3) * 27
    [za + m * 9 for m in [0 til 2]].map ->
        [it + n for n in [0 til 2]]
    .reduce (++), []

차집합 계산

그리고 후보숫자를 찾기 위해서 두 배열의 차집합을 구하는 함수를 하나 작성한다.

sub = (xs, ys) -> [x for x in xs when x not in ys]

이제, 필요한 유틸리티 함수들은 준비되었으니, 스도쿠 게임을 풀어보자.

스도쿠 게임 클래스

스도쿠 게임 클래스를 작성할 차례이다. 이 클래스는 크게 세가지 기능을 갖는데,

  1. 주어진 일련의 문자열로부터 81개의 숫자를 읽어들여서 스도쿠 판을 구성한다. 이 판은 Node들의 배열이다.
  2. 스도쿠 문제를 푼다.
  3. 결과를 출력한다.

그리고 문제를 푸는 과정에서 특정 위치에서 가용한 후보 숫자를 뽑아내는 메소드도 하나 있어야 한다.

입력값 파싱하여 스도쿠판 생성

먼저 주어진 문자열 데이터로 스도쿠 판을 구성하는 부분을 살펴보자.

class Sudoku
    (str-data) ->
        @data = str-data.split '' .map (c, i) ->
            new Node (parseInt c), i

특정 위치의 가용한 값 찾기

특정 위치 i에서 가용한 숫자를 찾는 메소드를 작성한다. 가용한 숫자는 아래 세 개 집합을 먼저 구해야 한다.

  • ar : i와 동일한 행에 있는 값들
  • ac : i와 동일한 열에 있는 값들
  • az : i와 동일한 zone에 있는 숫자들

이 세 배열을 합친 후 1~9에서 빼면 가용한 숫자가 나오게 된다. 이미 유틸리티 함수들을 작성해두었으므로 이 부분도 간단하다.

    avaiables: (i) ->
        [x, y] = i2c i
        ar = [0 til 9].map (e) ~> @data[9 * y + e].value
        ac = [0 til 9].map (e) ~> @data[9 * e + x].value
        az = in-zone i .map (e) ~> @data[e].value
        sub [1 to 9], ar++ac++az

문제 풀기

최종적으로 게임을 푸는 로직이다. 이는 재귀적으로 동작하는데

  1. 임의의 흰색 칸을 고른다. 만약 흰 칸이 남아있지 않다면 게임을 완료한 것이므로 true를 리턴한다.
  2. 만약 가용한 숫자가 없으면 false를 리턴한다.
  3. 가용한 숫자가 있다면 그 중 하나를 현재 칸에 쓰고 현재 칸의 색을 회색으로 바꾼다.
  4. 그리고 다음 흰 칸으로 넘어간다.
  5. 다음 칸이 false를 리턴했다면 다음 가용숫자를 사용해본다. 남은 가용숫자가 없다면 false를 리턴한다.

이런 식으로 돌아간다. 따라서 최종적으로 true가 리턴되면 게임이 풀린 것이고, false가 리턴된다면 문제가 잘못되어 풀 수 없다는 뜻이다.

    run: ->
        nv = @data.filter (.color == \white)
        if nv.length == 0 then return yes
        cv = nv[0]
        for a in @avaiables cv.order
            cv <<< color:\gray, value:a
            if @run! then return yes
        cv <<< color:\white, value:0
        no

결과 출력

최종적으로 결과를 9×9 격자모양으로 생성하는 메소드를 하나 추가한다. 한 행씩 구분자를 이용해서 문자열로 만든다음, 각 행을 역시 구분자로 엮어주면 된다.

    description: ->
        [0 til 9].map (r) ~>
            [0 til 9].map (c) ~>
                @data[c + r * 9].value.to-string!
            .join '|'
        .join '\n' + '-' * 17 + '\n'

테스트

오일러프로젝트에서 가져온 예시 데이터를 이용해서 한 번 실행해보도록 하자.

a = new Sudoku \003020600900305001001806400008102900700000008006708200002609500800203009005010300 
a.run!
a.description! |> console.log

결과는 다음과 같다.

4|8|3|9|2|1|6|5|7
-----------------
9|6|7|3|4|5|8|2|1
-----------------
2|5|1|8|7|6|4|9|3
-----------------
5|4|8|1|3|2|9|7|6
-----------------
7|2|9|5|6|4|1|3|8
-----------------
1|3|6|7|9|8|2|4|5
-----------------
3|7|2|6|8|9|5|1|4
-----------------
8|1|4|2|5|3|7|6|9
-----------------
6|9|5|4|1|7|3|8|2

개선점

아무래도 첫 시작 부분에서 영 좋지 못한 선택을 하면 dead end를 확인하는데 까지 많은 비용이 소요된다. 그리고 스도쿠는 올바른 값을 채워나갈수록 문제가 점점 더 단순해지므로 다음 방법을 적용하면 문제를 푸는 속도를 개선할 수 있다.

  1. 문제가 항상 풀린다는 가정하에, 최초 주어진 값을 기준으로 가용한 숫자가 1개 밖에 없다면 이 노드들도 해당 값을 적용해서 검은칸으로 만든다. 그리고 새로 추가된 칸의 숫자 덕분에 답이 결정되는 다른 칸들이 나올 수 있다. 이렇게 제약조건을 계속 전파해나가면 빈칸의 수를 줄여서 속도가 개선된다.
  2. 흰색 칸 중에서 가용한 숫자의 개수가 적은 쪽을 먼저 선택하면 dead end든 올바른 정답이든 만날 수 있는 확룰이 커진다.

전체 소스코드는 다음과 같다.

LiveScript + NodeJS

back call

livescript는 자바스크립트로 컴파일되는 스크립트 언어로, 그 문법적 특징이 하스켈
의 것을 많이 참고하고 있다. 예를 들면 괄호없이 함수를 호출한다거나, 함수 파라미
터간의 컴마를 생략할 수 있다거나 하는 것들이 있다.

한 때 흥미삼아서 조금 만져보다가 관뒀었는데, 갑자기 모 사이트에서 NodeJS 튜토리얼을 몇 개 따라해보려다가1 생각이나서 그 사이트의 튜토리얼 코드들을 라이브스크립트로 코딩해봤더니 이거 썩 괜찮더라.

특히 화살표(->)를 이용한 익명함수 정의 문법2을 이용해서 함수를 인자로 전달하거나 함수가 함수를 리턴하는 것을 좀 더 간단하게 표기할 수 있게 한다.

예를 들어 이벤트핸들러를 등록하는 코드를 생각해보자.

var xhr = new XMLHttpRequest();
...
xhr.addEventListenser('onload', function(complete){
    console.log(complete.target.responseText);
});

LiveScript + NodeJS 더보기