Javascript, 스터디

(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