Outsider's Dev Story

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

Simply Lift : Chapter 4 - Forms

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

4장
폼(Forms)


이번 장에서 Lift가 템플릿을 어떻게 처리하는지 보겠습니다. 다중페이지 인풋폼과 Ajax폼을 두루 지원하는 구식 방법(디자이너가 잇풋에 이름을 붙이고 애플리케이션이 이 이름들은 변수들에 매핑하는 방법)의 폼프로세싱부터 시작하겠습니다.



4.1 구식의 똑똑하지 않은 폼

폼에 대한 HTML을 보겠습니다.

리스팅 4.1: dumb.html

<div id="main" class="lift:surround?with=default&at=content">
  <div>
    This is the simplest type of form processing... plain old
    mechanism of naming form elements and processing the form elements
    in a post-back.
  </div>

  <div>
    <form action="/dumb" method="post" class="lift:DumbForm">
      Name: <input name="name"><br>
      Age: <input name="age"><br>
      <input type="submit" value="Submit">
    </form>
  </div>
</div>

꽤 일반적으로 보입니다. 폼을 정의했습니다. 이제 해야할 유일한 것은 <form>태그의 class="lift:DumbForm"속성을 가진 폼과 동작을 연동하는 것입니다. 페이지는 본래의 컨텐츠를 제공하는 같은 URL로 폼이 포스트(post)되는 것을 의미하는 포스트백(post-back)입니다.

코드가 폼을 처리하는 것을 보겠습니다.

리스팅 4.2: DumbForm.scala

package code
package snippet

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

/**
 * 폼 POST로부터 쿼리 파라미터를 획득하고
 * 처라하는 스니펫
 */
object DumbForm {
  def render(in: NodeSeq): NodeSeq = {

    // 각 파라미터를 평가하기 위해서 for문을 사용합니다
    for {
      r <- S.request if r.post_? // post임을 확인합니다
      name <- S.param("name") // name필드를 얻습니다
      age <- S.param("age") // age필드를 얻습니다
    } {
      // 모든 것이 기대된 대로 되었으면
      // 알림을 표시하고 사용자를 
      // 다시 홈페이지로 보냅니다
      S.notice("Name: "+name)
      S.notice("Age: "+age)
      S.redirectTo("/")
    }

    // post와 모든 파라미터를 엊지 못했으면 
    // HTML을 통과시킵니다(pass through)
    in
  }
}

꽤 간단합니다. 요청이 post이고 쿼리파라미터가 존재하면 화면은 이름과 나이를 알리고 애플리케이션 홈페이지로 다시 리다이렉트 합니다.

이렇게 처리하지 않는 충분한 이유가 있습니다.

먼저, HTML과 Scala코드사이에 네이밍이 일치하지 않으면 폼 필드를 잃어버릴 것입니다. 그리고 정렬된 네이밍을 유지하는 것은 항상 쉬운 것은 아닙니다.

두번째, 예측할 수 있는 네임을 가진 폼은 replay attack을 이끈다. 공격자가 사용자가 만든 폼서밋을 획득하고 들어온 필드를 새로운 값으로 치환할 수 있으면 훨씬 쉽게 애플리케이션을 해킹할 수 있습니다.

세번째, 상태를 유지하는 것은 수동(manual) 폼에서는 아주 어렵게 됩니다. 주요한 키(primary key)나 변경될 수 있는 다른 정보를 담고 있는 히든필드에 의지해야 합니다.

Lift는 HTML 폼을 다루는데 있어서 훨씬 더 강력하고 안전한 메카니즘을 제공합니다.



4.2 OnSubmit

Lift 디자인의 일부는 사용자 행위와 유저인터페이스 엘리먼트를 연결하는 VisualBasic을 반영했습니다. 이것은 간단하지만 아직은 아주 강력한 개념입니다. 각 폼엘리먼트는 서버의 함수과 연결되어 있습니다.1 게다가 Scala에서 함수는 close over scope(scope내에서 현재 변수를 획득하는)이기 때문에 쉽고 안전하게 웹클라이언트에 상태를 노출하지 않고 상태를 유지할 수 있습니다.

이제 어떻게 동작하는지 보겠습니다. 먼저 HTML입니다.

리스팅 4.3: onsubmit.html

<div id="main" class="lift:surround?with=default&at=content">
  <div>
    Using Lift's SHtml.onSubmit, we've got better control
    over the form processing.
  </div>

  <div>
    <form class="lift:OnSubmit?form=post">
      Name: <input name="name"><br>
      Age: <input name="age" value="0"><br>
      <input type="submit" value="Submit">
    </form>
  </div>
</div>

이 HTML에서 유일하게 다른 점은 <form class="lift:OnSubmit?form=post">입니다. 폼의 행위 즉 스니펫은 OnSubmit.render를 호출합니다. form=post 속성이 폼을 post-back으로 만듭니다. 이것이 <form>태그: <form method="post" action="/onsubmit">의 method와 action 속성을 설정합니다.

스니펫을 보겠습니다.

리스팅 4.4: OnSubmit.scala

package code
package snippet

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

/**
 * HTML엘리먼트에 행위와 함수를 바인딩하는 스니펫
 */
object OnSubmit {
  def render = {
    // 값을 주입하기 위한 몇몇 변수들을 정의합니다
    var name = ""
    var age = 0

    // 폼을 처리합니다
    def process() {
      // age가 13보다 작으면 에러를 표시합니다
      if (age < 13) S.error("Too young!")
      else {
        // 그렇지 않으면 사용자에게 피드백을 주고
        // 홈페이지로 리다이렉트합니다
        S.notice("Name: "+name)
        S.notice("Age: "+age)
        S.redirectTo("/")
      }
    }

    // 폼 엘리먼트 각각을 함수에 연결합니다.
    // 함수는 엘리먼트가 제출되었을 때 실행될 행위입니다.
    "name=name" #> SHtml.onSubmit(name = _) & // set the name
    // set the age variable if we can convert to an Int
    // Int로 변환할 수 있으면 age 변수를 설정합니다
    "name=age" #> SHtml.onSubmit(s => asInt(s).foreach(age = _)) &
    // 폼이 제출되었을 때 변수를 처리합니다
    "type=submit" #> SHtml.onSubmitUnit(process)
  }
}

DumForm.scala처럼 스니펫은 싱글톰으로 구현되었습니다. render메서드는 2가지 변수 name과 age를 선언합니다. process() 메서드는 건너뛰고 행위(behavior)와 폼엘리먼트를 연결시키는 것을 보겠습니다.

"name=name" #> SHtml.onSubmit(name = _)은 name 속성이 “name”과 동일한 들어오는 HTML 엘리먼트를 취하고 SHtml.onSubmit 메서드를 통해서 함수와 폼엘리먼트를 연결합니다. 함수는 String 파라미터를 취하고 name변수의 값을 String으로 설정합니다. HTML의 결과는 <input name="F10714412223674KM">입니다. 새로운 name 속성은 함수(인풋에 name을 설정하는)와 폼엘리먼트를 연결하는 GUID(globally unique identifier)입니다. 폼이 제출되었을 때 일반적인 HTTP post나 Ajax를 통해서 함수가 폼엘리먼트의 값과 함께 실행될 것입니다. 폼제출상에서 이 함수가 수행됩니다.

age 폼필드: "name=age" #> SHtml.onSubmit(s => asInt(s).foreach(age = _))에 대해서 보겠습니다. 실행되는 함수는 String을 Int로 파싱을 시도하려고 Helpers.asInt를 사용합니다. 성공적으로 파싱되면 age변수는 파싱된 Int로 설정됩니다.

마지막으로 submit 버튼: "type=submit" #> SHtml.onSubmitUnit(process)와 함수를 연결합니다. SHtml.onSubmitUnit메서드는 파라미터가 없는 함수(파라미터로 단일 String을 취하는 함수보다는)를 취하고 폼이 제출되었을때 함수를 적용합니다.

process() 메서드는 name과 age 변수들의 범위를 갖혀있고(close over, 클로저를 말하는 듯 합니다.) 메서드가 함수로 올려졌을때 그것은 아직 변수들은 close over합니다. 이것은 함수가 적용되었을 때 이 메서드에서 다른 함수로 같은 name과 age 변수의 인스턴스들을 참조한다는 것을 의미합니다. 그러나 85개의 브라우저에서 85개의 폼의 복사본을 오픈했다면 각각은 name과 age 변수들의 다른 인스턴스들이 갖혀있습니다. 이 방법에서 Lift는 애플리케이션이 브라우저에게 복잡한 상태를 노출하지 않고도 복잡한 상태를 가질수 있도록 해줍니다.

이 폼 예제의 문제는  잘못된 나이를 입력했을때 전체 폼이 리셋된다는 것입니다. 이제 에러 핸들링을 어떻게 다 좋게 할 수 있는지 보겠습니다.



4.3. 상태유지 스니펫(Stateful Snippets)

사용자에게 더 나은 경험을 제공하기 위해서 name과 age변수들의 상태를 다중 폼 제품을 넘어서 획득할 필요가 있습니다. Lift가 이것을 하기 위한 메카니즘은 상태유지 스니펫2입니다. StatefulSnippet의 서브클래스인 스니팻은 자동적으로 여분의 숨겨진 파라미터를 폼의 처리과정동안에  보장되되도록 폼에 추가합니다. StatefulSnippet의 같은 인스턴스가 사용될 것입니다.3

HTML 템플릿을 보겠습니다.

리스팅 4.5: stateful.html

<div id="main" class="lift:surround?with=default&at=content">
  <div>
    Using stateful snippets for a better
    user experience
  </div>

  <div>
    <div class="lift:Stateful?form=post">
      Name: <input name="name"><br>
      Age: <input name="age" value="0"><br>
      <input type="submit" value="Submit">
    </div>
  </div>
</div>

템플릿은 onsubmit.html의 템플릿처럼 괜찮아 보입니다. 스니펫을 보겠습니다.

리스팅 4.6: Stateful.scala

package code
package snippet

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

/**
 * 상태유지 스니펫. 이 스니펫과 연결된 상태는 
 * 인스턴스 변수들안에 있습니다.
 */
class Stateful extends StatefulSnippet {
  // 상태는 상태유지 스니펫의 이 인스턴스에서 유일합니다
  private var name = ""
  private var age = "0"

  // 사용다를 다시 돌려보내 수 있도록
  // 사용자로부터 온 whence를 획득합니다
  private val whence = S.referer openOr "/"

  // StatefulSnippet은  
  // 메서드의 명시적인 디스패치를 요구합니다
  def dispatch = {case "render" => render}

  // 각 HTML엘리먼트와 행위를 연결합니다
  def render =
    "name=name" #> SHtml.text(name, name = _, "id" -> "the_name") &
    "name=age" #> SHtml.text(age, age = _) &
    "type=submit" #> SHtml.onSubmitUnit(process)

  // 폼을 처리합니다
  private def process() =
    asInt(age) match {
      case Full(a) if a < 13 => S.error("Too young!")
      case Full(a) => {
        S.notice("Name: "+name)
        S.notice("Age: "+a)
        S.redirectTo(whence)
      }

      case _ => S.error("Age doesn't parse as a number")
    }
}

공정한 양의 다른 점이 있습니다. 첫째 클래스 정의: class Stateful extends StatefulSnippet 입니다. 스니펫 인스턴스 그 자체가 상태를 담고 있기 때문에 오부젝트 싱글톤이 될 수 없습니다. 멀티플 인스턴스가 되기 위해서 클래스로 선언되어야 합니다.

인스턴스 변수에서 상태(name, age와 사용자가 어디서 왔는지에 대한 whench)를 획득합니다.

StatefulSnippets은 “관례에 따라(by-convention)”보다는 명시적으로 디스패칭하는 메서드인 dispatch 메서드를 요구합니다.

render메서드는  마크업과 행위를 연결시키려는 익숙한 CSS Selector Transforms을 사용합니다. 그러나 SHtml.onSubmit을 사용하기 보다는 name과 value 속성 모두를 설정한 HTML <input> 엘리먼트를 명시적으로 생성하기 위해서 SHtml.text를 사용하고 있습니다. 첫번째 인풋의 경우에서 또한 명시적으로 id속성을 설정하고 있습니다. 애플리케이션에서 그것을 사용하고 있지는 않지만 어떻게 부가적인 속성을 추가하지를 보여주려는 것입니다.

마지막으로 process()메서드는 String인 age를 Int로의 변환을 시도합니다. 만약 Int이지만 13보다 작다면 에러를 보여줄 것입니다. String이 Int로 피싱될수 없다면 에러를 보여 주고 그렇지 않다면 사용자에게 알려주고 사용자가 온 페이지로 돌려보낼 것입니다.

이 예제에서 name과 age 필드에 잘못된 값을 입력한다면 무엇을 입력했는디 사용자에게 다시 보여주기 위해서 폼의 값들을 저장합니다.

StatefulSnippets을 위한 HTML의 결과와 다른 스니펫의 큰 차이점은  폼에 <input name="F1071441222401LO3" type="hidden" value="true">를 추가한다는 것입니다. 이 히든필드는 처음에 폼을 생성하려고 사용되는 Stateful의 인스턴스인 “Stateful”이라는 이름의 스니펫과 연결됩니다.

좋은 사용자 경험을 만들기 위해서 대안적인 메카니즘을 보겠습니다.



4.4 RequestVars

이 예제에서  RequestVars에 상태를 둠으로써 요청동안에 상태를 저장하려고 했습니다.(7.8 참고)

Lift는 XXXVars를 호출하는 상태를 위해서 타입세이프 컨테이너를 가지고 있습니다.   현재 요청4이 유효범위인 Wizard와 RequestVars로 범위가 한정되는 WizardVars인 세션 범위를 가지고 있는 SessionVars가 있습니다. Vars는 싱글톤: private object name extends RequestVar("")로 정의되었습니다. 그것들은 타입이 정해지고(여기서는 타입이 String입니다) 기본값을 가집니다.

그래서  마지막 2개의 예제에서의 HTML과 아주 비슷하게 보이는 HTML을 보겠습니다.

리스팅 4.7: requestvar.html

<div id="main" class="lift:surround?with=default&at=content">
  <div>
    Using RequestVars to store state
  </div>

  <div>
    <form class="lift:ReqVar?form=post">
      Name: <input name="name"><br>
      Age: <input name="age" id="the_age" value="0"><br>
      <input type="submit" value="Submit">
    </form>
  </div>
</div>

이제 스니펫 코드를 보겠습니다.

리스팅 4.8: ReqVar.scala

package code
package snippet

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

/**
 * RequestVar기반의 스니펫
 */
object ReqVar {
  // name, age, whence를 위한 RequestVar 홀더를 정의합니다
  private object name extends RequestVar("")
  private object age extends RequestVar("0")
  private object whence extends RequestVar(S.referer openOr

  def render = {
    // whence를 획득합니다 이미 설정되어 있지 않다면
    // whench RequestVar의 평가를 강제합니다
    val w = whence.is

    // RequestVar이 Settable{type=String}을 상속했기 때문에
    // 명시적인 함수가 필요없습니다.
    // 그래서 Lift는 텍스트 엘리먼트 생성을 위해서 
    // 어떻게 RequestVar을 get/set하는지 알고 있습니다
    "name=name" #> SHtml.textElem(name) &
    // 어디로 가야하는지 알기 위한 
    // whence를 설정하는 히든필드를 추가합니다
    "name=age" #> (SHtml.textElem(age) ++
                    SHtml.hidden(() => whence.set(w))) &
    "type=submit" #> SHtml.onSubmitUnit(process)
  }

  // Stateful에서와 같은 방법으로 처리합니다
  private def process() =
    asInt(age.is) match {
      case Full(a) if a < 13 => S.error("Too young!")
      case Full(a) => {
        S.notice("Name: "+name)
        S.notice("Age: "+a)
        S.redirectTo(whence)
      }

      case _ => S.error("Age doesn't parse as a number")
    }
}

스니펫은 RequestVars에 상태를 저장하기 때문에 싱글톤입니다.

<input>태그를 생성하려고 SHtml.textElem()를 사용합니다. 생성된 RequestVar을 get/set하는 메서드와 펑션으로 RequestVar를 건네줄 수 있습니다.

상태유지 폼의 메카니즘과 StatefulSnippet 메카니즘의 사용은 개인적인 선택입니다. 더 나은 것이 아니라 단지 다를 뿐입니다.

이제 에러 메시지와 함께 더 많은 과립(granular)을 어떻게 얻는지 보겠습니다.



4.5 필드 오류(Field Errors)

이전의 예제에서 사용자에게 오류를 노출했습니다. 그러나 어떤 필드에서 오류가 생겼는지는 말해주지 않습니다. 이게 오류 리포팅에 대해서 좀더 많은 정보를 주도록 하겠습니다.

먼저 HTML을 보겠습니다.

리스팅 4.9: fielderror.html

<div id="main" class="lift:surround?with=default&at=content">
  <div>
    Let's get granular about error messages
  </div>

  <div>
    <div class="lift:FieldErrorExample?form=post">
      Name: <input name="name"><br>
      Age: <span class="lift:Msg?id=age&errorClass=error">error</span>
      <input name="age" id="the_age" value="0"><br>
      <input type="submit" value="Submit">
    </div>
  </div>
</div>

이 HTML은 Age: <span class="lift:Msg?id=age&errorClass=error">error</span>부분이 다릅니다. 오류메시지를 둘 영역을 마크업안에 표시했습니다.

Stateful.scala와 아주 유사한 스니펫 코드를 보겠습니다. 작지만 중요한 다른 점이 있습니다.

리스팅 4.10: FieldErrorExample.scala

package code
package snippet

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

/**
 * Stateful.scala와 같은 StatefulSnippet
 */
class FieldErrorExample extends StatefulSnippet {
  private var name = ""
  private var age = "0"
  private val whence = S.referer openOr "/"

  def dispatch = {case _ => render}

  def render =
    "name=name" #> SHtml.text(name, name = _) &
    "name=age" #> SHtml.text(age, age = _) &
    "type=submit" #> SHtml.onSubmitUnit(process)

  // 상태유지(Stateful)와 비슷합니다
  private def process() =
    asInt(age) match {
      // Msg span에서 id에 대응하는 오류에 대한
      // 파라미터를 알려줍니다
      case Full(a) if a < 13 => S.error("age", "Too young!")
      case Full(a) => {
        S.notice("Name: "+name)
        S.notice("Age: "+a)
        S.redirectTo(whence)
      }

      // Msg span에서 id에 대응하는 오류에 대한
      // 파라미터를 알려줍니다
      case _ => S.error("age", "Age doesn't parse as a number")
    }
}

주요한 다른 점은 case Full(a) if a < 13 => S.error("age", "Too young!") 입니다. “age”를 S.error에 건네주고 이것은 마크업에서 Msg 스니펫의 id에 대응됩니다. 이것은 Lift에게 어떻게 오류메시지와 마크업을 연결시키는지 말해줍니다.

그러나  Lift에는 복잡한 폼을 처리하는 더 좋은 방법이 있습니다. : LiftScreen



4.6 LiftScreen

웹 애플리케이션을 만들 때 하는 것중 대부분은 인풋과 동적인 컨텐츠를 연결하는 화면(screen)을 생성하는 것입니다. Lift는 유효성검사, 뒤로가기버튼등을 지원하는 단일 페이지와 다중페이지 인풋 폼을 만드는 Screen과 Wizard를 제공합니다.

이제 화면을 위한 HTML을 보겠습니다.

리스팅 4.11: screen.html

<div id="main" class="lift:surround?with=default&at=content">
  <div>
    Let's use Lift's LiftScreen to build complex
    simple screen input forms.
  </div>

  <div class="lift:ScreenExample">
    Put your form here
  </div>
</div>

폼 엘리먼트를 명시적으로 선언하지 않았습니다. 단지 다음과 같은 스니펫을 가리켰을 뿐입니다.

리스팅 4.12: ScreenExample.scala

package code
package snippet

import net.liftweb._
import http._

/**
 * 화면의 필드들을 선언합니다
 */
object ScreenExample extends LiftScreen {
  // 필드들과 기본값들
  val name = field("Name", "")

  // age는 유효성검사 규칙을 가집니다
  val age = field("Age", 0, minVal(13, "Too Young"))

  def finish() {
    S.notice("Name: "+name)
    S.notice("Age: "+age)
  }
}

화면에서 필드들과 그것들의 유효성 검사 규칙들을 정의하고 화면이 종료되었을때 무엇을 해야하는지를 정의합니다. 나머지는 Lift가 담당합니다.

기본적으로 폼을 생성하기 위한 마크업은 /templates-hidden/wizardall.html에서 찾을 수 있습니다. 또한 screen-by-screen 원칙에 따라 템플릿을 선택할 수 있습니다.



4.7 Wizard

LiftScreen은 단일 화면 애플리케이션에는 아주 좋습니다. 다중화면에 필요한 인풋과 유효성 검사를 가지고 있다면 Wizard가 바로 당신이 원하는 것입니다. 단지 스니펫 호출을 일으키는 마크업은 건너뛰겠습니다. wizard 코드가 여기 있습니다.

리스팅 4.13: WizardExample.scala

package code
package snippet

import net.liftweb._
import http._
import wizard._
import util._

/**
 * 다중 페이지 인풋 화면을 정의합니다
 */
object WizardExample extends Wizard {

  // 첫번째 스크린을 정의합니다
  val screen1 = new Screen {
    val name = field("Name", "")
    val age = field("Age", 0, minVal(13, "Too Young"))
  }

  // 두번째 스크린을 정의합니다
  val screen2 = new Screen {

    // 라디오 버튼
    val rad = radio("Radio", "Red", List("Red", "Green", "Blue"))

    // 셀렉트
    val sel = select("Select", "Archer", List("Elwood", "Archer", "Madeline"))

    // 텍스트에어리어
    val ta = textarea("Text Area", "")

    // 최소길이를 가진 패스워드 인풋
    val pwd1 = password("Password", "", valMinLen(6, "Password too short"))

    // 그리고 커스텀 유효성 검사
    val pwd2 = password("Password (re-enter)", "", mustMatch _)

    // List[FieldError]를 리턴합니다. 필드의 ID를 인서트하는 
    // String에서 List[FieldError]로 함축적 컨버전이 있습니다. 
    def mustMatch(s: String): List[FieldError] =
      if (s != pwd1.is) "Passwords do not match" else Nil
  }

  def finish() {
    S.notice("Name: "+screen1.name)
    S.notice("Age: "+screen1.age)
  }
}

위의 화면 예제처럼 단지 선언적일 뿐입니다. 뒤로가기 버튼은 동작합니다. 웹브라우저에서 다중탭에서 동작하는 다중 wizard를 갖을 수 있고 서로 간섭하지 않습니다.



4.8 Ajax

전체 페이지 HTML에서 추가적으로 Lift는 Ajax 폼을 지원합니다. Lift의 폼은 브라우저에서 GUID들과 연결된 서버사이드 함수이기 때문에 폼을 전체페이지 로드에서 Ajax로 전환하는 것은 아주 사소한 것입니다. 마크업을 보겠습니다.

리스팅 4.14: ajax.html

<div id="main" class="lift:surround?with=default&at=content">
  <div>
    An example of doing forms with Ajax.
  </div>
   
  <form class="lift:form.ajax">
    <div class="lift:AjaxExample">
      Name: <input name="name"><br>
      Age: <span class="lift:Msg?id=age&errorClass=error">error</span><input name="age" id="the_age" value="0"><br>
      <input type="submit" value="Submit">
    </div>
  </form>
</div

주요한 다른 점은 <form class="lift:form.ajax">입니다. 이것은 Lift의 내장된 form 스니펫을 호출하고 현재 폼을 Ajax폼으로 지정합니다. 스니펫은 다음과 같습니다.

리스팅 4.15: AjaxExample.scala

package code
package snippet

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

/**
 * 프로세싱을 위한 Ajax... Stateful 예제와 아주 비슷해 보입니다
 */
object AjaxExample {
  def render = {
    // 상태
    var name = ""
    var age = "0"
    val whence = S.referer openOr "/"

    // process 메서드는 응답의 일부로써 
    // 브라우저에 다시 보내질 JsCmd를 리턴합니다
    def process(): JsCmd= {

      // 사용자가 스피너 아이콘을 볼 수 있도록
      // 400 밀리초동안 sleep합니다
      Thread.sleep(400)

      // 매칭을 합니다
      asInt(age) match {
        // 에러를 보여주거나 아니면 아무것도 하지 않습니다
        case Full(a) if a < 13 => S.error("age", "Too young!"); Noop

        // 사용자가 온 페이지로 리다이렉트하고
        // 그 페이지에서 화면을 표시합니다
        case Full(a) => {
          RedirectTo(whence, () => {
            S.notice("Name: "+name)
            S.notice("Age: "+a)
          })
        }

        // 더 많은 오류들
        case _ => S.error("age", "Age doesn't parse as a number"); Noop
      }
    }

    // 바인딩은 평범해 보입니다
    "name=name" #> SHtml.text(name, name = _, "id" -> "the_name") &
    "name=age" #> (SHtml.text(age, age = _) ++ SHtml.hidden(process))
  }
}

코드는 Stateful 코드랑 많이 비슷합니다. 처리되는 age필드에 히든필드를 추가해야 하기 때문에 submit 버튼(submit버튼은 Ajax에서는 시리얼라이즈(serialize)되지 않습니다.)에 바인딩하지 않을 것은 빠졌습니다.

process()메서드는 Ajax 폼제출에 대한 응답안에 브라우져로 다시 보내는 JavaScript 커맨드인 JsCmd를 리턴합니다. 이 경우에 Noop에 따라오는 오류를 화면에 표시하기 위해서 S.error을 사용하고 리다이렉트를 합니다.

Ajax작업이 수행되는 곳이라는 것을 알려주기 위해서 사용자가 브라우저에서 스피너(spinner)을 볼 수 있도록 process()메서드안에서 400밀리초동안 멈춥니다.

하지만 일반적인 HTML 프로세싱과 Ajax 프로세싱의 핵심은 거의 동일하고 둘다 아주 쉽다.



4.9 하지만 때로는 구식이 좋다(But sometimes Old Fashioned is good)

이번 장에서 Lift의 폼 구성과 프로세싱 특징들을 보았고 서버의 함수와 클라이언트의 GUID를 연결하는 것의 능력과 가치를 보여주었습니다. 하지만 때로는 URL파라미터를 통한 파라미터 처리를 갖는 것이 좋습니다. 그리고 Lift에서 이것을 하는 것도 역시 쉽습니다.

이 챕터를 위한 예제들에서 모든 페이지는 다음 코드를 가지고 있습니다.

<form action="/query">
  <input name="q">
  <input type="submit" value="Search">
</form>

http://localhost:8080/query?q=catfood같은 URL을 생성하는 평범한 폼입니다. 이 URL은 복사될 수 있고 잘라질 수 있고 공유될 수 있습니다.

이 URL을 처리하는 것은 쉽습니다.

리스팅 4.16: Query.scala

package code
package snippet

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

object Query {
  def results = ClearClearable andThen
  "li *" #> S.param("q"). // 쿼리파라미터를 얻습니다
  toList. // Box를 List로 변환합니다
  flatMap(q => {
    ("You asked: "+q) :: // 쿼리 앞에 붙힙니다
    (1 to toInt(q)).toList.map(_.toString) // Int로 변환될 수 있다면
    // 변환하고 Int의 sequence를 리턴합니다
  })
}

S.param("param_name")을 사용해서 쿼리파라미터를 추출하고 그걸로 무언가를 수행합니다.



4.10 결론

Lift의 폼생성과 처리도구는 전체 HTTP 요청의 일부이거나 Ajax 요청을 통해서 안전하고 간단하면서 강력한 HTML폼을 생성하고 처리하는 넓고 다양한 메카니즘을 제공합니다.
  1. 상태유지등에 대해서 당황하기 전에 20장 Lift and State에 대해서 읽어보세요 [Back]
  2. 상태가 없는(stateless) 스니펫은 없습니다. 상태유지 스니펫은 SHtml.onSubmit()로 구성된 폼보다 서버사이드 리소스들은 전혀 더 소비하지 않습니다. 그리고 상태는 확장에 대한 장벽이 아닙니다. 20장을 보세요. [Back]
  3. 일찌감치 숨겨진 폼 파라미터의 보안 함축에 대해서 얘기했습니다. 숨겨진 파라미터 메카니즘은 같은 이슈에 대해서 취약하지 않습니다. 왜냐하면 숨겨진 파라미터 그 자체가 서버에서 호출되는 함수를 야기시키는 그냥 GUID이기 때문입니다. 클라이언트로 노출되는 상태가 없기 때문에 취약점을 이용하려고 해커가 획득하거나 변경시킬수 있는 것이 없습니다. [Back]
  4. 이 경우에 “요청”은 전체 HTML 페이지 로드와 페이지에 수반되는 모든 Ajax 연산을 의미합니다. 또한 현재 HTTP 요청의 범위를 가지는 TransientRequestVar가 있습니다. [Back]
2011/02/15 01:43 2011/02/15 01:43