(연재) m.route – 단일페이지 애플리케이션을 위한 라우팅 – Mithril

mithril은 단일 페이지 애플리케이션(Single Page Application, SPA)을 만들 수 있게 해주는 시스템으로 개별 페이지에 대한 북마크 및 브라우저의 히스토리 메카니즘을 그대로 사용할 수 있게도 해준다. m.route()는 라우팅 시스템을 총괄하는 함수로 , 현재 페이지에서 사용가능한 URL을 정의하고, 특정 URL로 리다이렉팅하거나 현재 경로를 리턴하는 등의 기능을 수행한다.

  • m.route(rootElement, defaultRoute, routes) – 각 루트를 정의하고 루트의 URL 패턴별 대응하는 앱을 지정한다.
  • m.route(path) – 특정한 경로로 리다이렉트한다. m.route.set(path)으로 변경되었다.
  • m.route() – 현재 경로를 리턴하는 함수 m.route.get()으로 변경

라우팅 규칙 정의

단일 페이지 애플리케이션에서는 단일 페이지를 가리키는 주소를 사용하면서 GET 파라미터에 따라서 해당 페이지가 어떤 모드로 표시될 것인지를 구별하게 된다. m.route() 함수를 사용해서 이러한 라우팅 규칙을 정의할 수 있다. 라우팅 규칙을 정의할 때의 각각의 파라미터는 다음과 같다.

  • rootElement : 현재 페이지의 루트 요소로, URI에 따라서 다른 vnode로 마운트될 DOM 요소를 가리킨다.
  • defaultRoute : 디폴트 루트. 정의되지 않은 경로로 접근하면 이 곳으로 리다이렉트된다.
  • routes : 경로-컴포넌트의 쌍으로 구성되는 JS객체. 각 경로로 접근시 대응하는 컴포넌트가 루트 요소에 마운트된다.

예를 들어 홈, 로그인화면, 대시보드 세 개의 화면으로 구성된 웹 앱을 SPA로 작성한다고 가정해보자. Home, Login, Dashboard는 각각 현재 페이지의 body 요소에 마운트될 수 있는 독립된 컴포넌트로 작성된다.

그러면 스크립트 말미에 다음과 같이 라우팅 규칙을 정의할 수 있다. 참고로 여기서 루트(“/”)은 현재 페이지의 URL을 의미하게 된다.

#livescript

m.route document.body, '/', do
  \/ : Home
  \/login : Login
  \/dashboard : Dashboard

m.route.prefix

단일 페이지에서 어떻게 여러 페이지에 접근하는 효과를 낼 수 있을까? 일단 페이지는 고정되어야 하니 페이지에 대한 URL은 항상 똑같아야 한다. 그렇다면 페이지 주소에 추가적으로 붙을 수 있는 부가 정보를 이용해서 경로를 구분할 수 있다. 여기에 사용되는 부가 정보에는 두 가지가 있을 수 있는데,

  1.  GET 파라미터 : 주소 뒤에 ?으로 시작하여 붙는 인자들
  2. 앵커   : 주소 뒤에 #으로 시작하여 붙는 앵커 식별자

그외에 ‘/’으로 구분하여 경로처럼 사용하는 방법도 있으나, 이는 서버에서 라우팅 규칙을 다시 쓰도록 설정해주어야 하는 번거로움이 있다. 해시를 사용하는 규칙이 기본적으로 쓰이며 (이 방식은 앵커링과 비슷하게 동작하므로 페이지 리로딩이 없다.) 경우에 따라 “?”으로 변경해서 파라미터 기반으로 변경할 수 있다.

 

변수가 포함되는 경로

경로 패턴의 일부분이 변수화될 수 있다. 예를 들어 특정 키에 대한 편집화면에 대한 주소를 /edit/:key 와 같이 설정하면 /edit/hello, /edit/world 와 같은 패턴들은 모두 해당 라우터에 의해 처리된다. “…”을 뒤에 붙이면 그 하위 경로 전체가 슬래시를 포함해서 문자열의 형태로 해당 변수에 할당된다.

이렇게 할당되는 변수들은 해당 라우터의 vnode의 .attrs 속성으로 들어가게 된다. (vnode.attrs.key 와 같이 참조할 수 있다.)

페이지 번호가 바뀌는 것과 같이 같은 경로에서 키값이 변경되는 경우를 생각해보자. 이 경우 해당 컴포넌트가 완전히 처음부터 새롭게 재생성되는것이 아니라 기존 상태에서 변경분만 새로 그려지게 된다. 따라서 oninit, oncreate 훅이 호출되지 않고 대신에 onupdate 훅이 호출된다. 따라서 다음 페이지로 넘어갈 때에는 onbeforeupdate에서 데이터를 받아오고, onupdate에서 다시 세팅해야 한다.

리다이렉팅

1.0으로 버전업하면서 m.route() 함수는 인자에 따라 오버로드되는 것처럼 행동했던 것이 세부 동작이 구분되는 형식으로 변경되었다. m.route.set()을 통해서 링크 강제 이동 혹은 리다이렉트가 수행된다. 현재 페이지에서 키 값만 변경되는 경우에는 다음과 같은 식으로 호출할 수 있다.

m.route.set m.route.get!, {key: 2}

 

 

데이터 프리로딩

보통 이러한 SPA를 만들 때 페이지를 렌더링하기전에 컴포넌트는 추가적으로 서버로부터 데이터를 내려받을 필요가 있다. 데이터를 로딩하는 것은 보통 컴포넌트를 두 번 렌더링하게 한다. (최초 라우팅시 한 번, 그리고 데이터가 다운로드되면 다시 한 번)

state = do
  users: []
  load-users: -> 
    m.request \/api/v1/users .then (users) !-> state.users = users

m.route document.body, \/user/list, do
  \/user/list : do
    oninit: state.load-users
    view: -> 
      if state.users.length > 0 then
        state.users.map -> m \div it.id
      \loading

RouteResolver를 쓰면 다음과 컴포넌트를 렌더링하기 전에 데이터를 프리로딩할 수 있다. 이는 짧은 통신 딜레이에 의한 화면의 깜빡임을 해소할 수 있는 수단이 될 수 있다.

state = ... # 위와 동일

m.route document.body, \/user/list, do
  \/user/list : do
    onmatch: state.load-users
    render: ->
      state.users.map -> m \div it.id

onmatch 훅은 라우터가 렌더링할 컴포넌트를 필요로 할 때 호출되며, 경로가 변경될 때 한 번만 호출된다. 대신 같은 경로인 경우에는 다시 그리기를 하지 않는다. 이 훅은 보통 컴포넌트가 초기화가 필요한 경우에 사용된다.  renderonmatch 특정한 컴포넌트를 리턴하여 렌더링할 수 있다. 만약 onmatch에서 false가 리턴된다면 렌더링은 수행되지 않고 기본 경로로 리다이렉트 된다. 그외의 경우 onmatch에서 리턴한 값은 render의 인자로 넘어가게 된다.


관련 글 목차

  1.  mithril 앱의 기본 구성 및 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와 통신하기