Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Simply Lift : Chapter 6 - Wiring

원문 : http://simply.liftweb.net/index-Chapter-6.html#toc-Chapter-6

6장
와이어링(Wiring)

대화형 웹 애플리케이션은 단일 웹페이지상에 많은 의존성 컴포넌트들을 가지고 있습니다. 예를들면(그리고 이 예제는 이 챕터에서 사용할 예제입니다.) 애플리케이션에서 장바구니를 가지고 있을 것입니다. 장바구니는 아이템들과 갯수를 담고 있을 것입니다. 장바구니에서 아이템을 추가/삭제함으로써 장바구니는 소계(sub-total), 세금, 총계가 업데이트 될 것입니다. 게다가 장바구니에서아이템들의 갯수는 몇몇 페이지에서 장바구니의 내용없이 노출될 것입니다. 모든 여러가지 페이지 레이아웃을 위해서 이런 모든 의존성을 추적하는 것은 아주 어려운 작업입니다. 사이트를 수정해야할 때가 왔을 때 팀은 모든 아이템이 어디 있고 어떻게 그것들을 업데이트하는 지를 반드시 기억하고 있어야 하고 하나라도 잘못되면 사이트는 깨지게 됩니다.

Lift의 Wiring은 단일페이지와 다중탬에서 복잡한 의존성을 관리하는 간단한 해결책을 제공합니다. Lift의 Wiring은 셀(스프리드시트같은)간의 공식적인 관계를 선언하고  각 셀과 연관된 사용자인터페이스 컴포넌트(하나 이상의 컴포넌트가 될 수 있습니다.)를 선언하도록 합니다. Lift는 술어(predicates)의 변화에 기반하여 의존하는 사용자 인터페이스 컴포넌트들을 자동적으로 업데이트하게 됩니다.  Lift는 초기화 페이지가 렌더링될때와 Ajax 및 Comet으로 페이지가 업데이트될 때 이것을 수행할 것입니다. 바꿔 말하면, Wiring은 스프리드시트와 같고 페이지는 변화가 표시된 값을 바꾸는 것과 같이 어떤 술어(predicate) 값이 바뀌었을 때 자동적으로 업데이트 되게 됩니다.



6.1 셀(Cells)

스프리드싯트처럼 Lift의 Wiring는 셀(Cell)에 기반합니다. 셀들은 3개의 타입을 갖습니다: ValueCell, DynamicCell, FuncCell.

ValueCell는 사용자가 입력하거나 사용자의 어떤 액션에 따른 값을 담고 있습니다. ValueCell는 장바구니의 아이템이나 세금비율을 표시할 것입니다.

DynamicCell는 셀이 접근되어질때바다 변경되는 값을 담고 있습니다. 예를 들면 랜덤수나 현재시간입니다.

FuncCell은 값이나 다른 셀에 적용된 공식에 기반한 값을 가지고 있습니다.

이것을 보여주기 위한 약간의 코드를 보겠습니다:

val quantity = ValueCell(0)
val price = ValueCell(1d)
val total = price.lift(_ * quantity)

수량(quantity)와 가격(price)를 위한 2개의 ValueCell을 정의했습니다. 그 다음에 수량(quantity)으로 곱해지는 공식을 가진 가격(price)을 “lifting”함으로써 totoal을 정의했습니다.

scala> import net.liftweb._
import net.liftweb._

scala> import util._
import util._

scala> val quantity = ValueCell(0)
quantity: net.liftweb.util.ValueCell[Int] = ValueCell(0)

scala> val price = ValueCell(0d)
price: net.liftweb.util.ValueCell[Double] = ValueCell(0.0)

scala> val total = price.lift(_ * quantity)
total: net.liftweb.util.Cell[Double] = FuncCell1(ValueCell(0.0),<function1>)

scala> total.get
res1: Double = 0.0

scala> quantity.set(10)
res2: Int = 10

scala> price.set(0.5d)
res3: Double = 0.5

scala> total.get
res4: Double = 5.0

꽤 멋집니다. 셀들사이의 임의의 복잡한 관계를 정의할 수 있고 그것들은 어떻게 계산해야 하는지 스스로 알고 있습니다.



6.2 UI에 연결하기(Hooking it up to the UI)

이제 어떻게 유저인터페이스와 셀들의 값을 결합시키는지에 대한 셀들간에 관계를 선언할 수 있습니다.

아주 간단하게 보겠습니다.

"#total" #> WiringUI.asText(total)

id=”total”인 엘리먼트를 total의 값을 보여주는 함수와 연결했습니다. 여기 메서드 정의가 있습니다:

/**
 * 주어진 하나의 셀이 새로운 값으로 엘리먼트를 
 * 업데이트할 postPageJavaScript를 등록합니다.
 * 
 * @param cell 연관될 셀
 * 
 * @return NodeSeq를 변화시킬 함수
 * (이미 정의된 것이 없다면 id 속성은 추가될 것입니다.)
 */
def asText[T](cell: Cell[T]): NodeSeq => NodeSeq =

허? 이것은 많이 알수 없는 말들입니다. postPageJavaScript는 무엇입니까?

그래서 여기 WiringUI의 마법이 있습니다: 대부분의 웹프레임워크들은 페이지 랜더링을 시기에 맞는 이벤트로써 다룹니다. 아마도(해안(Seaside)의 경우에) 폼 서밋이 되돌려졌을때 페이지의 상태도 되돌려지는 것 같은 페이지 랜더링 상태를 덮어씌우는 렌더링에 대한 약간의 부작용(side effect)가 있습니다. Lift는 전체 HTML 페이지 렌더링과 페이지내에서 이어지는 Ajax요청을 싱글스코프를 가진 단일 이벤트로 다룹니다. 이것은 RequestVar들이 페이지렌더링이 이용가능하고 페이지내의 수반되는 Ajax요청동안에 장소를 차지하고 있다는 것을 의미합니다. 페이지 렌더링의 결과인 상태의 일부는 () => JsCmd의 버킷(bucket)이거나 JavaScript를 리턴하는 함수들의 컬랙션인 postPageJavaScript입니다. 페이지와 연관된 어떤 HTTP 요청에 대해서 응답하기 전에 Lift는 이러한 모든 함수들을 수행하고 결과 JavaScript를 브라우저로 보내는 응답에 추가합니다. 페이지에 연관된 HTTP요청은 최초 페이지 렝더링, 페이지와 연관된 수반되는 Ajax요청, 페이지에 의해서 생성된 연관 Comet(롱 폴링)요청을 포함합니다.

유저인터페이스와 연결한 각 셀에 대해서 Lift는 DOM 노드의 id(그리고 id가 없다면 LIft는 하나를 할당할 것입니다.)와 Cell의 현재값을 획득합니다. Lift는 현재 셀의 값을 바라보는 함수를 생성하고 값이 변경된다면 Lift는 Cell의 현재값과 함께 DOM 노드를 업데이트하는 JavaScript를 생성합니다.

그 결과는 Ajax 오퍼레이션이 ValueCell의 값을 변경한다면 모든 의존 셀들도 업데이트 할 것이고 연관된 업데이트된 DOM은 HTTP 응답과 함께 전해질 것입니다.

값을 표현하는 많은 컨트롤을 가지고 있습니다. asText메서드는 Text(cell.toString)을 만듭니다. 하지만 WiringUI,apply는 Cell의 타입 T를 NodeSeq로 변경하는 함수로 연결하도록 해줍니다. 게다가 브라우저에서 jsEffect(타입 시그니처 (String, Boolean, JsCmd) => JsCmd) 함께 변화(transition)를 제어할 수 있습니다. 내가 좋아하는 fade를 포함해서 jQuery에 기반한 jsEffect들이 미리 만들어져 있습니다.

/**
 * 기존값을 페이드아웃하고 jQuery fast fade를 사용해서
 * 새로운 값을 페이드인합니다.
 */
def fade: (String, Boolean, JsCmd) => JsCmd = {
   (id: String, first: Boolean, cmd: JsCmd) => {
      if (first) cmd
      else {
         val sel = "jQuery('#'+"+id.encJs+")"
         Run(sel+".fadeOut('fast', function() {"+cmd.toJsCmd+" "+sel+".fadeIn('fast');})")
      }
   }
}

다음과 같이 사용할 수 있습니다:

"#total" #> WiringUI.asText(total, JqWiringSupport.fade)

이제 total필드가 업데이트되었을때 기존 값은 페이드아앗되고 새로운 값은 페이드인 될 것입니다.



6.3 공유된 쇼핑(Shared Shopping)

실제 코드예제를 보겠습니다.  Shop with Me 소스에서 이 코드를 찾을 수 있습니다.

예제는 간단한 쇼핑사이트입니다. 당신이 볼수 있는 아이템들이 있고 장바구니가 있습니다. 장바구니에 아이템을 추가할 수 있고 다중탭이나 브라우저 윈도우에서 장바구니를 본다면 장바구니가 변경되었을 때 모든 탭과 윈도우내의 장바구니는 업데이트될 것입니다. 게다가 다른 누군가와 당신의 장바구니를 공유할 수 있고 장바구니에 어떤 변화가 생겼을때 같은 장바구니를 공유하고 있는 모든 다른 브라우저에 전파될 것입니다.

데이터 모델은  REST 챕터(5.2장 참고)에서 사용했던 것과 동일합니다.

장바구니 정의를 보겠습니다.

리스팅 6.1: Cart.scala

package code
package lib

import model.Item

import net.liftweb._
import util._

/**
 * 장바구니(shopping cart)
 */
class Cart {
   /**
    * 장바구니의 내용
    */
   val contents = ValueCell[Vector[CartItem]](Vector())

   /**
    * 소계(subtotal)
    */
   val subtotal = contents.lift(_.foldLeft(zero)(_ + _.qMult(_.price)))

   /**
    * 세금이 붙은 소계(taxable subtotal)
    */
   val taxableSubtotal = contents.lift(_.filter(_.taxable).
                              foldLeft(zero)(_ + _.qMult(_.price)))

   /**
    * 현제 세율
    */
   val taxRate = ValueCell(BigDecimal("0.07"))

   /**
    * 계산된 세금
    */
   val tax = taxableSubtotal.lift(taxRate)(_ * _)

   /**
    * 합계
    */
   val total = subtotal.lift(tax)(_ + _)

   /**
    * 장바구니의 무게
    */
   val weight = contents.lift(_.foldLeft(zero)(_ + _.qMult(_.weightInGrams)))

   // 헬퍼 메서드

   /**
    * 괜찮은 제로(0) 상수
    */
   def zero = BigDecimal(0)

   /**
    * 장바구니에 아이템을 추가합니다.
    * 히미 장바구니에 있다면 수량을 증가시킵니다.
    */
   def addItem(item: Item) {
      contents.atomicUpdate(v => v.find(_.item == item) match {
         case Some(ci) => v.map(ci => ci.copy(qnty = ci.qnty +
                                       (if (ci.item == item) 1 else 0)))
         case _ => v :+ CartItem(item, 1)
      })
   }

   /**
    * 아이템 수량 설정. 0이거나 음수면 삭제합니다
    */
   def setItemCnt(item: Item, qnty: Int) {
      if (qnty <= 0) removeItem(item)
      else contents.atomicUpdate(v => v.find(_.item == item) match {
         case Some(ci) => v.map(ci => ci.copy(qnty =
                                       (if (ci.item == item) qnty
                                       else ci.qnty)))
         case _ => v :+ CartItem(item, qnty)
      })
   }

   /**
    * 장바구니에서 아이템을 삭제합니다.
    */
   def removeItem(item: Item) {
      contents.atomicUpdate(_.filterNot(_.item == item))
   }
}

/**
 * 장바구니내의 한 아이템
 */
case class CartItem(item: Item, qnty: Int, id: String = Helpers.nextFuncName) {

   /**
    * 담고있는 아이템에서 수량과 어떤 계산을 곱한 계산
    * (예를들면 무게를 가지는 것)
    */
   def qMult(f: Item => BigDecimal): BigDecimal = f(item) * qnty
}

/**
 * CartItem 컴페니언 오브젝트(CartItem companion object)
 */
object CartItem {
   implicit def cartItemToItem(in: CartItem): Item = in.item
}

아주 직관적으로 보입니다. 장바구내 내용물과 세율의 2개의 ValuCell을 갖습니다. 계산된 셀들을 가집니다. Cart클래스 정의의 하단에 장바구니의 내용물을 추가하고 삭제하고 수정하도록 하는 헬퍼메서드들이 있습니다. 또한 Item과 qnty(수량)을 담고 있는 CartItem 케이스 클래스를 정의합니다.

지금까지는 좋았습니다. 이제 모든 아이템을 보여주는 방법을 보겠습니다:

리스팅 6.2: AllItemsPage.scala

package code
package snippet

import model.Item
import comet._

import net.liftweb._
import http._
import sitemap._
import util._
import Helpers._

object AllItemsPage {
   // 모든 아이템을 보여줄 페이지를 위한 메뉴아이템 정의
   lazy val menu = Menu.i("Items") / "item" >> Loc.Snippet("Items", render)

   // 아이템들 보여주기
   def render = "tbody *" #> renderItems(Item.inventoryItems)

   // 아이템들의 리스트를 위해서 이러한 아이텝들을 보여줌
   def renderItems(in: Seq[Item]) =
      "tr" #> in.map(item => {
         "a *" #> item.name &
         "a [href]" #> AnItemPage.menu.calcHref(item) &
         "@description *" #> item.description &
         "@price *" #> item.price.toString &
         "@add_to_cart [onclick]" #>
         SHtml.ajaxInvoke(() => TheCart.addItem(item))}
}

SiteMap 엔트리를 정의합니다:

lazy val menu = Menu.i("Items") / "item" >> Loc.Snippet("Items", render)

그래서 사용자가 /item을 브라우징했을 때 인벤토리에 있는 모든 아이템이 보여집니다. Item들을 보여주기 위한 템플릿은 다음과 같습니다:

리스팅 6.3: items.html

<table class="lift:Items">
   <tbody>
      <tr>
         <td name="name"><a href="#">Name</a></td>
         <td name="description">Desc</td>
         <td name="price">$50.00</td>
         <td><button name="add_to_cart">Add to Cart</button></td>
      </tr>
   </tbody>
</table>

다음으로 Item을 보여주기 위한 코드를 보겠습니다:

리스팅 6.4: AnItemPage.scala

package code
package snippet

import model.Item
import comet._

import net.liftweb._
import util._
import Helpers._
import http._
import sitemap._

import scala.xml.Text

object AnItemPage {
   // 파라미터화된(parameterized) 페이지 만듭니다
   def menu = Menu.param[Item]("Item", Loc.LinkText(i => Text(i.name)),
                           Item.find _, _.id) / "item" / *
   }

class AnItemPage(item: Item) {
   def render = "@name *" #> item.name &
   "@description *" #> item.description &
   "@price *" #> item.price.toString &
   "@add_to_cart [onclick]" #> SHtml.ajaxInvoke(() => TheCart.addItem(item))
}

이것은 사용자가 /item/1234에 갔을때 무슨일이 발생하는지를 정의합니다. 이것은 다른 대부분의 Lift 코드들보다 더 “컨트롤러스럽(controller-like)”습니다. 메뉴 아이템 정의를 보겠습니다:

def menu = Menu.param[Item]("Item", Loc.LinkText(i => Text(i.name)),
                        Item.find _, _.id) / "item" / *

파라미터화된 Menu 엔트리를 정의하고 있습니다. 파라미터 타입은 Item입니다. 이것은 페이지가 Item을 보여주고 요청에 기반해서 Item을 계산할수 있어야 된다는 것을 의미합니다.

“Item”은 메뉴엔트리의 이름입니다.

Loc.LinkText(i => Text(i.name))는 아이템을 취하고 메뉴엔트리를 위해 보여줄 텍스트를 생성합니다.

Item.find _ 는 String을 취하고 그것을 Box[Item]으로 변환하는 함수입니다. 우리가 관심을 가지고 있는 요청의 파라미터에 기반해서 Item을 검색합니다.

_.id는 Item을 취하고 Item페이지를 표현하는 URL을 어떻게 만드는지 표현하기 위한 String을 리턴하는 함수(Item => String)입니다. 이것은 Item을 보여줄 페이지를 위해 Item을 HREF로 변환하려고 a [href]" #> AnItemPage.menu.calcHref(item)에 의해서 사용됩니다.

마지막으로 URL은 거의 무엇처럼 보이는가인 / “item” /*에 의해서 정의됩니다. 폼 /item/xxx의 들어오는 요청을 매칭할 것이고 xxx는 URL과 연관된 Item을 결정하기 위해서 String => Box[Item] 함수에 전달됩니다.

그래서 모든 아이템을 보여줄 수 있습니다. 모든 아이템에서 단일 아이템으로 탐색합니다. 각 아이템은 장바구니에 추가하도록 하는 버튼을 가지고 있습니다. Item은 다음 코드로 장바구니에 추가됩니다: SHtml.ajaxInvoke(() => TheCart.addItem(item))}). TheCart.addItem(item)은 장바구니가 변경되었을때 업데이트되기 위해서 무엇이 필요한가에 대한 관심없이도 애플리케이션 어느 곳에서든 호출될 수 있습니다.

어떻게 장바구니가 보여지고 관리되는지 보겠습니다:

리스팅 6.5: CometCart.scala

package code
package comet

import lib._

import net.liftweb._
import common._
import http._
import util._
import js._
import js.jquery._
import JsCmds._
import scala.xml.NodeSeq
import Helpers._

/**
 * 이 세션을 위한 현재 장바구니는 어떤것입니까
 */
object TheCart extends SessionVar(new Cart())

/**
 * CometCart는 장바구니를 보여주는 CometActor입니다
 */
class CometCart extends CometActor {
   // 우리의 현재 장바구니
   private var cart = TheCart.get

   /**
    * 당신 스스로 그리세요
    */
   def render = {
      "#contents" #> (
         "tbody" #>
         Helpers.findOrCreateId(id => // tbody가 id를 가지고 있다는걸 확인합니다
            // 장바구니 내용이 수정되었을 때
            WiringUI.history(cart.contents) {
               (old, nw, ns) => {
                  // 템플릿의 tr부분을 획득합니다
                  val theTR = ("tr ^^" #> "**")(ns)

                  def ciToId(ci: CartItem): String = ci.id + "_" + ci.qnty

                  // 장바구니 아이템의 열(row)를 만듭니다
                  def html(ci: CartItem): NodeSeq = {
                     ("tr [id]" #> ciToId(ci) &
                     "@name *" #> ci.name &
                     "@qnty *" #> SHtml.
                     ajaxText(ci.qnty.toString,
                           s => {
                              TheCart.
                              setItemCnt(ci, Helpers.toInt(s))
                           }, "style" -> "width: 20px;") &
                     "@del [onclick]" #> SHtml.
                     ajaxInvoke(() => TheCart.removeItem(ci)))(theTR)
                  }

                  // 리스트들 사이의 델타(delta)를 계산하고
                  // 델타에 기반해서 디스플레이를 업데이트하기 위해
                  // 현재 jQuery를 발생시킵니다
                  JqWiringSupport.calculateDeltas(old, nw, id)(ciToId _, html _)
               }
            })) &
      "#subtotal" #> WiringUI.asText(cart.subtotal) & // 소계를 보여줍니다
      "#tax" #> WiringUI.asText(cart.tax) & // 세금을 보여줍니다
      "#total" #> WiringUI.asText(cart.total) // 합계를 보여줍니다
   }

   /**
    * 외부 소스로부터 메시지를 처리합니다
    */
   override def lowPriority = {
      // 누군가 우리에게 새로운 장바구니를 보낸다면
      case SetNewCart(newCart) => {
         // 기존의 장바구니는 등록취소합니다
         unregisterFromAllDepenencies()

         // postPageJavaScript로부터 
         // 기존의 장바구니에 대한 의존성을 모두 삭제합니다
         theSession.clearPostPageJavaScriptForThisPage()

         // 새로운 장바구니를 설정합니다
         cart = newCart

         // 고쳐진 렌더링 조각들을 포함해서 전체 reRender를 수행합니다
         reRender(true)
      }
   }
}

/**
 * CometCart를 위한 새로운 장바구니를 설정합니다
 */
case class SetNewCart(cart: Cart)

코드를 간단히 보겠습니다:

object TheCart extends SessionVar(new Cart())

장바구니를 보관하기 위한 SessionVar를 정의합니다.
우리의 CometActor는 SessionVar로부터 현재의 장바구니를 획득합니다.

class CometCart extends CometActor {
   // 우리의 현재 장바구니
   private var cart = TheCart.get

다음으로 어떻게 cart.total을 그리는지 보겠습니다:

"#total" #> WiringUI.asText(cart.total) // 합계를 보여줍니다

이것은 그저 그것이 그렇게 되어야 하는 방법입니다.

변화에 기반해서 장바구니의 내용을 어떻게 그리거나 다시그리는지 그리고 장바구니로부터 아이템을 추가하거나 삭제하려고 브라우저 DOM을 다룰 JavaScript만을 어떻게 보내는지에 대한 근사한 조각을 보겠습니다.

"#contents" #> (
   "tbody" #>
   Helpers.findOrCreateId(id => // tbody가 id를 가지고 있다는걸 확인합니다
      // 장바구니 내용이 수정되었을 때
      WiringUI.history(cart.contents) {
         (old, nw, ns) => {
            // 템플릿의 tr부분을 획득합니다
            val theTR = ("tr ^^" #> "**")(ns)

            def ciToId(ci: CartItem): String = ci.id + "_" + ci.qnty

            // 장바구니 아이템의 열(row)를 만듭니다
            def html(ci: CartItem): NodeSeq = {
               ("tr [id]" #> ciToId(ci) &
               "@name *" #> ci.name &
               "@qnty *" #> SHtml.
               ajaxText(ci.qnty.toString,
                     s => {
                        TheCart.
                        setItemCnt(ci, Helpers.toInt(s))
                     }, "style" -> "width: 20px;") &
               "@del [onclick]" #> SHtml.
               ajaxInvoke(() => TheCart.removeItem(ci)))(theTR)
            }

            // 리스트들 사이의 델타(delta)를 계산하고
            // 델타에 기반해서 디스플레이를 업데이트하기 위해
            // 현재 jQuery를 발생시킵니다
            JqWiringSupport.calculateDeltas(old, nw, id)(ciToId _, html _)
         }
      }))

우선 <tbody> 엘리먼트의 id를 알고있는지 확인합니다: "tbody" #> Helpers.findOrCreateId(id =>

다음으로 컨텐츠가 변경되었을때 기존값(old)과 새로운 값(nw)과 기억된 NodeSeq(렌더링을 수행하기 위해서 템플릿이 사용합니다.)를 갖을수 있도록 CometCart를 cart.contents에 연결(wire up)합니다: WiringUI.history(cart.contents) { (old, nw, ns) => {

tehTR변수에서 <tr>엘리먼트와 연관된 템플릿의 일부를 획득합니다: val theTR = ("tr ^^" #> "**")(ns)

CartItem에 기반해서 CartItem을 표현하는 DOM 노드를 위한 안정된 id를 리턴합니다:

html메서드는 CartItem을 수량을 변경하고 장바구니에서 아이템을 삭제하기 위해서 Ajax제어를 포함하는 NodeSeq로 변환합니다.

마지막으로 CartItem의 기존리스트와 새로운 리스트사이의 델타에 기반해서 적절한 DOM엘리먼트를 추가하고 삭제함으로써 DOM을 조작할 JavaScript를 생성합니다: JqWiringSupport.calculateDeltas(old, nw, id)(ciToId _, html _)

다음으로 장바구니를 어떻게 변경하는지 보겠습니다. 2개의 브라우저 세션사이에서 장바구니를 공유하기를 원한다면... 2명이 자신의 브라우저에서 쇼핑하지만 하나의 장바구니에 물건들을 둘 때 장바구니를 바꿀 방법이 필요합니다. ComtetCart에 SetNewCart 메시지를 처리합니다:

// 누군가 우리에게 새로운 장바구니를 보낸다면
case SetNewCart(newCart) => {
   // 기존의 장바구니는 등록취소합니다
   unregisterFromAllDepenencies()

   // postPageJavaScript로부터 
   // 기존의 장바구니에 대한 의존성을 모두 삭제합니다
   theSession.clearPostPageJavaScriptForThisPage()

   // 새로운 장바구니를 설정합니다
   cart = newCart

   // 고쳐진 렌더링 조각들을 포함해서 전체 reRender를 수행합니다
   reRender(true)
}

위의 코드에서 어떻게 와이어링(Wiring)이 Lift의 Comet지원과 상호작용하는지에 대한 힌트가 있는 2개의 라인이 있습니다: unregisterFromAllDepenencies()와 theSession.clearPostPageJavaScriptForThisPage()

CometActor가 WiringUI에 있는 어떤 것에 의존할 때 Lift는 Cell과 CometActor사이의 약한 참조를 생성합니다. Cell이 값을 변경했을 때 CometActor를 찌릅니다. 그 다음 CometActor는 Cell을 변경하는 것과 연관된 브라우저 스크린의 실제 영역을 업데이트합니다. unregisterFromAllDepenencies()는 Cell로부터 CometActor의 연결을 끊습니다. theSession.clearPostPageJavaScriptForThisPage()는 CometActor와 연관된 모든 postPageJavaScript를 제거합니다. CometActor가 단일페이지과 연관되지 않았지만 많은 페이지에서 나타날 수 있기 때문에 그것은 자신의 postPageJavaScript 컨텍스트를 갖습니다.

퍼즐의 마지막 조각은 세션들 사이에서 Cart를 어떻게 공유하는가 입니다. UI관점에서 사용자가 “Share Cart”버튼을 눌렀을때 어떻게 모달 다이얼로그를 보여주는지가 여기 있습니다:

리스팅 6.6: Link.scala

package code
package snippet

import model._
import comet._
import lib._

import net.liftweb._
import http._
import util.Helpers._
import js._
import JsCmds._
import js.jquery.JqJsCmds._

class Link {
   // _share_link.html템플릿에 기반해서 모달 다이얼로그를 오픈합니다
   def request = "* [onclick]" #> SHtml.ajaxInvoke(() => {
      (for {
         template <- TemplateFinder.findAnyTemplate(List("_share_link"))
      } yield ModalDialog(template)) openOr Noop
   })

   // 모달 다이얼로그를 닫습니다
   def close = "* [onclick]" #> SHtml.ajaxInvoke(() => Unblock)

   // 공유를 위해서 href와 link를 생성합니다
   def generate = {
      val s = ShareCart.generateLink(TheCart)
      "a [href]" #> s & "a *" #> s
   }
}

기본적으로 ShareCart 오브젝트에 의해서 생성된 링크를 담고있는 다이얼로그를 두기위해서 jQuery의 ModalDialog 플러그인을 사용합니다. ShareCart.scala를 보겠습니다:

리스팅 6.7: ShareCart.scala

package code
package lib

import comet._

import net.liftweb._
import common._
import http._
import rest.RestHelper
import util._
import Helpers._

// RestHelper입니다
object ShareCart extends RestHelper {
   // private 상태
   private var carts: Map[String, (Long, Cart)] = Map()

   // 주어진 장바구니, 유일한 공유코드를 생성합니다
   def codeForCart(cart: Cart): String = synchronized {
      val ret = Helpers.randomString(12)

      carts += ret -> (10.minutes.later.millis -> cart)

      ret
   }

   /**
    * 이 장바구니에 올바른 링크를 생성합니다
    */
   def generateLink(cart: Cart): String = {
      S.hostAndPath + "/co_shop/"+codeForCart(cart)
   }

   // 가능하다면 String을 Cart로 변환하는 추출자(extractor)
   def unapply(code: String): Option[Cart] = synchronized {
      carts.get(code).map(_._2)
   }

   // 10분이상이 지난 모든 cart는 삭제합니다
   private def cleanup() {
      val now = Helpers.millis
      synchronized{
         carts = carts.filter{
            case (_, (time, _)) => time > now
         }
      }
      Schedule.schedule(() => cleanup(), 5 seconds)
   }

   // 5마다 청소합니다
   cleanup()

   // the REST part of the code
   serve {
      // 들어오는 URL을 매치합니다
      case "co_shop" :: ShareCart(cart) :: Nil Get _ => {
         // cart를 설정합니다
         TheCart.set(cart)

         // SetNewCart 메시지를 CometCart에 보냅니다
         S.session.foreach(_.sendCometActorMessage("CometCart", Empty, SetNewCart(cart)))

         // 브라우저를 /로 리다이렉트합니다
         RedirectResponse("/")
      }
   }
}

코드는 랜덤 ID들과 Cart들사이의 연락을 관리합니다. 사용자가 /co_shop/share_cart_id를 브라우징한다면 ShareCart는 공유된 Cart에 TheCart를 설정할 것이고 세션과 연관된 CometCart 인스턴스에 SetNewCart메시지를 보낼 것입니다.



6.4 결론

이번 장에서 Lift의 Wiring이 값들간의 복잡한 상호적인 관계를 만들고 웹 유저인터페이스에서 그러한 관계를 표면화시키기 위해서 어떻게 사용될 수 있는지를 보았습니다. Wiring은 Ajax나 Comet과 함께 사용될 수 있습니다. Wiring는 사용자 친화적이고 유지보수가 쉬운 복잡한 웹페이지들을 쉽게 만들수 있도록 해줍니다.
2011/04/12 02:40 2011/04/12 02:40