Outsider's Dev Story

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

Simply Lift : Chapter 3 - Snippets and SiteMap #1

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

3장
스니펫과 사이트맵(Snippets and SiteMap)


Lift는 3가지 방법으로 HTTP요청을 처리합니다 : HTML 페이지 생성, 로우레벨 HTTP 응답(예를들면 REST), Ajax/Comet요청에 대한 응답. Lift는 가장 일반적인 요청의 각 타입의 요청에 대해서 응답하기 위해 시맨틱을 많드려도 각 타입읠 요청들을 다르게 다룹니다.  달리 말하자면 이것은 데이터베이스 레코드에 상응하는 JSON 데이터를 서버로 다시 보내는 것과는 다른 많은 컴포넌트들과 함께 복잡한 HTML페이지를 만드는 것과는 다릅니다.

이 챕터에서 Lift가 각 페이지에 동적인 컨텐츠를 가지는 HTML페이지(메뉴등등), 접근을 제어하는 사이트 네비게이션 주위에 “크롬(chrome)”을 두는 것을 포함하는 URL에 기반하여 어떻게 동적인 HTML페이지를  생성하는지를 설명할 것입니다.

이 챕터를 위한 코드는 Simply Lift 배포판의  samples/snippet_and_sitemap 디렉토리에서 찾을 수 있습니다.



3.1 시작부분부터 보자 : Boot.scala

Lift 애플리케이션이 처음 시작되었을 때 Boot.scala의 코드가 실행됩니다.

리스팅 3.1 : Boot.scala

package bootstrap.liftweb

import net.liftweb._
import util._
import Helpers._

import common._
import http._
import sitemap._
import Loc._

import code.snippet._

/**
* 초기에 인스턴트화되고 실행되느 클래스. 애플리케이션이 Lift의 환경을 수정할수 있도록 해줍니다.
*/
class Boot {
    /**
     * 페이지가 디스플레이되어야 하는지 계산합니다. 이경우에 매분마다 보여질것입니다.
     */
    def displaySometimes_? : Boolean = (millis / 1000L / 60L) % 2 == 0

    def boot {
        // 스니펫을 찾아야 하는 위치
        LiftRules.addToPackages("code")

        // 사이트맵을 구성합니다
        def sitemap(): SiteMap = SiteMap(
            Menu.i("Home") / "index", // 메뉴를 정의하는 간단한 방법

            Menu.i("Sometimes") / "sometimes" >> If(displaySometimes_? _, S ? "Can't view now"),

            // 서브메뉴들을 가진 메뉴
            Menu.i("Info") / "info" submenus(
                Menu.i("About") / "about" >> Hidden >> LocGroup("bottom"),
                Menu.i("Contact") / "contact",
                Menu.i("Feedback") / "feedback" >> LocGroup("bottom")
            ),

            Menu.i("Sitemap") / "sitemap" >> Hidden >> LocGroup("bottom"),

            Menu.i("Dynamic") / "dynamic", // 동적인 컨텐츠를 가진 페이지

            Param.menu,

            Menu.param[Which]("Recurse", "Recurse",
                        {case "one" => Full(First())
                         case "two" => Full(Second())
                         case "both" => Full(Both())
                         case _ => Empty},
                        w => w.toString) / "recurse",

            // 이 메뉴가 /static 경로에 있는 모든 것을 비져블 가능하게 하기 때문에 더 복잡합니다.
            Menu.i("Static") / "static" / **)

        // sitemap을 설정합니다. 각 페이지에 접근제어를 원하지 않는다면 이 라인을 주석처리 하세요
        LiftRules.setSiteMapFunc(() => sitemap())

        // Ajax 호출이 시작되었을 때 스피너이미지를 보여줍니다.
        LiftRules.ajaxStart = Full(() => LiftRules.jsArtifacts.show("ajax-loader").cmd)

        // Ajax 호출이 끝났을때 스피너 이미지를 없앱니다.
        LiftRules.ajaxEnd = Full(() => LiftRules.jsArtifacts.hide("ajax-loader").cmd)

        // 요청을 UTF-8로 강제합니다
        LiftRules.early.append(_.setCharacterEncoding("UTF-8"))

        // 렌더링하기 위해서 HTML5를 사용합니다
        LiftRules.htmlProperties.default.set((r: Req) => new Html5Properties(r.userAgent))
    }
}

XML파일에서 설정 파라미터를 보관하는것 보다는 Lift는 Boot에서 코드로 설정파라미터들을 보관합니다. Boot는 서블릿 컨테이너가 Lift 애플리케이션을 로드할때 한번 실행됩니다. 부팅되는 동안 LiftRules 싱글톤에서 많은 Lift의 실행룰을 바꿀 수 있지만 부팅후에는 이러한 파라미터들은 고정됩니다.



3.1.1 LiftRules 룰

어떻게 Lift가 HTTP 요청을 응답으로 바꾸는지를 정의하는 설정 파라미터의 대부분은 LiftRules 싱글톤에 담겨있습니다. LiftRules를 위한 파라미터들 중 일부는 공통적으로 사용되고 어떤 것들은 아주 드물게 디폴트값으로 변경이 됩니다. LiftRules는 부팅동안에만 변경될 수 있고 다른때에는 불가능합니다. 그래서 부팅때 모든 설정을 지정해야합니다(또는 boot에서 호출되는 메서드 내에서)



3.1.2 프로퍼티들과 실행모드

실행되는 애플리케이션을 위한 많은 프로퍼티들이 Boot.scala에서 정의될 수 있기 때문에 text파일에서 가장 잘 정의된 프로퍼티들이 있습니다. Lift는 프로젝트당 여러개의 프로퍼티 파일들을 지원합니다. 프로퍼티파일들은 사용자, 머신, 실행모드에 기반해서 로드됩니다.

애플리케이션의 서브셋이나 특정한 환경을 위한 설정파일을 제공하기를 원한다면 Lift는 그것들이 사용되고 있는 컨텍스트와 관련있게 설정파일이 네이밍되기를 기대합니다. 표준 네이밍 포맷은 다음과 같습니다.
modeName.userName.hostName.props

예를들면
dpp.yak.props
test.dpp.yak.props
production.moose.props
staging.dpp.props
test.default.props
default.props

hostNmae과 userName은 옵션값이고 modeName은 “test”, “staging”, “production”, “pilot”, “profile”, 공백값(개발모드를 위한)중의 하나입니다. 표준 Lift 프로퍼티파일 확장자는 “props”입니다.

프로퍼티파일을 프로젝트의 src/main/resources/props 디렉토리에 두면 이것들은 빌드프로세스의 일부로써 패키팅 될 것입니다.



3.1.3 관례에 따라

Rails처럼 Lift도 관례에 따라 특정위치에서 아이템들을 찾을 것입니다. 예를 들면 Lift는 xxx.snippet 팩키지에서 스니펫을 구현한 클래스를 찾을 것입니다. xxx부분은 애플리케이션의 메인팩키지에 있습니다. Lift가 찾는 하나이상의 팩키지를 아래처럼 정의합니다.

// 스니펫을 찾아야 하는 위치
LiftRules.addToPackages("code")

여기서 Lift가 검색할 팩키지 리스트에 code팩키지를 추가했습니다. 또한 LiftRules.addToPackages("com.fruitbat.mydivision.myapplication")처럼 추가할 수도 있습니다.



3.1.4 잡다한 룰(Misc Rules)

다음 섹션까지 사이트맵 정의는 건너뛸 것입니다. 이 룰은 Ajax호출중에 어떻게 스피너 아이콘을 보여주는지 정의합니다.(이 함수가 사용가능하다면 Lift는 자동적으로 스피너 아이콘을 보여줄 것 입니다.)

// Ajax 호출이 시작되었을 때 스피너이미지를 보여줍니다.
LiftRules.ajaxStart = Full(() => LiftRules.jsArtifacts.show("ajax-loader").cmd)

그리고 이 룰은 기본 플랫폼 인코딩말고 UTF-8르 기본 캐릭터 인코딩을 설정합니다.

// 요청을 UTF-8로 강제합니다
LiftRules.early.append(_.setCharacterEncoding("UTF-8"))

부팅동안 조정하는 많은 파라미터들이 있습니다.



3.1.5 HTML5

Lift 2.2이전에 lift는 XHTML로 모든 템플릿을 다루고 브라우저에 XHTML을 발행했습니다. Lift 프로젝트가 2007년초에 시작되었을 때 이것은 아주 좋은 아이디어 처럼 보였었습니다. XHTML과 몇몇 JavaScript 라이브러리(예를 들면 구글 맵스는 XHTML 페이지에서 동작하지 않습니다.)가 채택되지 않는 세상으로 바뀌었습니다. Lift 2.2는 파서에서 둘다 지원하도록 HTML5를 옵션으로 추가하였고(그래서 템플릿에서 XML포맷을 요구하는 대신에 HTML5 템플릿을 읽을 수 있습니다.) 브라우저에 HTML5를 발행합니다. Lift는 아직 Scala NodeSeq 엘리먼트로써 페이지를 처리하므로 애플리케이션에서 바뀐점은 없습니다.

Lift 2.2 앱이 Lift의 XHTML 지원에 호환성있게 유지하기 위해서 기본적으로 XHTML 파서/시리얼라이저(serializer)가 사용되지만 boot에서 다음코드로 바꿀수 있는 HTML5 지원을 사용하기를 추천합니다.

// 렌더링하기 위해서 HTML5를 사용합니다
LiftRules.htmlProperties.default.set((r: Req) => new Html5Properties(r.userAgent))





3.2 사이트맵

Lift는 사이트맵을 호출하는 옵션적인 특징을 가지고 있습니다. 이걸 꼭 사용할 필요는 없지만 부팅시에 사이트맵을 설정하면 Lift는 사이트의 HTML 페이지 화이트리스트로써 사이트맵을 사용할 수 있습니다.(REST URL은 사이트맵에서 리스팅될 필요가 없습니다.) 사이트맵은 계층적인 메뉴들과 메뉴 아이템들의 그룹화, 전체 사이트맵의 디스플레이, 관계있는 사이트맵을 생성할 수 있도록 하는 네비게이션과 접근제어를 정의합니다. 이 섹션에서 사이트맵의 능력에 대해서 얘기할 것입니다.



3.2.1 사이트맵의 정의

사이트맵은 반드시 부팅때 정의되어야 하고 딱 한번만 정의되어야 합니다.1  전통적으로 SiteMap 인스턴스를 리턴하는 함수를 정의할 것입니다.

// 사이트맵을 구성합니다
def sitemap(): SiteMap = …

그 후 LiftRules에서 SiteMap를 정의합니다.

// sitemap을 설정합니다. 
// 각페이지에 접근제어를 원하지 않는다면 이 라인을 주석처리 하세요
LiftRules.setSiteMapFunc(() => sitemap())

개발모드에서  함수는 재구성된 SiteMap을 로드하기 위해서  모든 페이지에서 호출될 것입니다. 모든 다른 Lift의 실행모드에서 사이트맵은 부팅시에 한번만 구성됩니다.

SiteMap은 Menu인스턴스의 컬랙션입니다. 각 Menu는 하나의 Loc[_]와 서브메뉴(0이나 그이상)로써 Menu인스턴스의 셋을 가집니다. 각 Menu인스턴스는 유일한 이름을 가집니다.

HTML페이지가 사이트맵에서 정의되지 않았다면 Lift는 그 페이지를 서비스하지 않을 것입니다. SiteMap은 서비스할 페이지의 화이트리스트입니다. 게다가 Loc[_]는 다중 접근제어에 대한 룰을 가질수 있는 파라미터들을 같습니다.



3.2.2 가장 간결한 사이트맵

가장 간결한 사이트맵은 단일 페이지로 정의합니다.

def sitemap(): SiteMap = SiteMap(Menu.i("Home") / "index")

이것은 싱글메뉴 아이템을 가진 사이트맵입니다. 메뉴는 “Home”이라는 이름을 가지고 있고 “Home”문자열은 로컬라이징되어(8.1챕터 참조) 표시될 것입니다. Menu.i 메서드는 Loc[Unit]을 가진 Menu를 생성합니다.



3.2.3 Menu와 Loc[_]

아마도 왜 Menu와 Loc[_](“Loke”라고 읽고 location의 축약형입니다.)가 분리되어 있고 Loc가 타입파라미터를 취하는지 궁금할 것입니다.

Menu는 위치와 많은 서브메뉴들을 담고 있습니다. 근본적인 생각은 메뉴계층에서 다른 위치에 있는 하나의 Loc[_]을 가질수 있다는 것입니다. 그래서 과거를 바탕으로 그것들은 나누어졌지만 1대1관계에 있습니다.

Loc[_]는 Loc를 위한 현재 값 타입을 정의하는 타입 파라미터를 취합니다. 예를 들면 Loc가 위키페이지를 노출하는 페이지를 참조한다면 Loc의 타입파라미터는 WikiPage: Loc[WikiPage]가 될 것입니다.

각 Loc는 Loc[_]의 동작을 정의하는 많은 파라미터(“loke param”이라고 읽는 LocParam으로 알려진)를 가질 수 있습니다. 이러한 파라미터 들은 접근 제어 테스트, 템플릿 정의, 타이틀, 그룹등을 포함합니다.



3.2.4 접근 제어(Access Control)

If() LocParam을 가진 Loc에 의해서 표현된 URL이나 페이지에 대한 접근을 제어할 수 있습니다.

/**
 * 페이지가 디스플레이되어야 하는지 계산합니다.
 * 이경우에 매분마다 보여질것입니다.
 */
def displaySometimes_? : Boolean = (millis / 1000L / 60L) % 2 == 0

    Menu.i("Sometimes") / "sometimes" >> If(displaySometimes_? _, S ? "Can't view now")

접근이 가능하다면 true를 리턴하는 메서드를 정의하였습니다. If() LocParam을 추가하는 것은 true를 리턴하는 함수없이 페이지에 대한 접근을 제한할 것입니다. 메뉴아이템들은 접근제어 규칙을 통과하지 못한 페이지에서는 보이지 않을 것이고 심지어 사용자가 브라우저에 URL을 입력했을때도 페이지가 보여지지 않을  것입니다.(기본적으로 사용자는 홈 페이지로 리다이렉트되고 에러가 보여질 것입니다.)



3.2.5  숨김과 그룹

메뉴아이템들은 접근가능한 페이지라고 하더라도 기본 메뉴 계층에서 숨겨질수 있습니다. Hidden LocParam은 “기본메뉴에서 숨겨라”라고 말합니다.

Menu.i("About") / "about" >> Hidden >> LocGroup("bottom")

또한 메뉴아이템들은 네이밍된 그룹으로 그루핑되어 보여질 수 있습니다.

<span class="lift:Menu.group?group=bottom"></span>

결과는 다음과 같습니다.

<a href="/about">About</a> <a href="/feedback">Feedback</a> <a href="/sitemap">



3.2.6 서브메뉴

다음처럼 메뉴를 포함시킬 수 있습니다.

// 서브메뉴들을 가진 메뉴
Menu.i("Info") / "info" submenus(
    Menu.i("About") / "about" >> Hidden >> LocGroup("bottom"),
    Menu.i("Contact") / "contact",
    Menu.i("Feedback") / "feedback" >> LocGroup("bottom")

About, Contact, Feedback 페이지들은 Info페이지아래로 포함됩니다.



3.2.7 파라미터

들어오는 URL을 파싱해서 파라미터들을 타입세이프 변수들로 추출할 수 있습니다.

// 페이지 파라미터 정보를 잡아냅니다.
case class ParamInfo(theParam: String)

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

위의 코드는 “Param”이라는 메뉴를 생성합니다. 메뉴는 xxx가 어떤것에도 매치될 수 있는 /param/xxx라는 URL를 위한 것입니다.

URL이 /param/dogfood나 /param/fruitbat이 되었을 때 이것들은 Loc에 매치되고 (s => Full(ParamInfo(s))) 함수가 호출됩니다. Full Box가 리턴된다면 값은 Loc의 currentValue에 들어갑니다.

많은 URL 파라미터들을 매칭하는 Loc 구현을 직접 작성하는 것도 가능합니다.

획득한 파라미터에 접근하는 것(이 경우에는 ParamInfo)에 대한 정보는 3.4.5를 보세요.



3.2.8 와일드카드

주어진 경로의 모든 컨텐츠에 매치되는 메뉴를 생성할 수 있습니다. 이 경우에 /static/에 있는 모든 HTML 파일들은 제공될 것입니다.  이것은 /static/index, /static/fruitbat, /static/moose/frog/wombat/meow 를 포함합니다.

// 이 메뉴가 /static 경로에 있는 모든 것을 비져블 
// 가능하게 하기 때문에 더 복잡합니다.
Menu.i("Static") / "static" / **

Lift는 .(마침표)나 _(언더스코어)로 시작하거나 -hidden으로 끝나는 어떤 파일이나 디렉토리는 제공하지 않을 것입니다.



3.2.9 요약

여러가지 종류의 메뉴아이템과 함께 SiteMap을 어떻게 생성하는지 보여주었습니다. 이제 뷰를 보겠습니다.
  1. 개발모드에서 사이트맵은 네비게이션이 바꿀때마다 애플리케이션을 재시작 할 필요없이 사이트 컨텐츠를 바꿀수 있도록 동적으로 바뀔수 있습니다. 제공되는 페이지의 직렬화를 강제하는 각 페이지로드마다 사이트맵을 재구성하는 것과 관련된 중요한 성능문제가 있습니다. 메뉴아이템을 사용/사용불가하게 하고 동적으로 서브메뉴를 생성하는 사이트맵의 많은 특징들이 있습니다. 애플리케이션 디자인을 개발모드 메뉴 리로딩에 의지하지 마세요. [Back]
2011/02/13 03:32 2011/02/13 03:32