본문 바로가기

스프링/MVC 2편

타임리프 기본 기능

728x90

타임리프 사용 선언
<html xmlns:th="http://www.thymeleaf.org">

 

텍스트 - text, utext


타임리프의 가장 기본 기능인 텍스트를  출력하는 기능

 

타임리프는 기본적으로 HTML 테그의 속성에 기능을 정의해서 동작한다. HTML의 콘텐츠(content)에 데이터를 출력할 때는 다음과 같이 th:text 를 사용하면 된다.

 

<span th:text="${data}">

 

HTML 테그의 속성이 아니라 HTML 콘텐츠 영역안에서 직접 데이터를 출력하고 싶으면 다음과 같이 [[...]] 를 사용하면 된다.


컨텐츠 안에서 직접 출력하기 = [[${data}]]

 

컨트롤러

@Controller
@RequestMapping("/basic")
public class BasicController {

    @GetMapping("text-unescaped")
    public String textUnescaped(Model model){
        model.addAttribute("data", "Hello <b>Spring!</b>");
        return "basic/text-unescaped";
    }
}

 

html

<h1>text vs utext</h1>
<ul>
    <li>th:text = <span th:text="${data}"></span></li>
    <li>th:utext = <span th:utext="${data}"></span></li>
</ul>
<h1><span th:inline="none">[[...]] vs [(...)]</span></h1>
<ul>
    <li><span th:inline="none">[[...]] = </span>[[${data}]]</li>
    <li><span th:inline="none">[(...)] = </span>[(${data})]</li>
</ul></body>
</html>

 

나오는 결과

 

 

th:text를 사용하면 escape 처리가 되고, th:utext를 사용하면 escape 처리가 되지 않고, 태그를 렌더링

 

기본적으로는 escape 처리가 되도록 개발을 해야 함

 

변수 - SpringEL


타임리프에서 변수를 사용할 때는 변수 표현식을 사용한다.


변수 표현식 : ${...}

 

그리고 이 변수 표현식에는 스프링 EL이라는 스프링이 제공하는 표현식을 사용할 수 있다.

 

컨트롤러

@GetMapping("/variable")
public String variable(Model model) {
    //Object 사용
    User userA = new User("userA", 10);
    User userB = new User("userB", 20);
    
    //List 사용
    List list =  new ArrayList<>();
    list.add(userA);
    list.add(userB);

    //Map 사용
    Map<String, User> map = new HashMap<>();
    map.put("userA", userA);
    map.put("userB", userB);

    //모델에 담아서 뷰에서 출력할 수 있도록 함
    model.addAttribute("user", userA);
    model.addAttribute("users", list);
    model.addAttribute("userMap", map);

    return "basic/variable";
}

//내부에서 사용할 샘플 데이터 선언해주는거
@Data
static class User {
    private String username;
    private int age;

    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

 

 

html

h1>SpringEL 표현식</h1>
<ul>Object <li>${user.username} = <span th:text="${user.username}"></span></li>
  <li>${user['username']} = <span th:text="${user['username']}"></span></li>
  <li>${user.getUsername()} = <span th:text="${user.getUsername()}"></span></li>
</ul>
<ul>List
  <li>${users[0].username} = <span th:text="${users[0].username}"></span></li>
  <li>${users[0]['username']} = <span th:text="${users[0]['username']}"></span></li>
  <li>${users[0].getUsername()} = <span th:text="${users[0].getUsername()}"></span></li>
</ul>
<ul>Map
  <li>${userMap['userA'].username} = <span th:text="${userMap['userA'].username}"></span></li>
  <li>${userMap['userA']['username']} = <span th:text="${userMap['userA']['username']}"></span></li>
  <li>${userMap['userA'].getUsername()} = <span th:text="${userMap['userA'].getUsername()}"></span></li>
</ul>

<h1>지역 변수 - (th:with)</h1>
<div th:with="first=${users[0]}">
  <p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>

 

Object

user.username : user의 username을 프로퍼티 접근 user.getUsername()
user['username'] : 위와 같음 user.getUsername()
user.getUsername() : user의 getUsername() 을 직접 호출


List

users[0].username : List에서 첫 번째 회원을 찾고 username 프로퍼티 접근
list.get(0).getUsername()
users[0]['username'] : 위와 같음users[0].getUsername() : List에서 첫 번째 회원을 찾고 메서드 직접 호출


Map

userMap['userA'].username : Map에서 userA를 찾고, username 프로퍼티 접근
map.get("userA").getUsername()
userMap['userA']['username'] : 위와 같음
userMap['userA'].getUsername() : Map에서 userA를 찾고 메서드 직접 호출

 

 

결과는 위와 같이 출력

 

th:with 를 사용하면 지역 변수를 선언해서 사용할 수 있다. 지역 변수는 선언한 태그 안에서만 사용할 수 있다.

 

기본 객체들


타임리프는 기본 객체들을 제공한다.


${#request}
${#response}
${#session}
${#servletContext}
${#locale}

 

그런데 #request 는 HttpServletRequest 객체가 그대로 제공되기 때문에 데이터를 조회하려면 request.getParameter("data") 처럼 불편하게 접근해야 한다.

 

HTTP 요청 파라미터 접근: param
예) ${param.paramData}


HTTP 세션 접근: session
예) ${session.sessionData}


스프링 빈 접근: @
예) ${@helloBean.hello('Spring!')}

 

위의 내용이 이러한 불편한 점을 해결하기 위한 것이다.

 

컨트롤러

@GetMapping("/basic-objects")
public String basicObjects(HttpSession session) {
    session.setAttribute("sessionData", "Hello Session");
    return "basic/basic-objects";
}

//data객체를 반환하는 HelloBean을 스프링 빈으로 등록
@Component("helloBean")
static class HelloBean{
    public String hello(String data) {
        return "Hello " + data;
    }
}

 

이 내용을 컨트롤러에 추가하고 

 

<h1>편의 객체</h1>
<ul>
    <li>Request Parameter = <span th:text="${param.paramData}"></span></li>
    <li>session = <span th:text="${session.sessionData}"></span></li>
    <li>spring bean = <span th:text="${@helloBean.hello('Spring!')}"></span><li>
</ul>

 

위의 html 파일을 실행시키면 

 

 

이러한 결과가 출력된다.

 

<li><a href="/basic/basic-objects?paramData=HelloParam">기본 객체들</a></li>

 

파라미터 값은 index.html에서 이 페이지로 들어로 때 쿼리 스트링에 담아서 넘겨준 값을 param.paramData로 받아서 그 값을 출력한 것이고,

 

세션 값은 session.sessionData는 컨트롤러에서 session.setAttribute로 세션에 담아둔 값을 가져온 것이다.

 

스프링 빈은 직접 입력이 가능한 것도 확인할 수 있다.

 

이렇게 param, session, @을 통한 스프링 빈 접근과 같이 편리하게 그 값들을 불러올 수 있다.

 

유틸리티 객체와 날짜


타임리프는 문자, 숫자, 날짜, URI등을 편리하게 다루는 다양한 유틸리티 객체들을 제공한다.

 

타임리프 유틸리티 객체들은 아래와 같이 있다.

 

#message : 메시지, 국제화 처리
#uris : URI 이스케이프 지원
#dates : java.util.Date 서식 지원
#calendars : java.util.Calendar 서식 지원
#temporals : 자바8 날짜 서식 지원
#numbers : 숫자 서식 지원
#strings : 문자 관련 편의 기능
#objects : 객체 관련 기능 제공
#bools : boolean 관련 기능 제공
#arrays : 배열 관련 기능 제공
#lists , #sets , #maps : 컬렉션 관련 기능 제공
#ids : 아이디 처리 관련 기능 제공, 뒤에서 설명

 

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#expression-utilityobjects

 

Tutorial: Using Thymeleaf

1 Introducing Thymeleaf 1.1 What is Thymeleaf? Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text. The main goal of Thymeleaf is to provide a

www.thymeleaf.org

사용할 때는 위의 링크에 들어가서 그 부분을 찾아보면 된다.

 

날짜 사용

@GetMapping("/date")
public String date(Model model) {
    model.addAttribute("localDateTime", LocalDateTime.now());
    return "basic/date";
}

 

컨트롤러에 위와 같이 넣어주고

 

<li>yyyy-MM-dd HH:mm:ss = <span th:text="${#temporals.format(localDateTime,'yyyy-MM-dd HH:mm:ss')}"></span></li>

 

해당 경로의 html에 위와 같이 넣어주면 우리가 원하는 방식대로 날짜 포매팅을 거쳐서 출력할 수 있다.

#temporals 이게 날짜용 유틸리티 객체다.

 

 

<li>${#temporals.day(localDateTime)} = <span th:text="${#temporals.day(localDateTime)}"></span></li>
<li>${#temporals.month(localDateTime)} = <span th:text="${#temporals.month(localDateTime)}"></span></li>
<li>${#temporals.monthName(localDateTime)} = <span th:text="${#temporals.monthName(localDateTime)}"></span></li>
<li>${#temporals.monthNameShort(localDateTime)} = <span th:text="${#temporals.monthNameShort(localDateTime)}"></span></li>
<li>${#temporals.year(localDateTime)} = <span th:text="${#temporals.year(localDateTime)}"></span></li>
<li>${#temporals.dayOfWeek(localDateTime)} = <span th:text="${#temporals.dayOfWeek(localDateTime)}"></span></li>
<li>${#temporals.dayOfWeekName(localDateTime)} = <span th:text="${#temporals.dayOfWeekName(localDateTime)}"></span></li>
<li>${#temporals.dayOfWeekNameShort(localDateTime)} = <span th:text="${#temporals.dayOfWeekNameShort(localDateTime)}"></span></li>
<li>${#temporals.hour(localDateTime)} = <span th:text="${#temporals.hour(localDateTime)}"></span></li>
<li>${#temporals.minute(localDateTime)} = <span th:text="${#temporals.minute(localDateTime)}"></span></li>
<li>${#temporals.second(localDateTime)} = <span th:text="${#temporals.second(localDateTime)}"></span></li>
<li>${#temporals.nanosecond(localDateTime)} = <span th:text="${#temporals.nanosecond(localDateTime)}"></span></li>

 

위와 같이 다양한 방식으로 날짜를 뽑아내는 것도 참조하면 좋을 듯하다.

 

 

URL 링크


타임리프에서 URL을 생성할 때는 @{...} 문법을 사용하면 된다

 

@GetMapping("link")
public String link(Model model) {
    model.addAttribute("param1", "data1");
    model.addAttribute("param2", "data2");
    return "basic/link";
}

 

컨트롤러를 위와 같이 생성하고

 

<ul>
    <li><a th:href="@{/hello}">basic url</a></li>

    /hello?param1=data1&param2=data2
    <li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">hello query param</a></li>

    /hello/{param1}/{param2} => /hello/data1/data2
    <li><a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li>

    /hello/{param1}?param2=data2 => /hello/data1?param2=data2
    <li><a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li>
</ul>

 

위와 같이 link.html을 생성한다.

각 형태에 따른 url값은 위에 적어놓은 것으로 참고하면 된다.

 

마지막 경우의 경우 경로 변수와 관련해서 param1은 있지만 param2는 없는 것을 확인할 수 있는데, 이런 경우 있는 것은 자동으로 치환되고, 없는 것은 자동으로 뒤에 쿼리 파라미터로 붙게 된다.

 

단순한 URL

@{/hello} /hello


쿼리 파라미터

@{/hello(param1=${param1}, param2=${param2})}
/hello?param1=data1&param2=data2
() 에 있는 부분은 쿼리 파라미터로 처리된다.


경로 변수

@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}
/hello/data1/data2
URL 경로상에 변수가 있으면 () 부분은 경로 변수로 처리된다.


경로 변수 + 쿼리 파라미터

@{/hello/{param1}(param1=${param1}, param2=${param2})}
/hello/data1?param2=data2
경로 변수와 쿼리 파라미터를 함께 사용할 수 있다.
상대경로, 절대경로, 프로토콜 기준을 표현할 수 도 있다.


/hello : 절대 경로
hello : 상대 경로

 

리터럴


리터럴은 소스 코드상에 고정된 값을 말하는 용어이다.
예를 들어서 다음 코드에서 "Hello" 는 문자 리터럴, 10 , 20 는 숫자 리터럴이다.

 

String a = "Hello"
int a = 10 * 20

 

타임리프는 다음과 같은 리터럴이 있다.


문자: 'hello'
숫자: 10
불린: true , false
null: null

 

타임리프에서 문자 리터럴은 항상 ' (작은 따옴표)로 감싸야 한다.


<span th:text="'hello'"> 이러한 내용을


그런데 문자를 항상 ' 로 감싸는 것은 너무 귀찮은 일이다. 공백 없이 쭉 이어진다면 하나의 의미있는 토큰으로 인지해서 다음과 같이 작은 따옴표를 생략할 수 있다. 


룰: A-Z , a-z , 0-9 , [] , . , - , _


<span th:text="hello"> 이렇게 작은 따옴표를 생략하고 작성할 수 있다.


하지만 아래의 경우에는 오류가 발생한다.


<span th:text="hello world!"></span>


그 이유는 hello world는 중간에 공백이 있기 때문에 문자 리터럴의 공식대로 '로 묶어 주어야 하기 때문이다.


<span th:text="'hello world!'"></span> 이렇게 ' 로 감싸면 정상 동작한다.

 

@GetMapping("/literal")
public String literal(Model model) {
    model.addAttribute("data", "Spring!");
    return "basic/literal";

 

컨트롤러에 위의 내용을 추가한다.

 

<h1>리터럴</h1>
<ul>
    <!--주의! 다음 주석을 풀면 예외가 발생함-->
    <!-- <li>"hello world!" = <span th:text="hello world!"></span></li>-->
    <li>'hello' + ' world!' = <span th:text="'hello' + ' world!'"></span></li>
    <li>'hello world!' = <span th:text="'hello world!'"></span></li>
    <li>'hello ' + ${data} = <span th:text="'hello ' + ${data}"></span></li>
    <li>리터럴 대체 |hello ${data}| = <span th:text="|hello ${data}|"></span></li>
</ul>

 

html의 내용을 위와 같이 작성하고 실행하면

 

 

이렇게 확인할 수 있다.

 

기억할 것은 그냥 띄어쓰기가 안돼있는 문자의 경우에는 '를 생략 가능하고, 공백이 있는 문자의 경우에는 '로 감싸줘야 한다는 것이다.

 

리터럴 대체(Literal substitutions)

<span th:text="|hello ${data}|">


마지막의 리터럴 대체 문법을 사용하면 마치 템플릿을 사용하는 것 처럼 편리하다.

 

연산


타임리프 연산은 자바와 크게 다르지 않다. HTML안에서 사용하기 때문에 HTML 엔티티를 사용하는 부분만 주의하자.

 

@GetMapping("/operation")
public String operation(Model model) {
    model.addAttribute("nullData", null);
    model.addAttribute("data", "Spring!");
    return "basic/operation";
}

 

컨트롤러에 위의 내용을 추가한다. 기본적인 연산의 경우는 그냥 자바와 같기 때문에 특이한 연산만 아래에 예제로 작성한다.

 

<li>조건식
  <ul>
    <li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)?'짝수':'홀수'"></span></li>
  </ul>
</li>
<li>Elvis 연산자 - 데이터가 없을 때 사용
  <ul>
    <!--데이터가 있으면 ${data}의 내용이 출력되고, 없으면 뒤의 문구가 출력됨-->
    <li>${data}?: '데이터가 없습니다.' = <span th:text="${data}?: '데이터가없습니다.'"></span></li>
    <li>${nullData}?: '데이터가 없습니다.' = <span th:text="${nullData}?: '데이터가 없습니다.'"></span></li>
  </ul>
</li>
<li>No-Operation : _를 사용하는 것
  <ul>
    <li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</span></li>
    <!--값이 없으면 타임리프 태그를 무시하고 기본 값을 출력하도록 하는 것-->
    <li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가 없습니다.</span></li>
  </ul>
</li>

 

 

비교연산

HTML 엔티티를 사용해야 하는 부분을 주의하자, 
> (gt), < (lt), >= (ge), <= (le), ! (not), == (eq), != (neq, ne)


조건식

자바의 조건식과 유사하다.


Elvis 연산자

조건식의 편의 버전


No-Operation

_ 인 경우 마치 타임리프가 실행되지 않는 것 처럼 동작한다. 이것을 잘 사용하면 HTML의 내용 그대로 활용할 수 있다. 마지막 예를 보면 데이터가 없습니다. 부분이 그대로 출력된다.

 

속성 값 설정


타임리프 태그 속성(Attribute)

타임리프는 주로 HTML 태그에 th:* 속성을 지정하는 방식으로 동작한다. 

th:* 로 속성을 적용하면 기존 속성을 대체하며, 기존 속성이 없으면 새로 만든다.

 

<input type="text" name="mock" th:name="userA" />

 

즉 위와 같이 이미 name에 "mock"가 선언되어 있지만, th:name으로 "userA"를 지정해주면 userA가 name으로 들어가게 되는 것이다.

 

@GetMapping("/attribute")
public String attribute() {
    return "basic/attribute";
}

 

컨트롤러에 위의 내용을 지정하고 html을 실행한다.

 

<h1>속성 설정</h1>
<input type="text" name="mock" th:name="userA"/>

<h1>속성 추가</h1>
<!--class의 뒤에 붙임-->
- th:attrappend = <input type="text" class="text" th:attrappend="class=' large'" /><br/>
<!--class의 앞에 붙임-->
- th:attrprepend = <input type="text" class="text" th:attrprepend="class='large '" /><br/>
<!--알아서 적절하게 띄어쓰기 설정해서 붙여줌-->
- th:classappend = <input type="text" class="text" th:classappend="large" /><br/>

<h1>checked 처리</h1>
- checked o <input type="checkbox" name="active" th:checked="true" /><br/>
- checked x <input type="checkbox" name="active" th:checked="false" /><br/>
- checked=false <input type="checkbox" name="active" checked="false" /><br/>

 

 

페이지에서 소스를 확인해보면 위와 같이 자동으로 랜더링 된 것을 확인할 수 있음

 

checked 처리의 경우에도 그냥 html에서는 false든 true든 checked가 선언되면 무조건 체크된다. 하지만 th:checked를 사용하면 불린 값에 따라서 처리를 해준다.

 

 

반복


타임리프에서 반복은 th:each 를 사용한다. 추가로 반복에서 사용할 수 있는 여러 상태 값을 지원한다.

 

@GetMapping("/each")
public String each(Model model) {
    addUsers(model);
    return "basic/each";
}

private void addUsers(Model model) {
    List<User> list = new ArrayList<>();
    list.add(new User("UserA", 10));
    list.add(new User("UserB", 20));
    list.add(new User("UserC", 30));

    model.addAttribute("users", list);
}

@Data
static class User {
    private String username;
    private int age;

    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

 

위와 같이 컨트롤러 내용 작성, 모델에 "users"라는 이름으로 list의 내용을 주입

 

<h1>기본 테이블</h1>
<table border="1">
    <tr>
        <th>username</th>
        <th>age</th>
    </tr>
    <tr th:each="user : ${users}">
        <td th:text="${user.username}">username</td>
        <td th:text="${user.age}">0</td>
    </tr>
</table>

 

 

list에 있는 값들이 반복되어서 출력

 

반복시 오른쪽 컬렉션( ${users} )의 값을 하나씩 꺼내서 왼쪽 변수( user )에 담아서 태그를 반복 실행

 

th:each 는 List 뿐만 아니라 배열, java.util.Iterable , java.util.Enumeration 을 구현한 모든 객체를 반복에 사용할 수 있다. Map 도 사용할 수 있는데 이 경우 변수에 담기는 값은 Map.Entry이다.

 

<h1>반복 상태 유지</h1>
<table border="1">
    <tr>
        <th>count</th>
        <th>username</th>
        <th>age</th>
        <th>etc</th>
    </tr>
    <tr th:each="user, userStat : ${users}">
        <td th:text="${userStat.count}">username</td>
        <td th:text="${user.username}">username</td>
        <td th:text="${user.age}">0</td>
        <td>
            index = <span th:text="${userStat.index}"></span>
            count = <span th:text="${userStat.count}"></span>
            size = <span th:text="${userStat.size}"></span>
            even? = <span th:text="${userStat.even}"></span>
            odd? = <span th:text="${userStat.odd}"></span>
            first? = <span th:text="${userStat.first}"></span>
            last? = <span th:text="${userStat.last}"></span>
            current = <span th:text="${userStat.current}"></span>
        </td>
    </tr>
</table>

 

 

이건 현재 반복이 어떻게 되고 있는지 그 상태를 알려주는 기능이다.

 

<tr th:each="user, userStat : ${users}">

 

이렇게 적어주면 첫 번째 user에는 ${users}에서 가져온 값들이 하나씩 들어가는 것이고, 두 번째 userStat에는 현재 반복에 대한 상태를 알려준다.

 

두번째 파라미터는 생략 가능한데, 생략하면 지정한 변수명( user ) + Stat 가 된다. 여기서는 user + Stat = userStat 이므로 생략 가능

 

반복 상태 유지 기능

index : 0부터 시작하는 값
count : 1부터 시작하는 값
size : 전체 사이즈
even odd : 홀수, 짝수 여부( boolean )
first last :처음, 마지막 여부( boolean )
current : 현재 객체

 

조건부 평가


타임리프의 조건식에는 if , unless ( if 의 반대)가 있다.

 

if, unless

타임리프는 해당 조건이 맞지 않으면 태그 자체를 렌더링하지 않는다.
만약 아래의 조건이 false 인 경우 <span>...<span> 부분 자체가 렌더링 되지 않고 사라진다.


<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>


switch

* 은 만족하는 조건이 없을 때 사용하는 디폴트이다.

 

@GetMapping("/condition")
public String condition(Model model){
    addUsers(model);
    return "basic/condition";
}

private void addUsers(Model model) {
    List<User> list = new ArrayList<>();
    list.add(new User("UserA", 10));
    list.add(new User("UserB", 20));
    list.add(new User("UserC", 30));

    model.addAttribute("users", list);
}

 

컨트롤러 위와 같이 작성

 

<h1>if, unless</h1>
<table border="1">
  <tr>
    <th>count</th>
    <th>username</th>
    <th>age</th>
  </tr>
  <tr th:each="user, userStat : ${users}">
    <td th:text="${userStat.count}">1</td>
    <td th:text="${user.username}">username</td>
    <td>
      <span th:text="${user.age}">0</span>
      <span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
      <span th:text="'미성년자'" th:unless="${user.age ge 20}"></span>
    </td>
  </tr>
</table>

 

if와 unless를 사용하는 예제 html을 위와 같이 작성하고 실행하면 

 

 

위와 같은 결과가 나온다. 

1번은 if의 경우 userA의 나이가 10살이기 때문에 20살보다 어려서 true이기에 미성년자 출력되었고, if의 반대 역할을 하는 unless는 20살보다 클 경우의 반대이기 때문에 마찬가지로 true라서 미성년자 문구가 출력된다.

 

하지만 2, 3번의 경우 두 조건문에 대해서 false가 나오기 때문에 그냥 두 라인이 삭제되고, 나이만 출력된다.

 

<h1>switch</h1>
<table border="1">
  <tr>
    <th>count</th>
    <th>username</th>
    <th>age</th>
  </tr>
  <tr th:each="user, userStat : ${users}">
    <td th:text="${userStat.count}">1</td>
    <td th:text="${user.username}">username</td>
    <td th:switch="${user.age}">
      <span th:case="10">10살</span>
      <span th:case="20">20살</span>
      <span th:case="*">기타</span>
    </td>
  </tr>
</table>

 

switch를 사용하는 예제 html을 위와 같이 작성하고 실행하면

 

 

이런 결과가 출력된다. user.age의 값이 10이면 10살, 20이면 20살이 출력되고 그 외의 경우에는 기타가 출력되도록 case에 *을 설정했다.


주석


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>예시</h1>
<span th:text="${data}">html data</span>
<h1>1. 표준 HTML 주석</h1>
<!--
<span th:text="${data}">html data</span>-->
<h1>2. 타임리프 파서 주석</h1>
<!--/* [[${data}]] */-->
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->
<h1>3. 타임리프 프로토타입 주석</h1>
<!--/*/
<span th:text="${data}">html data</span>
/*/-->
</body>
</html>

 

 

실행 결과의 소스코드를 보면 위와 같이 나온다.

 

1. 표준 HTML 주석

자바스크립트의 표준 HTML 주석은 타임리프가 렌더링 하지 않고, 그대로 남겨둔다.


2. 타임리프 파서 주석

타임리프 파서 주석은 타임리프의 진짜 주석이다. 렌더링에서 주석 부분을 제거한다.

 

3. 타임리프 프로토타입 주석

타임리프 프로토타입은 약간 특이한데, HTML 주석에 약간의 구문을 더했다.
HTML 파일을 웹 브라우저에서 그대로 열어보면 HTML 주석이기 때문에 이 부분이 웹 브라우저가 렌더링하지 않는다.
타임리프 렌더링을 거치면 이 부분이 정상 렌더링 된다.


쉽게 이야기해서 HTML 파일을 그대로 열어보면 주석처리가 되지만, 타임리프를 렌더링 한 경우에만 보이는 기능이다.

 

블록


<th:block> 은 HTML 태그가 아닌 타임리프의 유일한 자체 태그다.

 

타임리프는 보통 속성으로 동작하지 태그로 동작하지 않지만 해결하기 어려운 것들을 해결하기 위해서 타임리프가 제공하는 이 블록이 필요할 때가 있다.

 

@GetMapping("/block")
public String block(Model model) {
    addUsers(model);
    return "basic/block";
}

 

앞서 계속 사용했던 정보 3개를 모델에 넣고 block 페이지로 이동

 

<!--div 2개를 같이 반복시키기 위해서 사용-->
<!--기존처럼 div에 th:each를 넣으면 그 div만 따로 반복됨-->
<th:block th:each="user : ${users}">
    <div>
        사용자 이름1 <span th:text="${user.username}"></span>
        사용자 나이1 <span th:text="${user.age}"></span>
    </div>
    <div>
        요약 <span th:text="${user.username} + ' / ' + ${user.age}"></span>
    </div>
</th:block>

 

이렇게 자체 태그인 블록을 사용해서 div 2개를 묶고 블록에 each를 적용시키면 블록 자체가 반복되면서 div 2개가 하나로 묶여서 반복되는 것을 확인할 수 있음

 

 

 

타임리프의 특성상 HTML 태그안에 속성으로 기능을 정의해서 사용하는데, 위 예처럼 이렇게 사용하기 애매한 경우에 사용하면 된다. 

 

<th:block> 은 렌더링시 제거된다.

 

자바스크립트 인라인


타임리프는 자바스크립트에서 타임리프를 편리하게 사용할 수 있는 자바스크립트 인라인 기능을 제공한다.
자바스크립트 인라인 기능은 다음과 같이 적용하면 된다.


<script th:inline="javascript">

 

@GetMapping("/javascript")
public String javascript(Model model) {
    model.addAttribute("user", new User("userA", 10));
    addUsers(model);
    return "basic/javascript";
}

 

먼저 컨트롤러 위와 같이 작성

 

<!-- 자바스크립트 인라인 사용 전 -->
<!--js에서는 태그를 사용해서 값을 렌더링할 수 없기 때문에 값을 바로 출력하는 [[${}]]문법을 사용하면 된다.-->
<script>
  var username = [[${user.username}]];
  var age = [[${user.age}]];

  //자바스크립트 내추럴 템플릿
  var username2 = /*[[${user.username}]]*/ "test username";

  //객체
  var user = [[${user}]];
</script>

<!-- 자바스크립트 인라인 사용 후 -->
<script th:inline="javascript">
  var username = [[${user.username}]];
  var age = [[${user.age}]];

  //자바스크립트 내추럴 템플릿
  var username2 = /*[[${user.username}]]*/ "test username";

  //객체
  var user = [[${user}]];
</script>

 

자바스크립트 코드가 있는 html 파일 생성

 

 

페이지 소스코드를 보면 위와 같이 나타난다.

 

아래는 자바스크립트 인라인을 사용하지 않을 때의 오류와 인라인을 사용하면 이를 어떻게 해결할 수 있는지에 관한 내용이다.

 

텍스트 렌더링

var username = [[${user.username}]];
var age = [[${user.age}]];

 

var username = [[${user.username}]];

- 인라인 사용 전 : var username = userA;
- 인라인 사용 후 : var username = "userA";


인라인 사용 전 렌더링 결과를 보면 userA 라는 변수 이름이 그대로 남아있다. 타임리프 입장에서는 정확하게 렌더링 한 것이지만 기대한 것은 "userA"라는 문자이다.


결과적으로 userA가 변수명으로 사용되어서 자바스크립트 오류가 발생한다. 다음으로 나오는 숫자 age의 경우에는 " 가 필요 없기 때문에 정상 렌더링 된다.


인라인 사용 후 렌더링 결과를 보면 문자 타입인 경우 " 를 포함해준다. 추가로 자바스크립트에서 문제가 될 수 있는 문자가 포함되어 있으면 이스케이프 처리도 해준다. 


자바스크립트 내추럴 템플릿

타임리프는 HTML 파일을 직접 열어도 동작하는 내추럴 템플릿 기능을 제공한다. 자바스크립트 인라인 기능을 사용하면 주석을 활용해서 이 기능을 사용할 수 있다.

 

var username2 = /*[[${user.username}]]*/ "test username";


var username2 = /*[[${user.username}]]*/ "test username";

- 인라인 사용 전 : var username2 = /*userA*/ "test username";
- 인라인 사용 후 : var username2 = "userA";


인라인 사용 전 결과를 보면 정말 순수하게 그대로 해석을 해버렸다. 따라서 내추럴 템플릿 기능이 동작하지 않고,  렌더링 내용이 주석처리 되어 버린다.

 

인라인 사용 후 결과를 보면 주석 부분이 제거되고, 기대한 "userA"가 정확하게 적용된다.


객체

타임리프의 자바스크립트 인라인 기능을 사용하면 객체를 JSON으로 자동으로 변환해준다.

 

var user = [[${user}]];


var user = [[${user}]];

- 인라인 사용 전 : var user = BasicController.User(username=userA, age=10);
- 인라인 사용 후 : var user = {"username":"userA","age":10};


인라인 사용 전은 객체의 toString()이 호출된 값이다.
인라인 사용 후는 객체를 JSON으로 변환해준다.

 

자바스크립트 인라인 each

<!-- 자바스크립트 인라인 each -->
<script th:inline="javascript">
  [# th:each="user, stat : ${users}"]
  var user[[${stat.count}]] = [[${user}]];
  [/]
</script>

 

자바 스크립트에서 반복문을 돌려야 하는 경우에는 위와 같이 적어준다. 위의 코드로 얻고자하는 값은 아래와 같다.

 

 

사용 방법은 아래처럼 하면 된다.

 

[# 기존의 th:each 사용법]...[/]

 

var user[[${stat.count}]] 부분은 상태, [[${user}]] 부분은 객체이기 때문에 JSON으로 반환

 

템플릿 조각 - jsp에 include 같은 기능


 페이지를 개발할 때 상단 영역이나 하단 영역, 좌측 카테고리 등등 여러 페이지에서 함께 사용하는 공통 영역이 많이 있다.

 

이런 부분을 코드를 복사해서 사용한다면 변경시 여러 페이지를 다 수정해야 하므로 상당히 비효율 적이다.

 

타임리프는 이런 문제를 해결하기 위해 템플릿 조각과 레이아웃 기능을 지원한다.

 

@Controller
@RequestMapping("/template")
public class TemplateController {

    @GetMapping("/fragment")
    public String template(){
        return "template/fragment/fragmentMain";
    }
}

 

새로운 컨트롤러 생성

 

<body>

<footer th:fragment="copy">
    푸터 자리 입니다.
</footer>

</body>

 

페이지 하단 역할을 하는 footer.html 생성

th:fragment의 이름으로 copy를 지정

 

<body>

<h1>부분 포함</h1>
<h2>부분 포함 insert</h2>
<div th:insert="~{template/fragment/footer :: copy}"></div>

<h2>부분 포함 replace</h2>
<div th:replace="~{template/fragment/footer :: copy}"></div>

<h2>부분 포함 단순 표현식</h2>
<div th:replace="template/fragment/footer :: copy"></div>

</body>

 

메인 페이지를 생성

 

여기서 template/fragment/footer :: copytemplate/fragment/footer.html 템플릿에 있는 th:fragment="copy" 라는 부분을 템플릿 조각으로 가져와서 사용한다는 의미이다.

 

 

그럼 위와 같이 출력된다.

 

 

페이지 소스를 보면 알겠지만, insert를 한 것은 그 div의 자리에 footer의 내용이 들어가는 것이고, replace를 사용하면 그 부분이 삭제되고, footer의 내용으로 바뀌게 된다.

 

템플릿 조각을 사용하는 코드가 단순하면 ~{}부분을 생략할 수 있지만, 정석적인 형식은 붙이는 것

 

<body>

<footer th:fragment="copyParam (param1, param2)">
    <p>파라미터 자리 입니다.</p>
    <p th:text="${param1}"></p>
    <p th:text="${param2}"></p>
</footer>

</body>

 

위와 같이 파라미터 값을 넣을 수도 있다. 변수가 바인딩돼서 넘어가고, 이를 받으면 바인딩 된 값이 출력된다.

 

<body>

<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>

</body>

 

위와 같이 파라미터를 전달해서 동적으로 조각을 렌더링 할 수도 있다.

 

 

위와 같이 출력된다.

 

템플릿 레이아웃 1 - 템플릿 레이아웃


이전에는 일부 코드 조각을 가지고와서 사용했다면, 템플릿 레이아웃은 코드 조각을 레이아웃에 넘겨서 사용하는 방법이다.


예를 들어서 <head> 에 공통으로 사용하는 css , javascript 같은 정보들이 있는데, 이러한 공통 정보들을 한 곳에 모아두고, 공통으로 사용하지만, 각 페이지마다 필요한 정보를 더 추가해서 사용하고 싶다면 다음과 같이 사용하면 된다.

 

@GetMapping("/layout")
public String layout() {
    return "template/layout/layoutMain";
}

 

컨트롤러 코드 작성

 

<head th:fragment="common_header(title,links)">

  <title th:replace="${title}">레이아웃 타이틀</title>

  <!-- 공통 -->
  <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
  <link rel="shortcut icon" th:href="@{/images/favicon.ico}">

  <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>

  <!-- 추가 -->
  <th:block th:replace="${links}" />

</head>

 

layoutMain.html에 넣어줄 base.html을 만든다.

이 파일은 우리의 홈페이지에 헤더와 관련된 어떤 공통 레이아웃이라고 볼 수 있다.

 

<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
    <title>메인 타이틀</title>
    <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
    <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>

<body>
메인 컨텐츠
</body>

 

layoutMain.html 생성

여기서 base.html 파일을 가져다 쓴다.

 

<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">

 

먼저 이 부분을 통해서 메인에서는 내가 이 파일을 만들 때 지정한 헤더를 사용하지 않고, base에 있는 저 헤더로 교체할 것이라고 선언했다는 것을 알 수 있다. 

 

 이때 그냥 바꾸는 것이 아니다. 공통 레이아웃이 있더라도 각 사이트(페이지?)마다 조금씩 다르게 하고싶은 부분이 있을 것이다. 

 레이아웃 틀이 있고, 거기에 들어가는 css나 jquery부분에 차이를 두거나 혹은 추가 내용 부분등이 있을 것이다. 여기서 틀은 base가 될 것이고, 거기에 들어가는 내용은 main의 내용이 될 것이다.

 

 

실행 결과 페이지의 소스코드를 보면 위와 같이 나오는 것을 볼 수 있다.

 

우리는 main의 헤더 부분을 base.html 파일의 내용으로 바꿨는데, <title>의 내용을 보면 base의 것인 '레이아웃 타이틀'이 아니라 main에 있는 <title>의 내용인 '메인 타이틀'이 들어가 있는 것을 확인 가능하다.

 

<link>의 경우도 공통 부분의 <link>가 그대로 남아있음과 동시에 main페이지에서 추가 부분에 전달한 <link> 들이 포함된 것을 확인할 수 있다.

 

main페이지의 링크는 base의 블록에 들어가 한줄, 한줄 나오게 된다.

 

이러한 이유는 common_header(~{::title},~{::link}) 부분이 이유가 된다.

 

::title 은 현재 페이지의 title 태그들을 전달하고,
::link 는 현재 페이지의 link 태그들을 전달한다.

 

정리를 해보면 컨트롤러에서 layoutMain을 호출한다.

 

layoutMain은 <head>의 replace로  base의 common_header를 불러서 base의 <head>로 교체를 하는데, 이때 layoutMain의 title, link의 값을 넘겨준다.

 

이제 바뀔 것인데, 이 때 base의 기본 레이아웃은 그대로 가지고 있고, main에서 넘겼던 title과 link들의 값으로 렌더링 되어서 바뀌게 된다. 공통적인 부분은 그대로 공통 사항으로 렌더링 된다.

 

즉, base는 헤드를 구성하는 거대한 하나의 레이아웃이고, 여기에 일부분은 내가 원하는 것(페이지마다의 차이)으로 채울 수 있다.

 

쉽게 이야기해서 큰 틀인 레이아웃 개념을 두고, 그 레이아웃에 부분마다 필요한 코드 조각을 전달해서 완성하는 것으로 이해하면 된다.

 

템플릿 레이아웃2 - 템플릿 레이아웃 확장


템플릿 레이아웃을 위에서 처럼 <head>의 한 부분에만 적용시키는 것이 아니라 <html> 전체에 적용시킬 수도 있다.

 

@GetMapping("/layoutExtend")
public String layoutExtends() {
    return "template/layoutExtend/layoutExtendMain";
}

 

컨트롤러에 내용 추가

 

<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">

<head>
    <title th:replace="${title}">레이아웃 타이틀</title>
</head>

<body>

<h1>레이아웃 H1</h1>
<div th:replace="${content}">
    <p>레이아웃 컨텐츠</p>
</div>

<footer>
    레이아웃 푸터
</footer>

</body>
</html>

 

layoutFile.html 생성

이 html 파일이 껍데기가 된다. 아무리 많은 페이지가 있어도 모두 형식은 위와 동일하게 가는 것이라고 보면 된다. 

 

이 파일에서 페이지마다 변경되는 부분은 오직 <div> 부분 뿐이어야 한다.(컨텐츠 부분만 변경된다는 소리)

 

<!DOCTYPE html>
<html th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title}, ~{::section})}" xmlns:th="http://www.thymeleaf.org">

<head>
  <title>메인 페이지 타이틀</title>
</head>

<body>

<section>
  <p>메인 페이지 컨텐츠</p>
  <div>메인 페이지 포함 내용</div>
</section>

</body>
</html>

 

layoutExtendMain.html 생성

이건 개별 페이지라고 보면 된다. 이전에 <head> 부분과 마찬가지의 형태로 html 자체를 th:replace 태그를 사용해서 갈아치울 것이다.

 

이때 넘겨줄 값은 <title> 부분과 <section> 부분이다. 이렇게 넘겨준 각 페이지의 title은 껍데기의 title과 교체될 것이고, section은 껍데기의 <div th:replace="${content}"> 부분에 들어가서 내용을 각 페이지의 값으로 변경시켜서 화면을 표현하게 되는 것이다.

 

 

출력 결과를 보면 공통으로 설정된 <h1>태그와 <footer> 부분을 제외하고는 layoutExtendMain.html의 내용으로 교체되어 표현되는 것을 확인할 수 있다.

728x90