LiveScript 살펴보기 – 04 제어문

이번 글에서는 LS의 기본적인 제어 구문인 반복문과 분기문을 작성하는 방법에 대해서 살펴볼 예정이다. LS의 특징인 “거의 모든 것은 표현식이다”라는 점에서 for/while 문 자체도 하나의 표현식으로 최종 표현식의 결과를 모은 리스트로 평가될 수 있다는 점을 놓치지 말자. LiveScript 살펴보기 – 04 제어문 더보기

LiveScript 살펴보기 – 03 함수

LS에서 함수는 일반 문법 편에서 잠깐 언급했듯이 화살표를 써서 간단히 정의할 수 있다. 이 함수 표현에서 중요한 점 두 가지는 첫 째 우변은 하나 이상의 표현식이라는 점과 표현식이 순서대로 나열되는 경우 맨 마지막 표현식의 결과가 자동으로 리턴된다는 것이다.

함수

LS는 함수형 프로그래밍 언어의 스타일을 많이 도입했다고 하였다. 비록 LS가 진짜 순수한 함수형 언어는 아니지만, 함수형 언어의 스타일을 도입한다는 것은 LS내의 함수라는 것은 가급적 아래와 같은 특징을 갖도록 디자인되어야 한다는 것이다.

  1. 순수성 : 함수의 결과값이 순수하게 파라미터에만 의존할 것. 따라서 입력된 인자가 같다면 항상 리턴될 출력값도 같음을 보장한다.
  2. 간결성 : 사실 억지로 만든 말이기는 한데, 함수에 대한 연산 (커링, 바인딩, 파이핑, 합성)이 다양하고, 함수 자체가 1급 시민인 점(이는 JS로부터 자연스럽게 물려받는 특징이다.)에 착안하여, 가능한 간결하고 명료한 함수들을 정의하고, 이러한 함수들을 조합하여 필요한 함수를 생성하는 방법을 지향하는 것이다.

이전에 예에서 리스트의 최대값을 찾는 getMax 함수를 잠깐 언급한 바, 있는데 이 함수는 JS로 쓴다면 아래와 같을 것이다.

function getMax(arr) {
  if(arr.length < 1) { return null }
  var x = arr[0];
  var i = 1;
  var result = x;
  while(i < arr.length) {
    if (x < arr[i] ) {
      x = arr[i];
      result = x;
    }
    i += 1;
  }
  return result;
}

그리고 LS에서는 다음과 같이 쓴다.

get-max = (arr) -> arr.reduce (>?)

너무 심하게 부풀려서 비교한다고 생각할 수 있는데, 물론 JS에서도 함수형 스타일로 맵/필터/리듀스를 할 수 있는 API가 Array 타입에 존재하고 있다. 따라서 위 LS 코드를 JS로 쓴다면 (그것은 마치 함수형 스타일의 JS 코드겠지만) 다음과 같이 쓸 수 있다.

var getMax = function(arr){ 
  return arr.reduce(function(x, y){ 
    return x > y ? x : y; 
  }
);

다만, 여기서 말하고자 하는 것은 LS로 쓰면 함수를 엄청 짧은 코드로 쓸 수 있다는 점이 아니라, “두 수 중에서 큰 수를 판단하는 함수”를 리스트의 각 원소에 대해서 순차적으로 적용하면 리스트 내에서 가장 큰 수를 찾을 수 있다”는 간단한 아이디어에 관한 것이다. 두 개의 연산(함수)에 대해 이해하고 그것을 연관지어 원하는 기능을 쉽게 조합할 수 있는 것이 함수형 스타일의 가장 큰 특징이라 할 수 있겠다.

리턴

함수의 본체가 하나의 표현식인 경우에는 LS의 함수 정의 문은 one-liner로 작성된다. 두 개 이상의 표현식이 함수의 본체를 구성하는 경우에는 -> 뒤에서 줄을 바꾸고 들여써서 블럭을 구분하여 표기할 수 있다.

times = (x, y) -> x * y
sum = (arr) ->
  s = 0
  for i in arr
    s += i
  s

마지막 표현식은 자동으로 리턴되므로, 명시적으로 리턴이 없는 함수를 정의하고 싶다면 !-> 기호를 써서 정의한다.

호출

함수 호출 시, ()를 생략한다. 또한 인자들은 callable하지 않다면 콤마를 생략하고 나열할 수 있다. 인자를 받지 않는 함수는 !를 써서 호출됨을 표시한다. 그리고 인자 뒤에 오는 and, or, xor, 한 칸 띈 후의 ., ?. 등은 모두 암묵적으로 호출 구문을 완료한다. 따라서 공백을 기준으로 체이닝 표기를 간단히 할 수 있다.

$ \h1 .find \a .text! #=> h1 a 의 내용
# $('h1').find('a').text()

f!
[1 2 3].reverse!slice 1 #= [2, 1]

익명함수의 경우, 인자가 없는 케이스에는 do를 -> 에 쓰면 이를 호출한다. 간단하게 익명함수를 이용해서 여러 표현식을 묶은 블럭을 만들고 실행하는 방법으로 이해.

do -> 3 + 2 
#=> 5
#(function(){
#  return 3 + 2;
#})();

do를 이름이 있는 함수1와 썼을 때, 그리고 do가 표현식에 쓰인게 아니라면 이름지은 함수는 유지된다. 즉, do를 평가하기 전에 함수를 참조할 수 있고, do 문을 만났을 때 한 번 더 실행하는 것이다.

i = 0
f 9 #f를 한 번 실행. i == 1
i #=> 1
do function f x # 여기서 한 번 더 실행
  ++i
  x
i # => 2

축약된 객체 블럭 문법은 함수 호출 구문에서 사용할 수 없다. 단, 한 줄에 쓰여진 리터럴은 인식된다. 함수의 인자 중 하나로 객체 블럭을 쓰고자 한다면 do를 이용해서 블럭을 명시해야 한다.

func
  a:1
  b:2
## 컴파일 되지 않음

## 대신 이렇게 쓸 수 있다.
func a:1, b:2

## 보통은 do를 사용한다.
func do
  a: 1
  b: 2

이처럼 do는 함수 호출 시에 요긴하게 많이 쓰인다. 특히 개별 인자를 각 라인의 표현식으로 사용하려할 때 유용하다.

함수의 중위표현

파라미터를 2개 받는 함수는 백팃으로 둘러 싸서 중위연산자처럼 쓸 수 있다.

add = (x, y) -> x + y
2 `add` 3 #=> 5

g = (a, b) ->
  add ...     # 함수 내에서 쓰이는 `...`은  인자를 그대로 넘긴다는 의미로 해석할 수 있다. 
# 11

그리고 함수의 본체 내에서 …을 쓰면 암묵적으로 모든 인자의 리스트로 인식한다. 이는 특히 super를 호출할 때 유용하다.

파라미터

파라미터는 표현식을 쓰는 표기로도 확장된다. 즉 객체나 다른 파라미터에 관한식으로 파라미터를 받으면, 해당 값으로 맵핑된다는 이야기이다. 이를 통해서 함수 본체에서 각 파라미터간의 관계를 재설정해야 하는 부담을 줄 일 수 있다. 아래의 예제를 보자.

set-person-params = (
  person
  person.age        # 두 번째 인자는 첫 번째 인자의 .age 키에 배당된다.
  person.height ) -> person

p = set-person-params {}, 21, 180cm
# {age: 21, height: 180}

# 'this' 확장하기
set-text = (@text) -> this
# var setText = function(text){ this.text = text; return this; }

위의 setPersonParams() 함수는 세 개의 인자를 받는데, 그 중 두 번째, 세 번 째 인자는 첫 번째 인자의 프로퍼티로 명시되어 있다. 따라서 함수 호출 시에 해당 값이 주어지면, 이는 첫번째 인자로 넘겨진 객체의 프로퍼티로 자동으로 세팅된다. 따라서 함수 본체 내에서 person.age = age와 같은 처리를 따로 하지 않고 인자 자체를 확장하여 person.age로 쓰는 것으로 대체가능하다.

특히 이 확장은 객체의 메소드나 함수를 작성할 때, 특히 this를 다룰 때 매우 간결하게 쓰일 수 있다.

디폴트 값

파라미터에는 디폴트 값을 미리 지정해줄 수 있으며, (파이썬 스타일), 좀 헷갈릴 수는 있는데, 논리 연산을 수행하여 디폴트값/오버라이드를 적용할 수 있다.

add = (x && 4, y || 3) -> x + y
# x는 무조건 4로 오버라이드되고
# y는 없으면 3이 된다.

또한 객체를 통으로 받아서 특정 키를 분해해 낼 수 있다.

set-coords = ({x, y}) -> "#x, #y"
set-coords y:2, x:3 #=> "3, 2"

# 그리고 그 와중에 다시 디폴트 값을...
set-coords = ({x = 1, y = 3}) -> "#x, #y"

그리고 ...y 등과 같이 일련의 인자들을 하나의 리스트로 취급하는 것도 가능하다.

f = (x, ...ys) -> x + ys.1
f 1 2 3 4 #=> 4

이외에도 캐스팅 연산자를 파라미터에 붙일 수 있는데, 그러면 자동으로 평가된 후 들어간다.

f = (!!x) -> x
f 'truely' # true

g = (+x) -> x
g ' ' # 0

obj = {prop: 1}
h = (^^x) ->
  x.prop = 99
  x
h obj
obj.prop # 1

파라미터의 생략

JS의 함수는 호출 시 파라미터 개수에 크게 구애받지 않는다. 즉 선언된 파라미터보다 부족한 개수의 인자가 넘겨지면, 빈 인자는 undefined를 갖게되고, 인자가 과하게 많이 넘겨지면 함수의 로컬 스코프에 매핑되지 못한 인자는 모두 무시된다.

LS의 함수에 있어서 정의를 파라미터 없이 함수를 만들었다 하더라도 함수 본체에서는 파라미터를 참조하는 것이 가능하다. 사실 이는 JS의 스펙에 정의된 arguments2 객체에 의한 것이다.

단 인자 함수의 파라미터는 it으로 지칭한다. 그외의 파라미터는 모두 &0, &1 과 같은 식으로 번호 순서대로 참조하여 처리할 수 있다.

바운드 함수

바운드 함수는 특정 객체에 소유권이 묶인 함수를 말한다. 보통 함수 내에서 this를 참조하는 것은 해당 함수를 호출한 문맥이 되는데, 바운드 함수는 처음 바운드한 시점의 문맥이 유지된다. 바운드 함수의 자세한 내용에 대해서는 별도로 찾아보도록 하고, 바운드 함수를 작성하는 것은 ~> 를 써서 웨이브진 화살표를 쓴다는 점만 기억하자.

obj = new
  @x = 10
  @normal = -> @x  
  @bound = ~> @x

obj2 = x: 5
obj2.normal = obj.normal
obj2.bound = obj.bound

obj2.normal! # this.x 이고 이 때 this는 obj2 이므로 5
obj2.bound # 10. bound메소드는 obj에 바인딩되어 있으므로, 내부의 this는 obj를 가리킨다.

흔히 바운드 함수는 특정 객체의 메소드를 이벤트 핸들러를 사용하며, 그 내부에서 this를 참조할 때 유용하다.

- 대신 ~를 쓰는 것이 바운드 함수를 의미한다는 것은 매우 일관적으로 적용되며, !~>, ~~>, ~function 등의 표현이 그대로 사용될 수 있다.

커링

커링은 하스켈 커리의 이름을 따서 명명되었는데, 모든 다 인자 함수는 단인자 함수들이 합성된 상태로 볼 수 있다는 것을 말한다. 예를 들어 두 정수를 더하는 add 라는 함수가 있다면 다음과 같이 정의하고 호출할 수 있다.

add = (x, y) -> x + y
add 3 2

이 때, 이 호출식을 (add 3) 2라고 생각하는 것이다. 그러면 add 3은 정수 하나를 받아서 3을 더한 값을 리턴하는 단인자 함수가 된다.

그렇다면 다시, adda 라는 정수를 받아서 “b라는 정수를 받아 여기에 a를 더해서 리턴하는 함수”를 리턴하는 함수라고 생각할 수 있다.

add = (a) ->
  (b) -> a + b

add-one = add 1
add-one 2
# 3

이렇게 커링을 이용하면 쉽게 부분적용된 함수를 만드는 것이 가능해진다. 그리고 LS에서는 자동으로 커리되는 함수를 만들 수 있는 선언법으로 -->를 쓰는 것을 지원한다. 역시 같은 맥락에서 바운드된 커리드 함수는 ~~> 으로 선언할 숭 있다.

add = (a, b) --> a + b
add-one = add 1
add-one 2 #=> 3

접근자 단축

접근자 메소드/함수의 경우에 맵이나 필터 동작에 적용되는 경우 예를 들어 (x) -> x.prop과 같은 식으로 처리하는 것은 (.prop)으로 줄여 쓸 수 있다. 이는 특정 프로퍼티를 호출하는 것을 괄호로 둘러싼 것이기 때문에 메소드 호출 역시 같은 식으로 처리할 수 있다.

map (.length) <[ hello there you ]> #=> [5, 5, 3]
filter (.length < 4), <[ hello there you ]> #=> ['you']

map (.join \|) [[1 2 3], [7 8 9]]
#=> ['1|2|3', '7|8|9']

반대로 (obj.) 이라고 쓰는 것은 (it) -> (obj.it)의 단축 표현이 된다.

obj = one:1, two:2, three:3
map (obj.) <[one, three]> #=> [1, 3]

부분 적용

표현식 내에 언더스코어(_)를 사용하여 해당 표현식을 1개 인자로 받는 함수로 간주하고, 언더 스코어는 해당 인자의 위치를 표시하는 플레이스 홀더로 생각할 수 있다. 이는 커리드 함수에서 적절한 위치에 있지 않은 파라미터를 가변으로 남기고 싶을 때 유용하다.

# 여기서 쓰인 filter는 별도로 정의된 top-level의 함수라 가정한다.
filter = (fn, arr) --> arr fn
# 이 떄, 특정한 리스트에 대해서 고정하고 필터링 함수만 변경하려할 때,
# 다음과 같이 쓰게 되는데
filter-nums = (fn) -> filter fn, [1 to 5] # 
# 인자로 받게되는 fn의 위치를 `_`를 이용해서 표시해주면 된다.
filter-nums = filter _, [1 to 5]


filter-nums (<3) # [1, 2]

이러한 부분적용된 함수는 특히 고차 함수를 파이핑으로 연결할 때 유용하다. 아래 예제는 underscore.js를 이용해서 특정한 리스트를 조작하는 코드이다. “인자로 받은 객체가 다시 인자로 전해질 때”를 상정하기 때문에, _의 사용이 혼동되지 않고 해석될 수 있다.

[1 2 3]
|> _.map _, (* 2)
|> _.reduce _, (+), 0
# => 12

백 콜

콜백으로 주어지는 표현식들은 결국 블럭으로써, 들여쓰기를 적용해야 한다. 백 콜은 이러한 방식을 거꾸로 표현하여 콜백을 들여쓰지 않고 표현할 수 있게 해준다.

map (-> it * 2), [1 to 3] 

# 백 콜로 전환
x <- map _, [1 to 3]
x * 2  # 여기서부터는 _ 에 들어갈 표현을 쓸 수 있다. 

백 콜은 콜백을 들여쓰지 않게 해주기 때문에, 중첩되는 콜백지옥을 깔끔하게 처리해주는 장점을 가지고 있다. 만약 top레벨의 코드 중간에 백 콜을 쓰게 된다면, 이후 끝라인까지의 모든 내용이 콜백 내의 코드로 간주된다. 백 콜의 코드가 중간에서 끝나야 한다면 미리 do를 사용해서 들여쓰기 블록을 시작해주자.

do
  data <-! $.get 'ajaxtest'
  $ '.result' .html data  # $.get \ajaxtest의 콜백이며, 콜백의 인자는 data
  processed <-! $.get 'ajaxprocess', data  # 콜백 내에서 `ajaxprocess`에 대한 처리를 또 호출
  $ '.result' .append processed # 여기는 콜백 내의 콜백이지만, 들여쓰기는 더 이상 없다.

alert \hi

위 코드는 다음과 같이 컴파일 된다.

$.get('ajaxtest', function(data){
  $('.result').html(data);
  $.get('ajaxprocess', data, function(processed){
    $('.result').append(processed);
  });
});
alert('hi');

LET/NEW

함수와 관련하여 let, new에 대한 표현을 짚고 마무리하도록 하겠다. let은 특정한 익명함수를 생성하는데, 해당 함수 내에서의 특정한 문맥을 생성해준다. 즉 let A = B { 블럭} 의 형태이며, 이 때 블럭 내에서 언급되는 A는 모두 B로 치환된다.

let $ = jQuery
  $.isArray []

위의 이 표현은 $jQuery가 되는 블럭 스코프를 생성한 후에 $.isArray []를 평가하였으므로 true가 된다. 이 때 외부 스코프에 $이 있더라도 여기서는 자체 스코프만을 참조할 것이다. 비슷하게 아래와 같은 코드도 작성할 수 있다.

x = let @ = a:1, b:2
  @b ^ 3
x #=> 8

new는 새로운 익명 컨스트럭터를 만들어서 즉시 호출하는 개념이다.

doc = new
  @name = \spot
  @mutt = true

# {name: 'spot', mutt: true}

보너스

다음은 nodejs를 통한 간단한 HTTP 서버의 기본 구현을 LS로 작성한 것이다. 콜백속에서 또 콜백을 전달하는 형태의 함수호출 패턴이 존재하고, 체이닝이 쓰인다. 이를 do와 백콜을 이용하여 깔끔하게 작성할 수 있다.

require! <[ fs http url ]>
const ROOTDIR = 'html/'

do
  (req, res) <-! htttp.create-server
  url-obj = url.parse req.url, yes no
  do 
    err, data <-! fs.read-file ROOTDIR + url-obj.pathname
    if err? 
      res.write-head 404
      res.end <| JSON.stringify err
    else
      res.write-head 200
      res.end data
.listen 8080

참고자료

  • LiveScript.net
  • HTTP 서버 구현: https://mylko72.gitbooks.io/node-js/content/chapter7/chapter7_4.html

  1. named function. 여기서는 function 키워드를 써서 정의한 함수를 말한다. 
  2. arguements에 대한 MDN 설명 참조. 

LiveScript 살펴보기 – 02 연산자

이번 글에서는 LiveScript의 연산자에 대해서 살펴보겠다. 함수형 언어 스타일을 도입하면서 코드가 간결해지는 대신에 기본적인 사칙/비교 연산외의 여러 연산자들이 많이 사용된다.

연산자

기본적인 연산자는 JS와 동일하다. 단 모듈로 연산자가 추가되었다 (%%)

  • -3 % 4 : -3
  • -3 %% 4 : 1

제곱연산자는 오른쪽 연관이며, 다른 단항 연산자보다 높은 우선순위를 가진다. ^** 를 같이 쓸 수 있다. 그리고 증감연산자도 있다. (함수형 언어에서 증감 연산자는 가급적 안쓰는게 좋으며, LS 코드에서는 for 문도 순회의 개념이기 때문에 별로 쓸 일이 없다.)

비트연산

비트 연산은 .&. 과 같이 연산자 앞뒤로 .을 붙여서 사용한다.

비교 연산

기본적인 비교 연산자는 JS와 동일하다. 대신에 파이썬 처럼 연속으로 비교할 수 있다. (2 < 4 <= 7 > 3) 비교연산에서 특별한 점은 max, min 연산자가 있다는 점이다. 각각 >?, <? 을 쓸 수 있다.

get-max = -> it.reduce (>?)
get-max [5 1 2 4 3] #=> 5

기본적으로 LS의 비교는 엄격한 동질성을 따진다 (암시적 강제 형변환을 허용하지 않는다.)

2 + 4 == 6 # 2 + 4 === 6, true
2 + 4 == '6' # 2 + 4 === '6', false
# JS에서 2 + 4 == '6' -> 6 == '6' 으로 true이다.

명시적으로 JS의 모호한 비교를 쓰려면 ~= 연산자로 비교해야 한다.

캐스팅

+, - 는 문자열 앞에 쓰이면 숫자값으로 캐스팅하는 용도가 된다.

논리 연산

&&, || 도 똑같이 쓰이지만, and, or이 더 자연스럽다. 또한 이들은 함수/인자 사이에서 결합/분배가 발생한다.

(f or g) 1 # f(1) || g(1)
(f and g or h) 3 4 # f(3, 4) && g(3,4) || h(3, 4)

in 과 of

for 문에서 리스트의 각 원소를 순회하거나, 객체의 각 키를 순회하는데는 각각 in, of를 사용한다. 또한 이 키워드는 연산자로 쓰일 때는 특정 원소나 키가 리스트나 객체에 있는지를 검사한다.

list = [7 8 9]
2 in [1 2 3 4 5] # 'contains`의 의미로 쓰였다.
3 in list
\id of id: 23, name: \rogers

파이핑

|> 표현은 파이핑으로 좌변에서 계산된 표현식의 결과를 우변으로 넘겨줄 수 있다.

expression1 = <# ... >
expression1 |> func # func(expression1)

4
|> (+ 1)
|> even
# even( (function(x){ return x + 1;})(4))
# => false

함수 합성

>>, << 은 함수를 합성한다.

(f >> g) x # g(f(x))
(f << g) x y # f(g(x, y))

리스트 결합

  1. ++ 을 사용한다.
  2. * 3 과 같은 식으로 특정 리스트를 n 번 반복할 수 있다.
  3. * 'sep' 은 문자열 리스트를 구분자를 써서 하나의 문자열로 join 할 때 쓴다.

그외 단항 연산자는 리스트 앞에 붙여서 스프레딩 할 수 있다.

문자열 연산

문자열 연산은 여러 면에서 리스트와 유사한 점이 많다.

  • + 으로 두 문자열을 결합할 수 있다.
  • += 연산도 지원한다.
  • * 3 의 반복 표현도 지원된다.
  • .replace, .split 을 정규식 객체와의 연산으로 처리할 수 있다. str - /reg/, say year / \y
'say yeah' - \a #=> 'sy yeh'
'say yeah' / \y #=> ['sa', ' ', 'eah']
'x' * 3 #=> 'xxx'
string = 'hello' + ' ' + 'world'
string += ' everyone'

?

?는 기본적으로 주어진 심볼의 존재여부를 확인하는데, 여러 가지 맥락으로 쓰인다.

bigfoot ? 'grizzly bear' 
# bigfoot이 undefined이므로 'grizzly bear'가 대체 선택된다.

string = \boom if window?
# window가 있으면 string=\boom이다. 

document?.host # document가 있으면 .host를 액세스한다.

클래스와 타입

isinstanceof 는 해당 객체가 오른쪽의 타입의 인스턴스인지 검사한다. 이 때 우변은 리스트 일 수 있는데 리스트인 경우 나열된 타입 중 하나에 매칭하는지 검사한다.

typeof는 특정 객체의 타입을 조사한다. JS와 동일한데, typeof!로 쓰면 해당 타입의 이름을 얻을 수 있다.

now = new Date!
now isinstanceof Date # true
now isinstanceof [Date, Object] # true
# 이 때 타입명은 constructor이기도 하기 때문에 callable하다. 따라서 쉼표 넣어야 한다.

typeof /r/ # 'object'
typeof! /r/ # 'Regexp'

객체의 특정 키를 삭제하는 delete는 삭제된 키의 값을 리턴한다. JS의 delete는 삭제가 실제로 일어났는지 여부만 리턴하는데, 이는 delete!를 사용해야 한다.

프로퍼티 복사와 클론

<<< 은 우변에서 좌변으로 특정 객체의 고유 정보를 복사해준다. <<<<은 모든 프로퍼티를 복사한다. 객체를 클론하는 것은 ^^를 이용할 수 있다. 클로닝은 실제 값을 복사하는 것이 아니라, 동일한 프로토타입의 껍데기를 만든다. 따라서 프로토타입의 속성은 복사될 것이다. 아래는 객체의 사본을 만드는 것인데, 클로닝한 다음에 import해서 프로퍼티를 다시 복사 받아야 한다.

obj = { one: 1 }
obj2 = ^^obj <<< obj
obj2.two = 2
obj2 # {two: 2}

공식 홈페이지에서는 clone 하면 프로퍼티까지 복사되는 것으로 알고 있는데, 그렇지 않은 거 같다? 이 이슈는 깃헙에도 올라와 있는데, 공홈 내용을 안고치네…

반출과 반입

export 연산자는 모듈을 정의할 때 exports보다 편리하게 쓸 수 있다.

export func = ->
export value
export value-a, value-b, value-c
export
  a: 1
  b: -> 123

require!를 쓰면 외부 파일의 모듈을 반입할 수 있다.

LiveScript 살펴보기 – 01 : 기본 문법에 대해

LiveScript(이하LS)는 자바스크립트로 컴파일되는 스크립트 언어로 함수형 언어의 스타일과 기능을 많이 도입하여 간결하고 불필요한 보일러 플레이트를 최대한 배제한 코드를 작성할 수 있는 언어이다. 커피스크립트의 간접적인 방언이며 (창시자가 역시 커피스크립트를 만든 사람이다.) 객체 지향 및 절차 지향적인 코드를 작성함에 있어서도 많은 잇점을 누릴 수 있는 언어이다.
LiveScript 살펴보기 – 01 : 기본 문법에 대해 더보기

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