(연재) 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 – 서버와의 통신

 

(연재) 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] 더보기

자바스크립트 배열의 정렬

javascrip는 개인적으로 참 마음에 안드는 부분이 많은데, 그 중에서도 배열의 sort() 메소드는 좀 좌절스러운 것이…

a = [0 to 10].map -> parseInt Math.random! * 100
# [ 74, 7, 45, 41, 43, 85, 84, 66, 41, 91 ]
a.sort!
# [ 41, 41, 43, 45, 66, 7, 74, 84, 85, 91 ]

숫자로 된 배열을 정렬할 때도 사전식으로 비교해서 어이없는 결과를 만들어낸다…. sort() 메소드는 비교 함수를 받긴하므로, 정수 크기별로 비교하려면, 다음과 같이 각 값을 정수형으로 계산한 결과를 던져주는 비교함수를 넣어준다. (-첫째인자 + 둘째인자로 코딩하면 -첫째인자에 의해서 자동으로 정수형으로 인식함.

#livescript
a.sort -> +&0 -&1
# [ 7, 41, 41, 43, 45, 66, 74, 84, 85, 91 ]

업데이트

원글의 내용이 라이브스크립트로 씌여져 있어서 문법이 익숙치 않은 사람을 위해서 자바 스크립트 문법으로 대체합니다.

let a = [0,1,2,3,4,5,6,7,8,9].map( () => parseInt(Math.random() * 100));
console.log(a.sort((x, y) => +x-y));

m.withAttr – 이벤트 핸들러 처리 – Mithril

m.withAttr() 함소는 가상노드로 만들어지는 DOM 요소에 이벤트 핸들러를 손쉽게 추가해주는 편의함수이다. 예를 들어 input 필드에 텍스트를 입력할 때 항상 소문자로만 입력을 받고 싶다고 하자. 그러면 oninput 이벤트 핸들러를 통해서 입력된 값을 소문자로 변환해서 어딘가에 저장하고, 이 값을 value 속성을 통해서 표시할 수 있다. 대략 다음과 같은 식으로 구현해볼 수 있을 것이다.

# javascript
let state = {desc: ''}

let test = {
  view: function() { 
    return m('input', {
      oninput: function (e) { state.desc = e.target.value.toLowerCase(); },
      value: state.desc
    });
  }
};

m.mount(document.body, test);

이벤트 핸들러는 이벤트 객체가 넘어가게되고 특정한 필드 값을 참조하기 위해서는 event.target.property 와 같은 식으로 접근해야하기 때문에 조금 번거롭다. m.withAttr() 함수는 이벤트가 발생한 요소의 속성 이름과 핸들러 함수(그 속성값을 받아서 처리하는)를 조합하여 이벤트 핸들러로 등록한다. 따라서 보다 범용적으로 만들어진 프로세스 함수를 간단하게 이벤트 핸들러로 편입할 수 있는 장점이 있다.  만약, m.withAttr() 함수를 사용한다면 위 코드는 아래와 같이 정리될 수 있다.

 

# JS
let state = {
  desc: '',
  setDesc: function(value){ state.desc = value.toLowerCase(); }
};

let test = {
  view: function() {
    return m('input', {
        oninput: m.withAttr('value', state.setDesc),
        value: state.desc
    });
  }
};

m.mount(document.body, test);

## LS equivalent
state = { desc: '' , set-desc: (v) !-> state.desc = v.to-lower-case! }
test = 
  view: -> m \input do
    oninput: m.with-attr \value state.set-desc
    value: state.desc

m.withAttr()의 장점은 주어진 속성을 읽어들이는 객체가 이벤트가 발생한 객체가 아닌, 이벤트핸들러가 바인드되는 객체라는 점이다. 다음과 같은 예를 살펴보자.

state = {desc: '...!'}
test = 
  view: -> 
    m 'div[href="/foo"]' do
      onclick: (e) !-> state.desc = e.target.href
    , m \span state.desc

m.mount document.body, test

위 앱에서 “…!”를 클릭하면 이는 span 요소를 클릭했기 때문에 e.target 은 span 요소를 가리키며, 이 요소의 href 속성은 undefined가 되기 때문에 텍스트가 사라진다. 이벤트 핸들러는 div 객체에 달려 있고, span 객체에서 발생한 이벤트가 전파되면서 div 객체에서 처리했기 때문에 발생하는 불일치이다. (이를 해결하려면 e.target이 아닌 e.currentTarget을 써야 한다.) m.withAttr()은 항상 e.currentTarget을 쓰기 때문에 이러한 불일치를 사전에 방지할 수 있다.


관련 글 목차

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