GSP(Groovy Server Page)

GSP(Groovy Server Page)


GSP는 Grails의 뷰 기술이다.
이 것은 ASP와 JSP 사용자에게 친숙하도록 설계됐다. 하지만 좀 더 유연하고 직관적으로 설계하였다.
Grails의 GSP는 grails-app/views 디렉토리에 있다.
관례(Convension)에 따라서 자동적으로 렌더링되도록 할 수 있고 render 메소드를 사용할 수도있다.(Convension 으로 자동으로 view 페이지를 찾아도 되지만 Controller의 Action에서 명시적으로 지정할 수 도 있다는 의미)
Convention이란 view는 grails-app/views/ 컨트롤러 이름/액션.[gsp | jsp] 규칙으로 자동으로 view 페이지를 찾아준다.

GSP Basics





Groovy 로직(<% %> 스클립틀릿 구문)을 GSP 안에 넣을 수도 있지만 권장되는 방법도 아니고,
JSP 에서 사용되는 스크립틀릿 구문과 페이지 지시어 등 사용법도 같고,
실제로 사용되지도 않기때문에 설명은 생략함


Expressions


GSP 표현식은 JSP의 EL 표현식, Groovy의 GString과 유사할 뿐만 아니라 ${표현식}의 형태로 사용한다
JSP와 다르게 ${..}블럭 안에 Groovy 표현식을 사용할 수 있다

view.jsp를 아래와 같이 변경한다.
확인주소 : http://localhost:8080/mygrails/book/view?name=kshmeme
hello, ${params.name}!



BookController 의 내용을 아래와 같이 변경한다.

package net.grails.my

class BookController {	

def view ={	
}	
}




변경후 실행하면


Controller에서 model 데이터를 넘겨주지 않아도 GSP 표현식 에서 Groovy 표현식을 사용할 수 있음을 알 수 있다.
(JSP 표현식과 가장 큰 차이점이라고 생각됨)


GSP에서는 ${..}블럭에 있는 변수의 값이 그대로 표현된다.(JSP도 마찬가지 이지만..)

XSS(Cross-Site-Scripting)공격의 위험을 줄이기 위해 자동으로 HTML을 제거하도록 설정할 수 있다.

먼저 스크립트가 실행되는 예제를 살펴보면..
BookController 의 내용을 아래와 같이 변경한다.
확인주소 : http://localhost:8080/mygrails/book/view?name=kshmeme

package net.grails.my

class BookController {	

def view ={	
		def hello = "<script>alert('hello, "+params.name+"');</script>"
		
		[hello : hello]
	}	
}




view.gsp의 내용을 아래와 같이 변경한다


${hello}


아래와같이 alert()메시지로 인삿말이 나오는 것을 확인할 수 있다.




HTML 제거하는 방법은
grails-app/conf/Config.groovy 파일에서 코덱을 설정한다.
grails.views.default.codec 값이 기본으로 설정되어 있다면 html로 변경하고
없다면 추가한다.
코덱 변경 후 서버재구동을 한다.

grails.views.default.codec='html'






설정변경후 서버 재구동 후 확인을 해보면

경고창으로 인삿말이 나오던 페이지가
아래 이미지처럼 텍스트로 나오게 된다.




GSP Tags


GSP의 빌트인 태그들에 대해서 설명한다
Tag라이브러리를 만드는 방법은 추후 설명예정 (Tag Libraries)
빌트인GSP태그는 g: 로 시작한다.

변수를 정의하는 set 태그를 이용하여 예제를 실행해본다.
view.gsp를 아래와 같이 수정한다.
확인주소: http://localhost:8080/mygrails/book/view

<%@ page contentType="text/html;charset=UTF-8" %>
<!--  방법 1 -->
<g:set var="now" value="${new Date()}">   
</g:set>

<!--  방법 2 -->
<g:set var="now2" >
${new Date()}   
</g:set>

방법1: ${now} <br />
방법2: ${now2}






gsp태그를 사용해야 하므로 page 지시어를 삽입했다
첫번째 방법처럼 attribute로 전부 지정을 해도되고 value 속성은
두번째 방법처럼 태그바디에 지정을 해도된다.







Variables and Scopes



변수의 scope의 종류는 아래와 같다.
page - 현재 페이지에서만(기본).
request - 현재 요청에서만
flash - flash 스콥에 저장되므로 다음 요청까지 살아남는다.
session - 사용자의 세션에서
application - 어플리케이션 어디에서나 살아남는다.

gsp 에서의 flash만 새로운 개념이다.
나머지는 jsp 에서 존재하는 개념이다.



Logic and Iteration


조건문과 반복문에 대해서 설명한다.

조건문(if, elseif, else) 에 대한예제를 살펴보면
view.gsp를 아래와 같이 변경한다.

확인주소: http://localhost:8080/mygrails/book/view?role=admin
파라미터를 user, 또는 다른 값으로도 변경하여 확인해본다.



<%@ page contentType="text/html;charset=UTF-8" %>
<g:if test="${params.role=='admin'}" >
	관리자 입니다.
</g:if>
<g:elseif test="${params.role=='user'}" >
일반사용자입니다.
</g:elseif>
<g:else>
로그인하지 않았습니다.
</g:else>





파라미터가 role 이 admin이면 관리자, user이면 일반사용자 아니면 로그인하지 않은 사용자를 나타낸다.

아래와같이 관리자, 일반사용자 등 메시지가 표시된다



반복문(each, while)에 대한 예제
view.gsp의 소스를 아래와 같이 변경한다.

<%@ page contentType="text/html;charset=UTF-8" %>
<g:each in="${1..3}"  var="num">
<p>each : ${num}</p>
</g:each>

<g:set var="num" value="${1}"></g:set>
<g:while test="${num<4 }">
<p>while : ${num++}</p>
</g:while>





each문 : in : 데이터(배열, Collection 등) var: 반복문 안에서 사용되는 지역변수
while문 : num이 4보다 작으면 반복문(while)실행


아래와같이 브라우저에 출력되는 걸 확인할 수 있다.





Search and Filtering


객체의 컬랙션을 정렬하고 필터링이 필요할 때 findAll과 grep을 사용할 수 있다.
Book 도메인 클래스를 아래와 같이 변경한다
확인주소 : http://localhost:8080/mygrails/book/view

package net.grails.my

class Book {

    static constraints = {
    }
	
	String name;
	String userid;
	String author;
}





BookController 클래스를 아래와 같이 변경한다


package net.grails.my

class BookController {	

def view ={	

	//데이터 생성
	def lists = new ArrayList<Book>()
	def nameList = ["Groovy in Action", "토비의 스프링", "HeadFirst JSP"]
	def authorList =["디에크쾨니히", "tobi", "케이시시에라"]
	(0..2).each {
		seq->
		Book temp = new Book()
		temp.name = nameList[seq]
		temp.author = authorList[seq]
		lists.add(temp)
	}
	        //생선된 데이터를 view 에 전달
		render(view:"/book/view",model:[list:lists])
	}	
}




view.gsp를 아래와 같이 변경한다
<%@ page contentType="text/html;charset=UTF-8" %>
<!-- in : 데이터(배열, Collection 등) var: 반복문 안에서 사용되는 지역변수 -->


<g:findAll in="${list}" expr="it.author=='tobi'">
	<p>book name : ${it.name}, author : ${it.author }</p>
</g:findAll>


<g:grep in="${list.name }" filter="~/.*?Groovy.*?/">
<p>book name : ${it}
</g:grep>




in : 반복에 들어갈 객체
expr : GPath 표현식
filter : 정규식 또는 정규식을 구현한 필터 클래스


아래에서 결과를 확인할 수 있다.


첫번째 bookname 은 findAll을 이용하여 저자가 'tobi' 결과를 보여주었고,
두번째 bookname은 grep을 이용하여 정규식을 이용하여 책 이름에 Groovy가 들어간 결과를 보여주었다


Links and Resources


컨트롤러와 액션을 쉽게 연결 할 수 있는 태그도 지원한다( 태그를 자동으로 만들어준다.)
이 것은 URL 매핑(URL Mappings)에 의존하여 자동으로 행해진다
확인주소 : http://localhost:8080/mygrails/book/view

view.gsp를 아래와 같이 변경한다.


<%@ page contentType="text/html;charset=UTF-8" %>

<!-- 현재 컨트롤러에 show action  -->
<g:link action="show" id="1">Book 1</g:link><br />
<!-- book controller에 default action -->
<g:link controller="book">Book Home</g:link><br />
<!-- book controller에 list action -->
<g:link controller="book" action="list">Book List</g:link><br />
<!-- book controller list action 또 다른 표현방식-->
<g:link url="[action:'list',controller:'book']">Book List</g:link><br />
<!-- list action에 쿼리스트링지정, 기타 속성들-->
<g:link action="list"  target="_blank" params="[sort:'title',order:'asc']"  userattr="userattaVal">
     Book List
</g:link><br />




브라우저에서 확인을 해보면



태그가 생성된 걸 확인할 수 있다

실제 렌더링 된 소스는


link tag에서 알지 못하는 속성은 그대로 html로 렌더링 해 준다


Forms and Fields


GSP에서 HTML 폼과 필드를 다루는 여러 태그 중 하나이다.
HTML의 form 태그에 컨트롤러/액션을 이해할 수 한 것이다.
view.gsp 소스를 아래와 같이 변경한다.
확인주소 : http://localhost:8080/mygrails/book/view

<%@ page contentType="text/html;charset=UTF-8" %>

<g:form name="myForm" url="[controller:'book', action:'list']">
book/list로 submit 되는 form tag

</g:form>








화면을 띄우고 소스보기를 해보면



url에서 지정한 controller와 action에 맞게 from 태그의 action을 지정한 것을 볼 수 있다.
그 외에도 여러가지 태그들을 GSP에서 만들 수 있다.
자세한 다른 태그들은
http://grails.org/doc/latest/ref/Tags/grep.html
에서 더 확인할 수 있다.


Views and Templates


Template Basics(템플릿의 기초)


Grails는 템플릿을 식별하기 위해서 뷰의 이름 앞에 '_'를 붙이는 관례를 사용한다
Book을 렌더링 하는 템플릿은 grails-app/views/book/_bookTemplate.gsp에 위치한다

아래와 같이 bookTemplate이라는 이름의 템플릿을 만든다






bookTemplate 이라는 이름의 템플릿은 _bookTemplate.gsp 라는 이름의 gsp 파일을 만든다(under bar)

확인주소: http://localhost:8080/mygrails/book/view

_bookTemplate.gsp 의 파일 내용을 아래와 같이 변경한다

<div class="book" id="${book?.id}">
   <div>Title: ${book?.title}</div>
   <div>Author: ${book?.author}</div>
</div>




BookController 의 내용을 아래와 같이 변경한다

package net.grails.my

class BookController {

    def index() { }
	def view = {
		Book book = new Book()
		book.title = "Groovy in Action"
		book.author = "쾨니히"
		book.id = 1
		
		
		[myBook:book]
	}
}




view.gsp의 내용을 아래와 같이 변경한다


<%@ page contentType="text/html;charset=UTF-8" %>
<g:render template="bookTemplate" model="[book:myBook]" />





아래와 같이 _bookTemplate.gsp 의 내용을 view.gsp 에서 가져다 사용한 것을 알 수 있다.
view에서 템플릿을 렌더링 하려면 render 태그를 사용한다.
model=[book:myBook]
에서 book은 템플릿으로 넘기는 이름이고 myBook은 Controller에서 넘어온 모델이다




템플릿에 컬렉션 형태로도 넘길 수 있다.
BookController 의 내용을 아래와 같이 변경한다.
확인주소 : http://localhost:8080/mygrails/book/view


package net.grails.my

class BookController {

    def index() { }
	def view = {

		List<Book> lists = new ArrayList<Book>();
		(1..3).each{
			seq ->
			Book temp = new Book()
			temp.title = "title" + seq
			temp.author = "author" + seq
			temp.id = seq
			lists.add( temp)
		}
		
		[lists : lists]
	}
}






view.gsp의 내용을 아래와 같이 변경한다.

<%@ page contentType="text/html;charset=UTF-8" %>
<g:render template="bookTemplate" collection="${lists}" var="lists" />





_bookTemplate.gsp의 내용을 아래와 같이 변경한다.

<%@ page contentType="text/html;charset=UTF-8" %>


<div class="list">
	<g:each in="${lists }" var="book">
		<div class="book" id="${book.id}">
			<div>
				Title:
				${book.title}
			</div>
			<div>
				Author:
				${book.author}
			</div>
		</div>
		<br />
	</g:each>
</div>





아래와 같이 _bookTemplate.gsp 에서 렌더링 한 내용을 확인할 수 있다






Shared Templates(템플릿 공유하기)



grails-app/views/book/_bookTemplate.gsp 템플릿은 book 컨트롤러에서 사용한 템플릿이지만
어플리케이션 전체에서 사용가능하다.
보통 공유되는 템플릿은 grails-app/views/shared 폴더에 저장한다.
공유폴더에 템플릿을 하나 만들어보면..
먼저 view/하위에 shared 폴더를 생성한다.






shared 폴더 하위에 _mySharedTemplate.gsp 라는 템플릿을 하나 생성한다




_mySharedTemplate.gsp의 내용을 아래와 같이 변경한다(bookTemplate.gsp 와동일)


<%@ page contentType="text/html;charset=UTF-8" %>


<div class="list">
	<g:each in="${lists }" var="book">
		<div class="book" id="${book.id}">
			<div>
				Title:
				${book.title}
			</div>
			<div>
				Author:
				${book.author}
			</div>
		</div>
		<br />
	</g:each>
</div>





view.gsp의 내용을 아래와 같이 mySharedTemplate.gsp 템플릿을 사용하는 형태로 변경한다.
grails-app/views/shared/_mySharedTemplate.gsp 이라는 템플릿을 사용하기위해서는
grails-app/views 하위 경로부터 작성한다 /shared/mySharedTemplate(under bar 는 삭제한것에 유의)
확인주소 : http://localhost:8080/mygrails/book/view



<%@ page contentType="text/html;charset=UTF-8" %>
<g:render template="/shared/mySharedTemplate" collection="${lists}" var="lists" />




아래와같이 결과를 확인할 수 있다



Layouts with Sitemesh


Creating Layouts(레이아웃 만들기)


sitemesh에 기반한 레이아웃 지원한다.
레이아웃은 grails-app/views/layouts 디렉터리에 위치한다


레이아웃 폴더에 list.gsp 라는 레이아웃을 하나 생성한다





grails-app/views/layouts /list.gsp 의 내용을 아래처럼 변경한다.

<!DOCTYPE html>
	<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
		<title><g:layoutTitle default="default Title"/></title>
		<g:layoutHead/>
	</head>
	<body>
		<div>header~!!!</div>
		
		<div id="mainContents">		
			<g:layoutBody/>
		</div>
		<div>
		footer~!
		</div>
	</body>
</html>




layoutTitle - 대상 페이지의 제목을 출력
layoutHead - 대상 페이지의 head 태그의 내용을 출력
layoutBody - 대상 페이지의 body 태그의 내용을 출력


Triggering Layouts(레이아웃 사용하기)


여러가지 방법 중 가장 단순한 방법은 meta 태그를 이용하는 방법이다
확인주소 : http://localhost:8080/mygrails/book/view2

book/view2.gsp를 만든다.(만드는 과정 생략)
view2.gsp의 내용을 아래와 같이 변경한다.



<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR"/>
<meta name="layout" content="list"/>
<title>title 입니다~@_@</title>
<script type="text/javascript">

function msg(t){
	alert(t);
}
</script>
</head>
<body>
  <div class="body">
  view2 페이지 입니다!!
  </div>
</body>
</html>


view2.gsp의 내용은 아주 단순한 html페이지 이다.


BookController의 내용을 아래와 같이 변경한다.(view2 액션을 생성하였다)


package net.grails.my

class BookController {

    def index() { }
	def view = {

		List<Book> lists = new ArrayList<Book>();
		(1..3).each{
			seq ->
			Book temp = new Book()
			temp.title = "title" + seq
			temp.author = "author" + seq
			temp.id = seq
			lists.add( temp)
		}
		
		[lists : lists]
	}
	
	def view2 = {
		
	}
}




실행후 확인한다.


아래는 실제 렌더링된 html의 소스보기 이미지이다.


view2의 title이 list.gsp의 영역으로,
view2의 header 영역이 list.gsp의 영역으로,
view2의 body 영역이 list.gsp의 영역으로 삽입되어 렌더링 되어 진 것을 확이할 수 있다.


이게 가능한 이유는 view2.gsp에서 5번째 line에서

<meta name="layout" content="list"/>


라고 지정을 하였기 때문에 list 레이아웃이 사용되어진 것이다.


레이아웃을 사용하는 두 번째 방법은 관례(Convention)을 따르는 것이다.
예를들면 BookController에 list 액션에만 적용되는 레이아웃을 만들고 싶다면
/layout/book/list.gsp 파일을 생성하면된다.
확인주소 : http://localhost:8080/mygrails/book/list




/layout/book/list.gsp 내용을 아래와 같이 변경한다

<!DOCTYPE html>
	<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
		<title><g:layoutTitle default="default Title"/></title>
		<g:layoutHead/>
	</head>
	<body>
		<div>book/list/header~!!!</div>
		
		<div id="mainContents">		
			<g:layoutBody/>
		</div>
		<div>
		book/list/footer~!
		</div>
	</body>
</html>





BookController의 내용을 아래와 같이 변경한다.(list 액션 추가하였다)


package net.grails.my

class BookController {

    def index() { }
	def view = {

		List<Book> lists = new ArrayList<Book>();
		(1..3).each{
			seq ->
			Book temp = new Book()
			temp.title = "title" + seq
			temp.author = "author" + seq
			temp.id = seq
			lists.add( temp)
		}
		
		[lists : lists]
	}
	
	def view2 = {
		
	}
	
	def list = {
	
}
}



view/book/list.gsp 추가한다




list.gsp의 내용을 아래와 같이 변경한다

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR"/>

<title>Insert title here</title>
</head>
<body>
  <div class="body">
  book/list 입니다.
  </div>
</body>
</html>





실행후 확인하면 아래와 같이 /layout/book/list.gsp 레이아웃이 적용된 걸 확인할 수 있다.




Tag Libraries


사용자 정의 태그라이브러리를 지원한다.
만드는 방법은
1. Taglib로 끝나는 클래스를 만든다.
2. grails-app/taglib 디렉토리에 넣는다.

아래처럼 SimpleTaglib를 만들어보자.





SimpleTaglib 의 내용을 아래와 같이 변경한다

package mygrails

class SimpleTagLib {
	def dateFormat = { attrs, body ->
		out << new java.text.SimpleDateFormat(attrs.format).format(attrs.date)
	}
}



dafeformat 이라는 메서드는 attrs, body 두개의 인자를 받는다.
attrs : 태그의 속성
body : 태그의 본문내용
out : Stream(output Writer에 대한 참조)

dateFormat 은 format 속성(날짜 포맷)과 date(날짜 인스턴스)를 전달받아 문자열로 출력 하는 예제이다.

view.gsp를 아래와 같이 변경한다

<%@ page contentType="text/html;charset=UTF-8" %>

<g:dateFormat format="yyyy-MM-dd" date="${new Date() }"  ></g:dateFormat>




SimpleTagLib의 dateFormat 에서 사용되는 attrs를 정의한 것을 확인할 수 있다.

실행하면 아래 그림처럼 현재 날짜(date 속성)가 지정한 포맷(format 속성)으로 브라우저에출력되는 것을 확인할 수 있다.








Logical Tags



특정 조건이 만족돼야만 결과를 출력하는 논리 태그를 만들 수도 있다
예를들면 관리자일 경우에만 관리자 버튼이 화면에 나온다던지 하는..


IsAdminTagLib를 만들어보자.
SimpleTagLib 와 같은 위치에 IsAdminTagLib Groovy 클래스를 생성한다.

IsAdminTagLib 의 내용을 아래와 같이 변경한다



package mygrails

class IsAdminTagLib {
	
		def isAdmin = {attrs, body ->
			def user = attrs["user"]
			if(user != null && user == "kshmeme"){
				out << body()	
			}
		}
	
}








BookController의 내용을 아래와 같이 변경한다


package net.grails.my

class BookController {

   def view = {
	   def userid = ""
	   
	   if(params.containsKey("userid")){
		   
		   userid = params.userid
	   }
	   
	   ["userid" : userid]
   }
}






view.gsp의 내용을 아래와 같이 변경한다.



  <a href="javascript:;">마이페이지</a>
  <g:isAdmin user="${userid}">
  <a href="javascript:;">관리자</a>
</g:isAdmin>




BookController 의 view Action 에서 전달받은 userid 데이터를 IsAdminTagLib 의 isAdmin 메서드로 전달한다.
userid가 kshmeme 일 경우에만 관리자 버튼이 나타난다.
확인주소 : http://localhost:8080/mygrails/book/view?userid=kshmeme


아래와 같이 확인할 수 있다.





Iterative Tags



body를 여러번 실행 가능하게 할 수 있다.
IsAdminTagLib 의 소스를 아래와 같이 변경한다.


package mygrails

class IsAdminTagLib {
		def repeat = { attrs, body ->
			attrs.times?.toInteger().times { num ->
				out << body(num)
			}
		}
}





view.gsp의 내용을 아래와 같이 변경한다



<div>
<g:repeat times="3">
<p>Repeat this 3 times! Current repeat = ${it}</p>
</g:repeat>

</div>






times? : times가 null이면 코드실행 중단하고 null 리턴


확인주소 : http://localhost:8080/mygrails/book/view

IsAdminTagLib 에서는 times attribute가 존재하면 int로 변환 후 times 숫자만큼 loop를 돌며body를 출력한다.
이 때 현재 index(num)을 파라미터로 넘겨준다.


gsp에서는 IsAdminTagLib(repeat) 에서 전달받은 num을 it 이라는 이름으로 접근하여 출력 하였다.


문제점 : it 이라는 변수 이름은 태그를 중첩하여 사용할 경우 충돌을 일으킬 수 있기때문에
body 에서 사용할 변수이름을 지정해야한다.



IsAdminTagLib의 소스를 아래와 같이 변경한다.



package mygrails

class IsAdminTagLib {
	

def repeat = { attrs, body ->
	def var = attrs.var ? attrs.var : "num"
    attrs.times?.toInteger().times { num ->
        out << body((var):num)
    }
}
}





reapeat 태그에 var 속성이 존재하면 그대로 사용하고 존재하지 않을경우에는 num 이라는 이름으로 접근 할 수 있다.



view.gsp의 코드를 아래와 같이 변경한다

------------------------------------------------<br />
var 속성이 존재할 경우<br />
------------------------------------------------<br />
<div>
<g:repeat times="3"  var="repeatNum">
<p>Repeat this 3 times! Current repeat = ${repeatNum}</p>
</g:repeat>

</div>

<br />
------------------------------------------------<br />
var 속성이 존재하지 않을 경우<br />
------------------------------------------------<br />
<div>
<g:repeat times="3"  >
<p>Repeat this 3 times! Current repeat = ${num}</p>
</g:repeat>

</div>




아래와 같이 결과를 확인할 수 있다




두 개의 결과는 같지만 body 안의 변수의 이름이 다른 걸 확인할 수 있다




Tag Namespaces


태그는 자동으로 Grails의 기본네임스페이스에 추가되고 GSP 페이지에서 "g:" 접두어로 태그를 사용한다.
태그 하지만 이를 변경 할 수 있다.


IsAdminTagLib 클래스에 아래와 같이 namespace 멤버를 추가한다.


static namespace = "my"



서버 재구동 후 확인해보면 아래와 같이 에러가 발생한다.




g namespace에 repeat 태그가 존재하지 않는다 라는 에러 메시지를 확인할 수 있다.



view.gsp의 내용을 아래와 같이 g: 로 시작된 부분을 my로 변경한다.



------------------------------------------------<br />
var 속성이 존재할 경우<br />
------------------------------------------------<br />
<div>
<my:repeat times="3"  var="repeatNum">
<p>Repeat this 3 times! Current repeat = ${repeatNum}</p>
</my:repeat>

</div>

<br />
------------------------------------------------<br />
var 속성이 존재하지 않을 경우<br />
------------------------------------------------<br />
<div>
<my:repeat times="3"  >
<p>Repeat this 3 times! Current repeat = ${num}</p>
</my:repeat>

</div>




그러면 다시 정상적으로 작동하는 걸 확인할 수 있다