4장
폼(Forms)
이번 장에서 Lift가 템플릿을 어떻게 처리하는지 보겠습니다. 다중페이지 인풋폼과 Ajax폼을 두루 지원하는 구식 방법(디자이너가 잇풋에 이름을 붙이고 애플리케이션이 이 이름들은 변수들에 매핑하는 방법)의 폼프로세싱부터 시작하겠습니다.
4.1 구식의 똑똑하지 않은 폼
폼에 대한 HTML을 보겠습니다.
리스팅 4.1: dumb.html
꽤 일반적으로 보입니다. 폼을 정의했습니다. 이제 해야할 유일한 것은 <form>태그의 class="lift:DumbForm"속성을 가진 폼과 동작을 연동하는 것입니다. 페이지는 본래의 컨텐츠를 제공하는 같은 URL로 폼이 포스트(post)되는 것을 의미하는 포스트백(post-back)입니다.
코드가 폼을 처리하는 것을 보겠습니다.
리스팅 4.2: DumbForm.scala
꽤 간단합니다. 요청이 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
이 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
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
템플릿은 onsubmit.html의 템플릿처럼 괜찮아 보입니다. 스니펫을 보겠습니다.
리스팅 4.6: Stateful.scala
공정한 양의 다른 점이 있습니다. 첫째 클래스 정의: 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
이제 스니펫 코드를 보겠습니다.
리스팅 4.8: ReqVar.scala
스니펫은 RequestVars에 상태를 저장하기 때문에 싱글톤입니다.
<input>태그를 생성하려고 SHtml.textElem()를 사용합니다. 생성된 RequestVar을 get/set하는 메서드와 펑션으로 RequestVar를 건네줄 수 있습니다.
상태유지 폼의 메카니즘과 StatefulSnippet 메카니즘의 사용은 개인적인 선택입니다. 더 나은 것이 아니라 단지 다를 뿐입니다.
이제 에러 메시지와 함께 더 많은 과립(granular)을 어떻게 얻는지 보겠습니다.
4.5 필드 오류(Field Errors)
이전의 예제에서 사용자에게 오류를 노출했습니다. 그러나 어떤 필드에서 오류가 생겼는지는 말해주지 않습니다. 이게 오류 리포팅에 대해서 좀더 많은 정보를 주도록 하겠습니다.
먼저 HTML을 보겠습니다.
리스팅 4.9: fielderror.html
이 HTML은 Age: <span class="lift:Msg?id=age&errorClass=error">error</span>부분이 다릅니다. 오류메시지를 둘 영역을 마크업안에 표시했습니다.
Stateful.scala와 아주 유사한 스니펫 코드를 보겠습니다. 작지만 중요한 다른 점이 있습니다.
리스팅 4.10: FieldErrorExample.scala
주요한 다른 점은 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
폼 엘리먼트를 명시적으로 선언하지 않았습니다. 단지 다음과 같은 스니펫을 가리켰을 뿐입니다.
리스팅 4.12: ScreenExample.scala
화면에서 필드들과 그것들의 유효성 검사 규칙들을 정의하고 화면이 종료되었을때 무엇을 해야하는지를 정의합니다. 나머지는 Lift가 담당합니다.
기본적으로 폼을 생성하기 위한 마크업은 /templates-hidden/wizardall.html에서 찾을 수 있습니다. 또한 screen-by-screen 원칙에 따라 템플릿을 선택할 수 있습니다.
4.7 Wizard
LiftScreen은 단일 화면 애플리케이션에는 아주 좋습니다. 다중화면에 필요한 인풋과 유효성 검사를 가지고 있다면 Wizard가 바로 당신이 원하는 것입니다. 단지 스니펫 호출을 일으키는 마크업은 건너뛰겠습니다. wizard 코드가 여기 있습니다.
리스팅 4.13: WizardExample.scala
위의 화면 예제처럼 단지 선언적일 뿐입니다. 뒤로가기 버튼은 동작합니다. 웹브라우저에서 다중탭에서 동작하는 다중 wizard를 갖을 수 있고 서로 간섭하지 않습니다.
4.8 Ajax
전체 페이지 HTML에서 추가적으로 Lift는 Ajax 폼을 지원합니다. Lift의 폼은 브라우저에서 GUID들과 연결된 서버사이드 함수이기 때문에 폼을 전체페이지 로드에서 Ajax로 전환하는 것은 아주 사소한 것입니다. 마크업을 보겠습니다.
리스팅 4.14: ajax.html
주요한 다른 점은 <form class="lift:form.ajax">입니다. 이것은 Lift의 내장된 form 스니펫을 호출하고 현재 폼을 Ajax폼으로 지정합니다. 스니펫은 다음과 같습니다.
리스팅 4.15: AjaxExample.scala
코드는 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에서 이것을 하는 것도 역시 쉽습니다.
이 챕터를 위한 예제들에서 모든 페이지는 다음 코드를 가지고 있습니다.
http://localhost:8080/query?q=catfood같은 URL을 생성하는 평범한 폼입니다. 이 URL은 복사될 수 있고 잘라질 수 있고 공유될 수 있습니다.
이 URL을 처리하는 것은 쉽습니다.
리스팅 4.16: Query.scala
S.param("param_name")을 사용해서 쿼리파라미터를 추출하고 그걸로 무언가를 수행합니다.
4.10 결론
Lift의 폼생성과 처리도구는 전체 HTTP 요청의 일부이거나 Ajax 요청을 통해서 안전하고 간단하면서 강력한 HTML폼을 생성하고 처리하는 넓고 다양한 메카니즘을 제공합니다.
- 상태유지등에 대해서 당황하기 전에 20장 Lift and State에 대해서 읽어보세요 [Back]
- 상태가 없는(stateless) 스니펫은 없습니다. 상태유지 스니펫은 SHtml.onSubmit()로 구성된 폼보다 서버사이드 리소스들은 전혀 더 소비하지 않습니다. 그리고 상태는 확장에 대한 장벽이 아닙니다. 20장을 보세요. [Back]
- 일찌감치 숨겨진 폼 파라미터의 보안 함축에 대해서 얘기했습니다. 숨겨진 파라미터 메카니즘은 같은 이슈에 대해서 취약하지 않습니다. 왜냐하면 숨겨진 파라미터 그 자체가 서버에서 호출되는 함수를 야기시키는 그냥 GUID이기 때문입니다. 클라이언트로 노출되는 상태가 없기 때문에 취약점을 이용하려고 해커가 획득하거나 변경시킬수 있는 것이 없습니다. [Back]
- 이 경우에 “요청”은 전체 HTML 페이지 로드와 페이지에 수반되는 모든 Ajax 연산을 의미합니다. 또한 현재 HTTP 요청의 범위를 가지는 TransientRequestVar가 있습니다. [Back]
구식의 bind방식만 써오다가 새로운 bind방식으로 바꿔서 하다보니 ajaxForm을 어떻게 만들어야 할지 고민중이었는데 그냥 앞뒤로 태그만 쳐덕 붙이면 되는거였네요 ㅎㅎ;
한가지 문제가 크롬에서 <input type="email" /> 입력폼의 경우 ajax submit을 해도 snippet에서 값을 입력받지 못하네요
예전엔 <input type="image" />로 일반 submit을 하는경우 IE에서만 제대로 동작이 안되서 애먹은적이 있었는데... 에고 참;;
Lift로 개발을 하시나 보군요..
저도 좀 익혀볼려고 위 문서만 번역해서 올려놓고 사정상 Lift를 만져보지는 못하고 있네요 ㅠㅠ