(연재) m.request – 서버와의 통신 – Mithril

서버와의 통신

SPA를 만드는 과정에서 필수적인 기능 중 하나는 서버와의 통신이다. 대부분의 웹앱은 어떤 식으로든 1)서버로부터 데이터를 내려 받아 표시하고, 2)사용자가 작성/변경한 내용을 서버로 전송하거나 3)경우에 따라서는 파일 업로드도 지원해야 한다. 서버와의 통신을 위해서는 form을 쓰거나, XMLHttpRequst 같은 걸 사용해서 구현하는 방법이 있다. Mithril은 기본적으로 XMLHttpRequest를 사용해서 서버와 비동기 통신하는 것을 간단하게 처리할 수 있는 API를 제공한다.

기본적인 GET 요청

m.request() 함수가 서버 통신을 처리하는 메인 플레이어가 된다.  이 함수는 요청을 전달할 URL이나 객체형식의 옵션 정보를 인자로 받은 후, 서버 응답을 감싸고 있는 Promise 객체를 리턴한다.

Promise 객체라고 해서 긴장할 필요는 없고, .then() 메소드를 사용해서 콜백을 받고, 완료 시에 콜백에 래핑된 데이터를 넣어 호출해주는 타입을 생각하면 된다.

m.request()는 기본적으로 서버 응답이 JSON 형식으로 돌아오는 것을 가정한다. JSON 형식의 데이터는 자동으로 내부에서 JSON.parse를 사용해서 자바 스크립트 객체로 변환되고 이를 감싼 Promise로 변경된다. 0.2.x 버전에서는 자동으로 prop()으로 감싸졌는데, 더 이상 prop으로는 감싸지지 않는다. 아래 예제는 특정 주소로 GET 요청을 보내고, 응답으로 돌아온 JSON 데이터를 파싱하여 출력하는 예를 보여준다.

m.request do
  method: \GET
  url: \/api/v1/users
.then (users) -> console.log users

인자로 넘겨지는 부분에서 여러가지 옵션을 줄 수 있다.

옵션 키 필수여부 설명
url Yes 요청을 보낼 서버의 주소
method HTTP 메소드 타입. 기본적으로 GET으로 간주한다.
data 서버로 전송될 데이터. GET타입인 경우 쿼리 스트링으로 변환되어 URL에 추가되며, POST인 경우 페이로드로 업로드된다.
async 기본값은 true로 요청이 비동기로 전송되는 것을 의미한다.
user 인증을 위한 사용자 계정 필드
password 인증을 위한 패스워드 필드
withCredentials 제3자 도메인으로 쿠키를 전송할 것인지를 결정한다. 기본적으로 false
config xhr 객체를 받는 함수를 세팅할 수 있다. 흔히 프로그레스 바를 구현할 때 쓴다.
headers 헤더 항목에 임의의 키-값쌍을 전송할 수 있다.
type 특정한 생성자를 넘겨주면 리턴된 데이터를 해당 타입으로 만들어준다.
serialize 데이터를 직렬화하는 방법. 기본적으로 JSON.stringify를 사용한다.
deserialize 응답페이로드를 역직렬화하는 방법. 기본적으로 JSON.parse를 사용한다. 만약 파일을 받은 경우라면 항등 함수를 넣어줄 것
extract xhr, option을 받는 함수를 통해서 응답을 추출하는 방식을 컨트롤할 수 있다. 기본적으로 xhr.responseText를 취하게 된다.
useBody GET요청에서 데이터를 주소가 아닌 data 섹션에 담아서 실어보내도록 강제한다.
background 기본값은 false이고, 이는 통신이 완료되었을 때 모든 마운트되지 않은 컴포넌트를 새로 그리도록 한다.  true로 변경하면 이 동작을 하지 않는다.

Promise 처리

Promise 객체는 .then()을 통해서 두 개의 콜백을 처리할 수 있다. 해당 Promise가 resolve될 때 호출될 콜백과 reject될 때 호출될 콜백을 각각 받을 수 있다. 혹은 .catch()를 이용해서 에러를 처리할 수 있다. 다음은 데이터 통신을 통해서 응답에 따라 UI를 달리 렌더링하는 일반적인 예를 보여주고 있다.

Data = do
  todo:
    list: null
    error: ''
    fetch: !->
      m.request do
        url: \/api/v1/todos
      .then (items) !->
        Data.todos.list = items
      .catch (e) !->
        Data.todos.error = e.message

Todo = do
  oninit: Data.todo.fetch
  view: (vnode) ->
    if Data.todo.error then
      m \.error Data.todo.error
    else if Data.todo.list then
            Data.todo.list.map -> m \div it.title
         else '.loading-icon'

m.route document.body, \/, { \/ : Todos }

파일 업로드 구현하기

파일 업로드를 구현하는 방법은 다음과 같다.

  1. 드래그 이벤트나 파일 타입의 input 요소로부터 파일 객체를 얻고
  2. 이를 FormData로 래핑한다. 이 데이터를 전송하면 된다.

대략 다음과 같은 식으로 처리할 수 있다.

m.render document.bdoy, m 'input[type=file]', {onchange:upload}

!function upload e
  file = e.target.files.0
  data = new FormData!
    ..append \file file
  m.request do
    method: \POST
    url: \/api/v1/upload
    data: data             ## 폼데이터를 실어보내고
    serialize: (v) -> v    ## 이때 데이터는 그대로 보낸다.

파일 다운로드

파일 데이터를 내려받는 경우에는 deserialze 옵션을 정의해주어야 한다. 다음은 비동기로 svg 파일을 받아와서 렌더링해주는 예이다.

m.request do
  method: \GET
  url: \/files/icon.svg
  deserialize: -> it
.then !-> m.render document.body, m.trust it

관련 글 목차

  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 – 서버와의 통신

 

Mithril로 만드는 초간단 메모앱 – LiveScript

Mithril을 이용해서 간단한 메모앱을 만들어보자. 서버 사이드까지 만들 건 아니고 브라우저의 로컬 저장소를 이용해서 간단하게 메모들의 제목과 내용을 기록하고 보고, 편집할 수 있는 정도로 구현해보자. 사용언어는 Mithril하고 특별히 잘 어울린다 생각되는 LiveScript이며, 전체 코드 분량은 50줄 내외이다.

먼저 개별 메모(포스트)를 디자인한다. 제목과 본문 정도의 프로퍼티만 있으면 되는데, 추가적인 키 값을 하나 추가한다. 이 키 값은 포스트가 생성된 시점의 시간값으로 개별 포스트를 구분하는 식별자로 사용할 수 있게 한다.

class Post
  (args) ->
    @title = args?.title || ''
    @content = args?.title || ''
    @key = args?.key || new Date!get-time!

이렇게 작성된 포스트는 브라우저의 로컬 스토리지에 저장된다. 입출력을 담당할 객체를 하나 만들도록 하자. 키 값을 이용해서 저장된 내용을 불러오거나, 주어진 포스트를 저장할 수 있는 정도면 되기 때문에 다음과 같이 매우 간단하게 작성할 수 있다.

storage = do
  save: (post) !-> local-storage.set-item post.key, JSON.stringify post
  load: (key) -> new Post JSON.parse local-storate.get-item key

작성 UI

작성을 위한 페이지는 다음과 같이 구성하기로 한다.

  1. 페이지의 경로는 /edit/<key>가 된다.
  2. 키 값이 없으면 신규 포스트를 생성하고 키 값이 있으면 해당 키에 대한 포스트를 로딩해온다.
  3. 제목과 본문을 편집할 필드가 각각 있고
  4. 저장, 취소 버튼을 추가한다.  (취소버튼은 단순히 리스팅 페이지로 이동시키도록 하자.)

코드는 다음과 같다. 사실 바인딩 처리가 들어가기 때문에 작성 UI가 가장 코드가 길 것이다.

writer = do
  oninit: (vnode) !->
    vnode.state = if k = m.route.param \key then post: stroage.load k else new Post!
  view: (vnode) ->
    m \.writer,
    * \title,
      m \input, do
        oninput: m.with-attr \value, !~> vnode.state.post.title = it
        value: vnode.state.post.title
      m \br
      m \textarea, do
        onkeyup: m.with-attr \value, !~> vnode.state.post.content = it
        value: vnode.state.post.content
      m \br
      m \button, do
        onclick: !~>
          storage.save vnode.state.post
          m.route.set \/
        , \save
      m \button, {onclick: !-> m.route.set \/}, \cancel

상세 뷰 보기

이번에는 특정 키를 사용해서 해당 키가 가리키는 뷰의 내용을 보는 컴포넌트이다.

  1. 페이지 경로는 /view/<key> 가 되게 한다.
  2. 버튼은 edit, list 두 개를 추가한다. edit 버튼은 /edit/<key>로,리스트 버튼은 / 로 이동시킨다.

코드는 다음과 같다.

viewer = do
  oninit: (vnode) !-> 
    ## 키가 있으면 로드하고 없으면 루트로 리다이렉트
    if k = m.route.param \key then vnode.state.post = storage.load k
    else m.route.set \/
  view: (vnode) ->
    post = vnode.state.post
    m \.viewer,
    * m \h1, post.title
      m \.content post.content
      m \button {onclick: !~> m.route.set "/edit/#{vnode.state.post.key}"}, \edit
      m \button {onclick: !-> m.route.set \/}, \list

리스팅

이번에는 리스팅이다.

  1. 로컬스토리지의 length 속성을 이용해서 리스트를 만든다. (length 속성은 있지만 순회는 불가하다.) 각 키는 .key(i)를 통해서 얻을 수 있다.
  2. 이 기능을 이용해서 모든 키에 대해서 키 값과 제목을 얻어서 링크를 만들 수 있다.

참고로 링크를 만들 때 “/view/<key>”를 그냥 그대로 쓰면 해당 호스트의 루트로부터 시작하는 주소를 참조하기 때문에 이 SPA 페이지를 벗어나게 된다. 따라서 링크를 만들 때에는 {oncreate: m.route.link} 를 이용해서 라우터가 인식하는 경로로 변환처리되도록 해야 한다. 또 새로운 페이지를 만들기 위해서는 키 값을 숫자가 아닌 아무거나 줘서 load를 실패하게 만들면 된다.

lister = do 
  view: (vnode) ->
    m \.lister, 
      m \ul, [0 til local-storage.length].map (i) ->
        key = local-storage.key(i)
        title = storage.load key .title
        m \li, m \a {href: "/view/#key", oncreate: m.route.link}, title
      m \button {onclick: !-> m.route.set \/edit/_}, \write

모든 컴포넌트의 준비가 완료되었다. 라우팅 규칙을 세팅하면 끝이다.

m.route document.body, \/, do
  \/ : lister,
  "/view/:key" : viewer,
  "/edit/:key" : writer

HTML 페이지 준비

 

필요한 라이브러리들과 작성한 라이스브크립트 코드를 로딩하고 실행할 HTML페이지를 준비한다. 로컬에서 돌려보면 UI는 허접하나마 실제 작동하는 앱임을 확인할 수 있다.

<!doctype html>
<html>
    <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/1.1.6/mithril.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/livescript/1.6.0/livescript-min.js"></script>
    <script src="./blog.ls" type="text/ls"></script>
    <script>require('LiveScript').go();</script>
    </head>
    <body>
    </body>
</html>

 

(연재) 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와 통신하기

 

Todo 앱 : 예제 – Mithril [updated for 1.0]

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

간단한 TODO앱을 작성해보도록하자. ToDo 앱은 MVC 패턴으로 구성할 수 있는 가장 간단한 앱의 형태이다. 이 앱을 만들기 위한 기본적인 스케치는 다음과 같다.

  1. 텍스트를 입력할 수 있는 input 박스와 버튼이 있다. 텍스트에 할일을 입력하고 버튼을 클릭하면 아래쪽으로 새로운 할일이 목록에 추가된다.
  2. 목록의 각각의 할일은 체크 박스를 가지고 있다. 완료한 일은 체크 박스에 체크하여 완료로 표시한다. 완료 상태가 되면 해당 할일은 취소선을 적용하여 표시한다.
    Todo 앱 : 예제 – Mithril [updated for 1.0] 더보기