콘텐츠로 건너뛰기
Home » Mithril로 만드는 초간단 메모앱 – LiveScript

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>

 


<!doctype html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/0.2.3/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>

view raw

blog.html

hosted with ❤ by GitHub


class Post
(args) ->
@title = args?.title || ''
@content = args?.content || ''
@timestamp = args?.timestamp || new Date!get-time!
storage = do
save: (post) !-> local-storage.set-item post.timestamp, JSON.stringify post
load: (key) -> new Post JSON.parse local-storage.get-item key
writer = do
oninit: (vnode) ->
vnode.state = if k = m.route.param \key then post: storage.load k else post: new Post!
view: (vnode) ->
m \.writer,
* \title
m \input,
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
lister = do
view: (vnode) ->
m \.lister,
* m \ul, [0 til local-storage.length].map (i) ->
k = local-storage.key i
t = storage.load k .title
m \li, m "a", {href:"/view/#k", oncreate: m.route.link}, t
m \button, {onclick: !-> m.route.set \/edit/_}, \write
viewer = do
oninit: (vn) ->
if k = m.route.param \key then vn.state.post = storage.load
else m.route.set \/add/new
view: (vn) ->
post = vn.state.post
m \.wrapper,
* m \h1, post.title
m \.content post.content
m \button {onclick: !~> m.route.set "/edit/#{vn.state.post.timestamp}"}, \edit
m \button {onclick: !-> m.route.set \/}, \list
m.route document.body, \/, do
\/ : lister
'/edit/:key' : writer
'/view/:key' : viewer

view raw

blog.ls

hosted with ❤ by GitHub