태그 보관물: LiveScript

(javascript – mithril) 서버와의 비동기 통신 – m.request

m.request

목차

  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을 렌더링하는 기능 외에 서버와 양방향으로 통신할 수 있는 기능을 제공한다. 이 기능을 제공하는 클래스가 m.request이다. 이 모듈은 기본적으로 다음과 같은 기능을 제공한다.

  1. GET, POST 메소드를 지원하여 양방향 통신을 가능하게 한다.
  2. 서버와의 인터페이스는 기본적으로 JSON payload를 업로드하고 다운로드 받는 방식을 취한다. 물론 데이터를 어떻게 인코딩하여 전송할지를 커스터마이징할 수 있다.
  3. JSON으로 내려오는 정보는 기본적으로 m.prop 으로 래핑되어 getter – setter 방식으로 접근할 수 있게 한다.

기본적인 GET 요청 방식은 다음과 같다. 만약 /users 에 요청을 보내면 사용자 정보의 리스트를 받게 된다고 가정해보자.

users = m.prop {}
m.request do
  method: \post
  url: \/users
.then users
# users = m.request do ... 로 호출해도 된다. 

.then() 부분은 응답 데이터를 받는 콜백을 넘겨주어, 통신이 완료된 이후에 처리될 작업을 재개할 수 있다. 만약 콜백이 맨 마지막에 넘겨받은 인자값을 리턴하는 식으로 디자인했다면, 여러 개의 .then() 을 연속적으로 체이닝할 수 있다.

users = m.prop {}
check-users = (data) ->
  if data.length < 1 then m.route \/add
  data
m.request do
  method: \get
  url: \users
.then check-users
.then users

위 코드는 응답이 도착했을 때 전달받은 데이터의 원소 개수를 체크하여, 빈 배열이 내려온 경우에 m.route 함수를 이용하여 사용자 추가 화면으로 리다이렉트 되도록 한다.

에러 처리

.then() 메소드는 사실 두 개의 콜백을 인자로 받는다. 첫번째는 응답 데이터를 넘겨받아 데이터를 처리하는 콜백이며, 두 번째 인자는 선택적으로 들어가는데, 에러가 발생했을 때 에러 데이터를 받아 처리하는 콜백이다.

User = {}
User.list-even = -> # 짝수번 회원을 받아온다. 
  m.request do
    method: \get
    url: \/users
  .then (list) ->
    list.filter -> it.id % 2 == 0

controller = ->
  @error_info = m.prop ""
  @users = m.prop {}
  User.list-even!then (@users, @error_info)
  if @error_info! !== "" => # 에러 처리...
  else
     # 정상 데이터 처리...
     ...

만약 컨트롤러에서 정상 응답에 대한 별도 핸들러를 가지고 있지 않다면 첫번째 인자에 null을 넘겨준다.

var controller = !->
  @error = m.prop ''
  @users = m.request { ... } .then null, @error

응답 데이터를 특정 클래스로 캐스팅하기

m.request의 데이터 양식 중에 type 키를 특정 클래스의 constructor로 지정하면, 응답 데이터를 특정 클래스 인스턴스로 바로 변경가능하다.

# 서버로부터의 응답이 [ {name: "John"}, {name: "Mary"}] 라 가정한다.
User = (data) !->
  @name = m.prop data.name

users = m.request do
  method: \get
  url: \users
  type: user
# users => [User('John'), User('Mary')]

서버로부터의 응답이 값만 넘겨오는 것이 아니라 응답 코드 등 여러 메타 정보를 포함하고 있는 형태라면 그 정보 중에서 특정 상황에 맞는 일치하는 항목만을 꺼내고 싶을 때가 있을 것이다. 이 때는 다음과 같이 unwrap{Sucesss | Error}을 설정해줘서 상황별로 맞는 내용을 사용하게끔 한다.

# 서버로부터의 응답이 { data: [{name: "John"}, {name: "Mary"}], count: 2} 라 가정한다. 

users = m.request do
  method: \get
  url: \/users
  unwrap-success: (res) -> res.data
  unwrap-error: (res) -> res.error
# 위 요청이 성공한 경우, `users`는 `[ {name: "John"}, {name: "Mary"}] 이 된다.

데이터 인코딩 변경하기

m.request는 기본적으로 요청,응답의 데이터 페이로드를 JSON으로 가정한다. 만약 JSON 규격이 아닌 별도 규격을 사용한다면 serialize, deserialize 키에 대해서 인코딩 함수를 지정해준다. 다음은 서버로부터 텍스트 파일을 읽어들일 때 사용할 수 있는 방법이다.

file-contents = m.request do
  method: \get
  url: \/myfile.txt
  deserialize: -> it # (data) -> data

HTML5 폼 업로드

만약 폼 업로드를 해야한다면 serialize 키를 오버로드해야 한다.

dropEventHander = (e) !->
  file = e.dataTransfer.files[0]
  data = new FormData!
  data.append \file, file
  m.request do
    method: \post
    url: \upload
    data: data
    serialize: -> it

시그니처

m.request의 대략적인 시그니처는 다음과 같다.

Promise m.request(Options options)

리턴되는 Promisem.prop() 처럼 getter-setter 로 동작하는 인터페이스를 가진 객체이며, .then()이라는 메소드를 가지고 있다. 이 메소드는 응답데이터 및 에러처리를 위한 두 개의 콜백을 인자로 받는다. 또한 체이닝을 위해 .then() 자체도 다시 Promise 타입의 객체를 리턴해야 한다.

Promise :: GetterSetter {
  Promise then (any successCallback(any value), any errorCallback(any value))
}

m.request의 인자로 전달되는 객체는 Options 타입인데 이는 다음과 같이 정의된다.

Options :: XHROptions | JSONOptions 
where
    XHROptions :: Object {
      String method,
      String url,
      [String user,] # 인증이 필요한 경우 사용자
      [String password,] # 인증이 필요한 경우 패스워드
      [Object<any> data,] # 페이로드가 될 데이터
      [Boolean background,] # 백그라운드에서만 동작하고 템플릿 렌더링에 영향을 주지않을지 여부. 기본값은 false
      [any initialValue,]
      [any extract(XMLHttpRequest xhr, XHROptions options),]
      [any unwrapSuccess(any data, XMLHttpRequest xhr),]
      [any unwrapError(any data, XMLHttprequest xhr),]
      [String serialize(any dataToSerialize),]
      [any deserialize(String dataToDeserialize),]
      [void type(Object<any> data),]
      [XMLHttpRequest? config(XMLHttpRequest xhr, XHROptions options)]
    }
    JSONOptions :: Object {
      String dataType,
      String url,
      String callbackKey,
      String callbackName,
      Object<any> data
    }

앞서 소개되지 않은 몇 가지 추가 옵션들을 더 살펴보자.

초기값

initialValue 키는 (특히 백그라운드로 동작할 때) 응답이 오기 이전의 데이터 값을 대체할 기본값이다. 이를 사용하면 뷰 단에서 응답값이 null인지 여부를 매번 체크하지 않아도 되므로 백그라운드 동작 여부에 독립적인 코드를 사용할 수 있는 좋은 방법이다.

추출 방식

extract 키는 XMLHttpRequest 객체로부터 데이터를 뽑아내는데 사용된다. 기본값은 xhr.responseText를 사용하는데, 만약 필요한 데이터가 본문 payload 외에 응답 헤더 항목에 들어있다면 이를 사용하려고 할 때 쓸 수 있다. 예를 들어 서버가 JSON으로 응답을 주지만, HTTP 에러와 같이 오류는 JSON 포맷이 아닌 경우에 다음과 같이 작성하여 이에 대응할 수 있다.

m.request do
  method: \post
  url: \/foo
  extract: (xhr, xhr-options) ->
    if xhr-options.method == \HEAD then
      xhr.get-response-header 'x-item-count'
    else
      xhr.response-text

(Javascript | mithril ) 예제 – mithril로 만드는 초간단 클라이언트 사이트 블로그

로컬 스토리지를 이용한 메모장 만들기

미스릴을 이용해서 간단한 블로그 혹은 메모장을 만드는 과정을 살펴보도록 하겠다. 예전에는 뭐 ROR가지고 15분 만에 블로그만들기… 같은 게 유행했는데, 암튼 제대로된 UI는 없겠지만 최소 기능만 구현하는 것으로 해보자.

미스릴의 view쪽 코드를 작성하는 게 바닐라 자바스크립트로는 여러 가지 애로사항이 꽃필 수 있어서 라이브스크립트를 사용하기로 한다. 서버쪽 코드를 작성할 일이없기 때문에 60라인 이내로 다듬어지게 된다.

먼저 html 코드는 다음과 같다. 미스릴과 라이브스크립트 번역기를 로드하고 ㅇ제부터 작성할 스크립트 파일을 불러온 후에 이를 실행하는 코드가 전부이다.

그리고 body에는 화면이 비어있는 경우에 표시할 loading...이라는 문구만 적혀있게끔 한다.

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

스크립트 작성하기

위에서 적은대로 blog.ls라는 파일을 생성하고 작성을 시작한다. 먼저 각각의 글을 저장하게될 모델 클래스를 만들어보자. 별도의 서버나 DB가 없으므로 로컬 스토리지에 저장할 건데, 그러핟면 JSON 포맷으로 변환이 가능해야 한다. 사실 m.request를 이용해서 서버와 통신해서 별도의 서버나 DB에 저장할 수도 있고 이 때의 포맷도 JSON을 사용하므로 서버만 준비된다면 클라이언트쪽은 storage-manager 부분만 정리하면 나머지는 완전히 동일하게 가져갈 수 있으니 참고하자.

작성한 글을 구분할 키는 생성시점의 타임스탬프를 사용하도록 한다. 또 작성시에 UI와 바인딩되어야 하는 부분은 제목과 본문만 있으면 된다.

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

이제 작성한 포스트를 로컬 스토리지에 저장하고, 또 키를 이용해서 불러와 줄, 저장을 담당할 객체를 만들어보자.

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

글을 넣었다 뺐다하는 부분은 벌써 다 만들었다. 이번에는 작성하는 UI를 만들어보자. 그 전에 저장되는 글 상태를 볼 수 있도록 하는 임시 렌러더를 하나 만들어본다.

status = 
  view: (ctrl, post) ->
    m \ul,
      * m \li, post.timestamp!
        m \li, post.title!
        m \li, post.content!

비슷한 모양으로 코드가 반복되므로 다음과 같이 변경하는 것도 방법이다. (아래코드는 세줄에 걸쳐 썼지만 한 줄로 합쳐도 되는 코드이다.)

status = 
  view: (ctrl, post) ->
    m \ul, <[ timestamp title content ]>.map -> m \li, post[it]!

위 임시 정보창을 붙여서 작성창을 만들어보자.

writer = 
  controller: -> post: new Post!
  view: (ctrl) ->
    m \div.writer,
      * \title
        m \input, 
          onkeyup: m.with-attr \value, ctrl.post.title
          value: ctrl.post.title!
        m \textarea,
          onkeyup: m.with-attr \value, ctrl.post.content
          value: ctrl.post.content!
        m \button,
          onclick: ~> storage-manager.save ctrl.post
        , \save
        m.component status, ctrl.post

이제 여기까지 중간 점검!

m.mount document.body, writer

를 맨 아래에 추가해보고 브라우저에서 해당페이지를 열어보자.

> python -mhttp.server 8888

파이썬으로 간단한 HTTP 서버를 실행하고 해당 페이지를 열면 UI는 조악하지만 제목과 내용을 입력하는 창이 나오며 키를 누를 때마다 내용이 갱신되고, 언제든지 저장할 수 있는 페이지가 만들어졌다. 우리는 m.route를 이용해서 하나의 페이지에서 목록/작성/상세를 모두 구현할 것이다. 각각의 루트는 다음과 같이 구분하자.

  • ‘/’, ‘/list’ : 글 목록 표시
  • ‘/add/:key’ : 글 작성, 편집.
  • ‘/view/:key’ : 글 보기

이제 저장된 리스트를 표시하는 컴포넌트를 만들어보도록 하자. 저장된 키들을 순회하여 제목을 뽑아낸 다음, 이 제목과 키를 이용해서 링크를 만든다. 여기서는 정적 HTML 페이지를 사용할거니까, m.route를 이용해서 링크를 처리한다.

또, 리스트 아래에는 새 글 쓰기로 연결되는 버튼도 만든다.

lister = 
  view: (ctrl) ->
    * m \ul, [0 til local-storage.length].map (i) ~> 
        t = storage-manager.load local-storage.key i
        m \li, 
          m "a[href=javascritp:;]", {onclick: ~> m.mount "/view/#k"}, \t
      m \button, {onclick: -> m.route '/add'}, \write

다음은 작성된 내용을 표시하는 부분이다. 끝에는 리스트로 돌아가는 버튼과 현재글 편집 버튼을 추가했다.

추가: 컴포넌트가 구성될 때 controller는 최초 한 번만 호출된다. 이 시점에서 키가 올바른지 아닌지를 판단하여 올바르지 않은 키가 들어왔다면 새 창으로 이동하도록 해보자.

viewer = 
  controller: -> if k = m.route.param \key then post: storage-manager.load k
    else: m.route '/add/new'
  view: (ctrl) ->
    m \div.wrapper,
      m \h1, ctrl.post.title!
      m \div.content, ctrl.post.content!
      m \button, {onclick: ~> m.route "/add/#{ctrl.post.timestamp!}"}, \edit
      m \button, {onclick: -> m.route "/"}, \list

저장된 글을 편집하는 경우에는 키가 넘어온 경우에는 기존 글을 불러오고 아니면 새 글을 시작하면 된다. 이미 모든 필드가 바인딩되어 있기 때문에 writercontroller 함수만 아래와 같이 바꿔주면 된다.

writer = 
  controller: -> if k = m.route.param \key then post: storage-manager.load k
    else post: new Post!
  view: (ctrl) -> ...

이제 모든 주소들을 등록해준다.

m.route document.body, '/', do
  '/list': lister,
  '/add/:key': writer,
  '/view/:key': viewer

다했다!!!

모든 코드를 한 데 합쳤을 떄의 모양은 아래와 같다.