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 : 기본 문법에 대해 더보기

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() – 가상노드와 컴포넌트 – Mithril

가상노드

Mithril은 기본적으로 가상 DOM 노드를 생성하고 이를 실제 웹페이지 내의 DOM에 마운트하거나, 특정 DOM내부에 가상 노드를 렌더링하는 일을 수행하는 것으로 UI를 구성한다. m()은 가상 노드를 표현하는 함수이다. 기본적인 모양은 다음과 같다.

m( selector, attributes, children )
  • selector : 특정 노드를 생성하기 위한 태그명 혹은 CSS 셀렉터이다. 클래스나 아이디를 사용하면 기본적으로 div 태그로 표현된다.
  • attributes : 키-값쌍으로 정의되는 객체이며, 해당 노드의 속성을 지정할 수 있다.
  • children : 해당 노드의 자식노드가 된다. 자식노드에 들어올 수 있는 것은 다음의 타입이 있다.
    • 텍스트 값 : 해당 노드의 텍스트 노드가 된다.
    • m()으로 생성한 단일 가상노드 : 해당 노드의 직속 하위 DOM 노드
    • 가상노드의 배열 : 여러 DOM 노드가 하위에 들어가게 된다.  하위 노드에 m() 대신 문자열이 들어가면 div 객체로 대체된다.

다음은 라이브 스크립트에서 m()을 사용해서 DOM을 표현하는 몇 가지 예이다.

#livescript


m \div {id: \box} \hello
# 가장 표준적인 폼
# <div id="box">hello</div>

m \h2.title \hello
# CSS셀렉터를 이용해서 id, class 속성을 넣을 수 있다.
# <h2 class="title">hello</h2>

m 'input[type=checkbox]' {checked: on}
# 체크된 체크 박스

# ul > li * 3 의 목록
m \ul, 
* m \li \hello
  m \li \world
  m \li \foobar

이렇게 만들어진 가상노드는 m.render() 함수를 통해서 특정 DOM 내에 렌더링할 수 있다. (m.mount()를 사용하기 위해서는 단순 가상 노드가 아닌 컴포넌트가 만들어져야 한다.)

m.render document.body, do
  m \.wrapper, m \ul, [1, 2, 3].map -> m \li it

컴포넌트

컴포넌트는 가상노드와 로직을 하나로 감싼 덩어리이다. 컴포넌트는 실제로 동작하는 앱을 구축하기 위한 가장 기본이 되는 단위이기도 하다. 유효한 컴포넌트는 view()라는 메소드를 가지고 있는 어떠한 객체도 될 수 있다. 컴포넌트를 가상 노드로 변환하기 위해서는 m() 함수에 인자로 전달하면 된다.

다음은 공식 문서에 소개되는 간단한 앱의 원형이다. m() 함수로 특정 컴포넌트를 전달할 때, 해당 컴포넌트의 속성값과 자식 노드를 추가 인자로 전달할 수 있다.  컴포넌트의 view() 함수는 vnode 라는 단일 인자를 받는데, m 함수에 전달되는 추가 인자는 각각 이 vnode의 attrs, children 이 된다.

Greeter = do
  view: (vnode) ->
    m \div, vnode.attrs, [\hello, vnode.children]

m.render document.body,
  m Greeter, {style: {color:\red}} \world
# <div style="color:red;">helloworld</div> 로 렌더링된다.

컴포넌트의 라이프사이클 메소드

각 컴포넌트는 view외에도 여러 라이프사이클 메소드를 갖는다. 이 메소드들은 가상 노드가 생성되거나 변경될 때 각각의 이벤트 핸들러처럼 호출된다.

메소드 설명
oninit(vnode) vnode가 DOM 트리에 렌더링 되기 직전에 호출된다.
oncreate(vnode) vnode가 DOM 트리에 추가될 때 호출된다.
onupdate(vnode) 마운트된 가상노드가 다시 그려지기 전에 호출된다.
onbeforeremove(vnode) 가상노드가 제거되기 직전 호출된다.
onrevmoe(vnode) 가상노드가 제거되기 전에 호출된다.
onbeforeupdate(vnode, old) onupdate() 호출 전에 호출된다. 만약 이 메소드가 false를 리턴하면 다시 그리기가 수행되지 않는다.

가상노드의 상태

Mithril은 1.0으로 업그레이드되면서 가상노드에서 컨트롤러 레벨을 완전히 분리했다. 따라서 데이터 모델은 컴포넌트 속으로 넣기보다는 가급적 외부 객체를 사용하는 것이 권장된다. 이전버전에서는 view 함수가 controller를 인자로 받아서 컨트롤러 내의 상태값을 참조하거나 메소드를 호출했는데, 이 패턴을 그대로 가져오려면 oninit에서도 vnode인자를 받고, 이 vnode가 그대로 view()에 전달된다는 점을 이용해서 다음과 같이 만들 수 있다.

## v.0.2.x
## 컨트롤러를 이용해서 app 컴포넌트 내에
## 상태를 저장하는 list라는 프로퍼티를 정의한다.
app = do
  controller: !->
    @list = []
  view: (ctrl) ->
    m \ul, ctrl.list.map -> m \li it.value

## v.1.0+
## oninit()을 통해서 vnode의 state에 리스트를 정의했다.
app = do
  oninit: (vnode) !->
    vnode.state.list = []
  view: (vnode) ->
    m \ul, vnode.list.map -> m \li it.value

## recommended
## 가장 권장되는 방식은 컴포넌트와 데이터모델을 완전히 분리하는 것이다.
Data = { list: [] }

app = do
  view: (vnode) ->
    m \ul, Data.list.map -> m \li it.value

다만, vnode.state를 과도하게 쓰는 것은 그리 권장되지 않는다. 특히 메소드를 정의하는 경우 this의 처리가 매우 곤란해지기 때문이다. 아예 외부 객체로 정의하는 것이 가장 정신건강에 이롭다.

data = do
  list: []
  desc: ''
  add: !~>
    if @desc.length > 0 then
      @list.push new Item @desc
      @desc = ''
## add 메소드는 바인드되어 있으므로 외부에서 이벤트핸들러로 바로 전달가능하다.

관련 글 목차

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

  1. 물론 mithril은 내부적으로 캐시를 사용하며, 변경된 부분만 새로 그리는 등의 성능 향상을 위한 내부적인 장치들을 가지고 있다.