Outsider's Dev Story

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

Simply Lift : Chapter 3 - Snippets and SiteMap #2

3.3 뷰 먼저(View First)

한 번 접근제어가 사이트맵에 의해서 권한을 획득하면 Lift는 URL에 연관된 뷰를 로드합니다. Lift가 뷰를 위한 경로를 처리하는데 사용하는 많은 메카니즘이 있지만 가장 간단한 것은 URL 경로와 /src/main/webapp에 있는 파일간의 일대일 매핑입니다. URL이 /index이면 Lift는 /src/main/webapp/index.html의 로컬라이징된 버전을 찾을 것입니다. Lift가 일단 템플릿을 로드하면 Lift는 그것을 입력된 URL에 대한 응답으로 리턴되기를 원하는 동적컨텐츠로 바꿀 것입니다.



3.3.1 페이지 소스

페이지 소스를 보겠습니다.

리스팅 3.2: index.html

<!DOCTYPE html>
<html>
    <head>
        <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
        <title>Home</title>
    </head>
    <body class="lift:content_id=main">
        <div id="main" class="lift:surround?with=default&at=content">
            <div>Hello World. Welcome to your Lift application.</div>
            <div>Check out a page with <a href="/param/foo">query parameters</a>.</div>

            <span class="lift:embed?what=_embedme">
                replaced with embedded content
            </span>

            <div>
                <ul>
                    <li>Recursive: <a href="/recurse/one">First snippet</a></li>
                    <li>Recursive: <a href="/recurse/two">Second snippet</a></li>
                    <li>Recursive: <a href="/recurse/both">Both snippets</a></li>
                </ul>
            </div>
        </div>
    </body>
</html>

브라우저에서 페이지를 열면 아래처럼 보일것입니다.



3.3.2 동적 컨텐츠

템플릿은 유효한 HTML페이지입니다. 하지만 Lift가 어떻게 HTML을 인터프리팅하는지 말해주는 마커가 페이지에 있습니다.

<body> 태그는 lift:content_id=xxxx라는 class속성을 가지고 있다면 Lift는 id 매칭을 통해서 엘리먼트를 찾고 페이지를 렌더링하기 위한 시작점으로 사용할 것입니다. 이것은 디자이너가 애플리케이션에서 사용하는 동일한 계층에서 페이지를 수정하고 유지보수할 수 있게 해줍니다.



3.3.3  감싸기(Surround)과 페이지 크롬(page chrome)

템플릿은 다음 코드로 처리를 시작합니다:

<div id="main" class="lift:surround?with=default&at=content">

class 속성의 lift:surround?with=default;at=content는 Lift가 현제 엘리먼트를 default.html이라는 이름(일반적으로 /templates-hidden/ 디렉토리에 위치합니다.)의 템플릿으로 감싸고(surround) “content” id를 가진 엘리먼트에 현제 페이지의 컨텐츠를 배치하도록 지시합니다.

이 패턴은 사이트에서 모든 페이지 주위에 공통적인 크롬을 감싸도록 해줍니다. 또한 감싸기를 위해서 사용할 다른 템플릿을 명시할 수 있습니다. 게다가 템플릿 스스로가 감싸기를 위해서 사용할 다른 템플릿은 선택할 수 있습니다.



3.3.4 내장하기(Embed)

게다가 크롬으로 페이지를 감싸려고 다른 파일을 내장할 수 있습니다. 예를 들면 특정한 페이지에 내장된 쇼핑카트 컴포넌트를 가질 수 있습니다. 다음 코드로 내장합니다.

<span class="lift:embed?what=_embedme">
    replaced with embedded content
</span>

다시 한번 커맨드는 lift:로 시작하는 class속성으로  신호를 보냅니다. 이 경우에 _embedme.html파일에서 템플릿을 내장합니다.



3.3.5 결과

동적으로 페이지를 생성한 결과는 다음처럼 보입니다.

사용자 삽입 이미지



3.4 스니펫과 동적 컨텐츠

Lift 템플릿은 실행가능한 코드를 담고 있습니다. 이것은 순수하고, 가공하지 않은 유효한 HTML입니다.

Lift는 HTML페이지의 영역들을 정적에서 동적으로 변형하기 위해서 스니펫을 사용합니다. 키워드는 변형(transform)입니다.

Lift 의 스니펫은 스칼라 함수: NodeSeq => NodeSeq 입니다. NodeSeq는 XML노드의 컬렉션입니다. 스니펫은 오직 인풋 NodeSeq를 아웃풋NodeSeq로 바꿀 수 있습니다. 자 정확하지는 않지만 스니펫은 아마 쿠키를 셋팅하거나 데이터베이스 트랙젝션등을 포함하는 사이드 이펙트를 가질것입니다. 그러나 핵심 변형 개념(core transformation concept)는 중요합니다. 먼저 스니펫을 기능적으로 페이지의 분리된 부분으로 고립화합니다. 이것은 각 스니펫, 각 NodeSeq => NodeSeq,이 컴포넌트라는 것을 의미합니다.  둘째로 이것은 페이지가 제귀적으로 만들어지지만 항상 유효한 HTML을 유지한다는 것을 의미합니다. 이것은 개발자가 크로스 사이트 스크립팅 취약점을 가지기 어렵게 만듭니다. 셋째로 디자이너들은 HTML페이지를 디자인하기 위해서 프로그램에 대해서 배울 걱정을 할 필요가 없습니다. 왜냐하면 프로그램실행이 HTML에 내장되기 보다는 HTML로부터 별도로 추상화되기 때문입니다.



3.4.1 마크업(markup)안의 스니펫

콘 텐츠가 동적이라는 것을 나타내기 위해서 마크업은 스니펫 호출(invocation)을 가지고 있습니다.  일반적으로 class="someclass someothercss lift:mysnippet"의 형태를 가집니다.  class 속성이 lift:xxx를 가지고 있으면 xxx는 스니펫으로 처리될 것입니다. 스니펫은 속성을 취할 것입니다. 속성들은 URL 파라미터처럼(a?로 시작되고 ?로 구분된 name=value가 나오는) 인코딩될 것입니다. name과 value는 URL 인코딩이 됩니다.

또한 XML태그와 함께 스니펫을 호출할 것입니다.

<lift:my_snippet cat="foo">
    <div>xxxx</div>
</lift:my_snippet>

HTML5 파서는 모든 태그를 소문자로 강제하기 때문에 <lift:MySnipet>는 <lift:mysnippet>가 될 것입니다.

Lift 2.3은 또한 <div l="mysnippet?param=value">xxx</div>형식으로 스니펫을 호출할수 있게 될 것입니다.

스니펫을 호출하는 뒤에 2가지 메카니즘은 유효한 HTML5 템플릿의 결과가 아닐 것입니다.



3.4.2 스니펫 결정(Snippet resolution)

Lift 는 스니펫 이름에서 NodeSeq => NodeSeq를 처리하기 위해서 아주 복잡한 규칙 세트를 가지고 있습니다.(23.1 참고) 가장 간단한 메카니즘은 스니펫이름과 매칭되는 class나 object를 snippet 팩키지안에 갖는 것입니다.

그래서 lift:HelloWorld는 code.snippet.HelloWorld 클래스를 찾고 render메서드를 호출할 것 입니다.

lift:CatFood.fruitbat은 code.snippet.CatFood 클래스를 찾고 fruitbat 메서드를 호출할 것 입니다.



3.4.3 동적인 예제

dynamic.html 페이지를 보겠습니다.

리스팅 3.3 : dynamic.html

<!DOCTYPE html>
<html>
    <head>
        <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
        <title>Dynamic</title>
    </head>
    <body class="lift:content_id=main">
        <div id="main" class="lift:surround?with=default;at=content">
            This page has dynamic content.
            The current time is <span class="lift:HelloWorld">now</span>.
        </div>
    </body>
</html>

이 템플릿은 HelloWorld.scala에 정의된 HelloWorld 스니펫을 호출합니다.

리스팅 3.4: HelloWorld.scala

package code
package snippet

import lib._

import net.liftweb._
import util.Helpers._
import common._
import java.util.Date

class HelloWorld {
    lazy val date: Box[Date] = DependencyFactory.inject[Date] // 날짜 주입

    def render = "* *" #> date.map(_.toString)
}

그리고 동적인 컨텐츠는 다음과 같이 됩니다.

<span>Thu Dec 30 16:31:13 PST 2010</span>

HelloWorld 스니펫 코드는 간단합니다.

lazy val date: Box[Date] = DependencyFactory.inject[Date]

Date 인스턴스를 얻기 뒤해서 의존성 주입(dependency injection, 8.2 참조)을 사용합니다.
그다음에는

def render = "* *" #> date.map(_.toString)

마크업에 주입된 Date의 String값을 인서트하는 CSS Selector Transform을 생성합니다. 이 경우에는 스니펫을 호출하는 <span>입니다.



3.4.4 내장된 예제

<div class="lift:embed?what=_embedme">xxx</div>를 사용하는 템플릿을 어떻게 내장할 수 있는지 보았습니다.

이제 _embedme.html 템플릿을 보겠습니다.

<!DOCTYPE html>
<html>
    <head>
        <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
        <title>I'm embeded</title>
    </head>
    <body class="lift:content_id=main">
        <div id="main">
            Howdy. I'm a bit of embedded content. I was
            embedded from <span class="lift:Embedded.from">???</span>.
        </div>
    </body>
</html>

그리고 호출되는 Embedded.scala 프로그램입니다.

리스팅 3.5: Embedded.scala

package code
package snippet

import lib._

import net.liftweb._
import http._
import util.Helpers._
import common._
import java.util.Date

/**
 * 현재 페이지의 이름을 리스팅하는 스니펫
 */
object Embedded {
    def from = "*" #> S.location.map(_.name)
}

템플릿은 Embedded 스니펫의 from 메서드를 호출합니다. 이 경우에 어떤 생성자 파라미터도 가지지 않고 변경된 인스턴스를 자지지 않기 때문에 스니펫은 object 싱글톤입니다.

from 메서드입니다.

def from = "*" #> S.location.map(_.name)

현재 location의 name으로 컨텐츠를 교체하는 CSS Selector Transform를 생성합니다.



3.4.5 Param 예제

위에서 URL 파라미터를 획득하기 위해서 Loc[ParamInfo]를 생성하는 지를 보았습니다. 이제 /param/xxx 페이지를 보고 어떤게 파라미터에 접근할 수 있는지 보겠습니다.

리스팅 3.6: param.html

<!DOCTYPE html>
<html>
    <head>
        <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
        <title>Param</title>
    </head>
    <body class="lift:content_id=main">
        <div id="main" class="lift:surround?with=default;at=content">
            <div>
                Thanks for visiting this page. The parameter is
                <span class="lift:ShowParam">???</span>.
            </div>

            <div>
                Another way to get the param: <span class="lift:Param">???</span>.
            </div>

        </div>
    </body>
</html>

그리고 페이지에서 ParamInfo에 접근할 수 있는 두가지 다른 스니펫을 보겠습니다.

리스팅 3.7: Param.scala

package code
package snippet

import lib._

import net.liftweb._
import util.Helpers._
import common._
import http._
import sitemap._
import java.util.Date

// 페이지의 파라미터 정보를 획득합니다
case class ParamInfo(theParam: String)

// 페이지 파라미터 정보를 취하는 스니펫
class ShowParam(pi: ParamInfo) {
    def render = "*" #> pi.theParam
}

object Param {
    // /param/somedata를 위한 메뉴를 생성합니다
    val menu = Menu.param[ParamInfo]("Param", "Param",
                                s => Full(ParamInfo(s)),
                                pi => pi.theParam) / "param"
    lazy val loc = menu.toLoc

    def render = "*" #> loc.currentValue.map(_.theParam)
}

각 스니펫은 render메서드를 가집니다. 그러나 ShowParam 클래스는 현재의 Loc[_]로부터 ParamInfo를 담고있는 생성자 파라미터를 취합니다. 현재 Loc가 타입파라미터 ParamInfo를 가지고 있지 않다면 ShowParam의 인스턴스는 생성되지 않고 스티펫은 처리될 수 없습니다. 그러나 Loc[ParamInfo]를 가지고 있으므로 Lift는 Loc의 currentValue로 ShowParam을 생성한뒤 render 메서드가 호출되고 여기서 NodeSeq => NodeSeq인 CSS Selector Transform를 리턴합니다.

object param의 render메서드는 직접 Loc[ParamInfo]에 접근합니다. render 메서드는 Loc의 currentValue를 갖고 리턴되는 값인 CSS Selector Transform을 계산하기 위해서 사용합니다.



3.4.6 재귀(Recursive)

Lift 의 스니펫은 늦게(lazily) 평가됩니다.  이것은 스스로 스니펫을 갖거나 대안적으로 스스로 스니펫 호출을 가지고 있는 스니펫 바디의 일부를 선택하는  스니펫에서 마크업을 리턴하도록 하는 바깥쪽(outer) 스니펫이 실행될 때까지 스니펫의 바디가 실행되지 않는다는 것을 의미합니다.  예를 들면 마크업은 다음과 같습니다.

리스팅 3.8: recurse.html

<div id="main" class="lift:surround?with=default&at=content">
    <div>
        This demonstrates Lift's recursive snippets
    </div>

    <div class="lift:Recurse">
        <div id="first" class="lift:FirstTemplate">
            The first template.
        </div>

        <div id="second" class="lift:SecondTemplate">
            The second template.
        </div>
    </div>

    <div>
        <ul>
            <li>Recursive: <a href="/recurse/one">First snippet</a></li>
            <li>Recursive: <a href="/recurse/two">Second snippet</a></li>
            <li>Recursive: <a href="/recurse/both">Both snippets</a></li>
        </ul>
    </div>
</div>

Recurse 스니펫은 각각 스스로 스니펫을 호출하는 <div> 둘중 하나를 선택합니다.  Scala코드는 여기있습니다.

리스팅 3.9: Recurse.scala

package code
package snippet

import lib._

import net.liftweb._
import util._
import Helpers._
import http._
import scala.xml.NodeSeq

/**
 * 선택들(The choices)
 */
sealed trait Which
final case class First() extends Which
final case class Second() extends Which
final case class Both() extends Which

/**
 * 템플릿 둘중 하나를 선택합니다
 */
class Recurse(which: Which) {
    // 템플릿을 선택합니다
    def render = which match {
        case First() => "#first ^^" #> "*" // 오직 첫 템플릿만 선택합니다
        case Second() => "#second ^^" #> "*" // 오직 두번째 템플릿만 선택합니다
        case Both() => ClearClearable // 통과(passthru)합니다
    }
}

/**
 * 첫번째 템플릿 스니펫
 */
object FirstTemplate {
    // 통과(passthru)하지만 사이드이펙트 알림을 가집니다
    def render(in: NodeSeq) = {
        S.notice("First Template Snippet executed")
        in
    }
}

/**
 * 두번째 템플릿 스니펫
 */
object SecondTemplate {
    // 통과(passthru)하지만 사이드이펙트 알림을 가집니다
    def render(in: NodeSeq) = {
        S.notice("Second Template Snippet executed")
        in
    }
}

which의 값에 따라 마크업의 둘중 하나가 선택될 것입니다. 그리고 마크업의 각 부분은 스스로 알림(notice)를 보여주고 마크업을 관통하는 스니펫을 호출합니다.

이 기법을 사용해서 하나이상의 다른 스니펫을 선택하거나 lift:embed 스니펫을 리턴하는 스니펫을 가질 수 있으므로 아주 동적인 마크업 생성이 가능합니다.



3.4.7 요약

동적인 컨텐츠를 생성하기 위해서 사용되는 Lift의 스니펫 메카니즘의 약간 간단한 예제를 보았습니다. 7.1에서 스니펫에 대해서 더 읽을 수 있습니다.




3.5 결론(Wrap up)

이 장에서 Boot.scala으로 어떻게 애플리케이션의 동작을 정의하는지 보았습니다. 네이게이션을 생성하고 접근 제어를 강제하려고 사용되는 Lift의 SiteMap을 보았습니다. 어떻게 템플릿 시스템이 동작하는지(Lift에는 템플릿화하는 여러가지 다른 방법이 실제로 있지만 가장 일반적인 메카니즘을 보았습니다.) 보았습니다. 스니펫의 동작방식도 보았습니다.

다음 장에서는 Lift의 폼(form) 핸들링을 보겠습니다.
2011/02/13 03:39 2011/02/13 03:39