Spring

Thymeleaf - Basic

ch-yang 2023. 2. 17. 20:34

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

Thymeleaf는 가이드 문서가 잘 정리 되어 있다.
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

타임리프 사용 선언

<html xmlns:th="http://www.thymeleaf.org">
  ...
</html>

xmlns : 해당 문서를 위한 XML 네임스페이스(Namespace)를 명시한다.

텍스트 - text, utext

  • HTML 엔티티 : '<' 와 같은 HTML의 특수 문자를 문자로 표현하는 방법이 필요한데, 이를 HTML 엔티티라 한다.
  • Escape : 특수 문자를 HTML 엔티티로 변경하는 것을 Escape라 한다.
  • Escape (th:text, [[...]]), Unescape (th:utext, [(...)])

1. 태그 안에서 속성으로 정의

<ul>
  <li>th:text = <span th:text="${data}"></span></li>
  <li>th:utext = <span th:utext="${data}"></span></li>
</ul>

2. 콘텐츠 안에 직접 데이터 입력

<ul>
  <li><span th:inline="none">[[...]] = </span>[[${data}]]</li>
  <li><span th:inline="none">[(...)] = </span>[(${data})]</li>
</ul>

SpringEL (Expression Language)

Thymeleaf에서는 변수 표현식 ${...} 으로 변수를 표현하는데여기에 스프링이 제공하는 표현식(SpringEL)을 사용할 수 있다. 

다양한 표현식 사용 방법이 있지만, 다음의 표현식만 사용해야겠다.

// Object
th:text="${user.username}"

// List
th:text="${users[0].username}

// Map
th:text="${userMap['userA'].username}"

지역 변수 선언

<div th:with="first=${users[0]}">
    <p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>

기본 객체들

request, response, 세션, 빈 등도 접근이 가능하지만, 현재로서는 필요해 보이는 것이 없어 Skip. HTTP 요청 파라미터 접근만 알면 될 것 같다.

// url?paramData=hello
<ul>
  <li>Request Parameter = <span th:text="${param.paramData}"></span></li>
</ul>

 유틸리티 객체와 날짜

더보기

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

유틸리티에서 필요한건 나중에 필요할 때 찾아보도록 링크만 남겨야겠다.

#temporals 사용법 일부만 정리

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

<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>

URL

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

() 에 있는 부분은 쿼리 파라미터로 처리가 되는데, 경로상에 PathVariable이 있으면 경로 변수에 먼저 할당되고 남은 파라미터만 쿼리 파라미터가 된다.

상대경로, 절대경로, 프로토콜 기준(참고 링크)을 표현할 수 도 있다.

리터럴

리터럴 : 소스 코드상에서 고정된 값을 말한다 (ex. 'hello', 10, true, false, null)

타임리프에서 문자 리터럴은 항상 ' (작은 따옴표)로 감싼야 한다. 공백이 없다면 " " 로 감싸도 하나의 의미있는 토큰으로 인지하는데, 굳이 생략하지 말고 문자 리터럴은 항상 ' ' 로 감싸도록 하자

예시 : <span th:text="'hello world!'"></span>
    > 잘 보면 " " 안에 ' ' 로 감싼 문자열이다. 

리터럴 대체 (Literal subsitutions)

이건 많이 편해보인다. 변수와 같이 문자열을 만들 때 "'hello' + &{data}" 이렇게 만들 수도 있는데, 리터럴 대체 문법을 사용하면 편리하다. | |  사이에 변수가 포함 된 문자열을 작성한다.

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

연산

타임리프의 연산은 자바와 크게 다르지 않지만, HTML 엔티티를 사용하는부분만 조심하면 되겠다.

  <li>비교 연산
    <ul>
      <li>1 > 10 = <span th:text="1 &gt; 10"></span></li>
      <li>1 gt 10 = <span th:text="1 gt 10"></span></li>
      <li>1 >= 10 = <span th:text="1 >= 10"></span></li>
      <li>1 ge 10 = <span th:text="1 ge 10"></span></li>
      <li>1 == 10 = <span th:text="1 == 10"></span></li>
      <li>1 != 10 = <span th:text="1 != 10"></span></li>
    </ul>

첫번째 예시 &gt; 마저도 연산되어 false가 출력됐다. 작은 따옴표로 감싸지 않으면 연산해버리는 것 같다.

삼항 연산도 가능하고, Elvis 연산도 가능하다. Elvis 연산은 true case가 없는 삼항 연산 모양이다. 조건 데이터가 null이 아니면 조건의 데이터를 출력하고 null이면 false case를 출력한다.

이 연산들의 예시는 No-Operation ( _ ) 으로도 표현 가능하다. No-Operation을 사용하면 타임리프가 실행되지 않을 것처럼 동작한다. No-Operation 예시의 false case를 보면 공백이 있는데 지워도 똑같다. 그런데 표준 문서를 보니 컨벤션인 것 같다. 공백도 포함해서 쓰자.

<span th:text="(10 % 2 == 0)?'짝수':'홀수'"></span>

<span th:text="${data}?: '데이터가 없습니다.'"></span>
<span th:text="${nullData}?:'데이터가 없습니다.'"></span>

<span th:text="${data}?: _">데이터가 없습니다.</span>
<span th:text="${nullData}?: _">데이터가 없습니다.</span>

속성 값 설정

예시와 같이 속성 값을 대체 할 수 있다. name 속성에 할당 된 "mock"이 "userA"로 대체 된다.

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

속성 추가도 되는데 attrappend, attrprepend는 띄어쓰기가 자동으로 삽입되지 않으니 주의해야 한다.

// 기존 속성 값 뒤에 추가
- th:attrappend = <input type="text" class="text" th:attrappend="class=' large'" /><br/>
// 기존 속성 값 앞에 추가
- th:attrprepend = <input type="text" class="text" th:attrprepend="class='large '" /><br/>
// 알아서 자연스럽게 추가
- th:classappend = <input type="text" class="text" th:classappend="large" /><br/>

  checked 속성은 "true"를 대입하든, "false"를 대입하든 존재하면 checked 처리 된다. 그래서 타임리프에서는 check는 값이 false인 경우 check 속성 자체를 제거한다. (타임리프 렌더링 거치지 말고, 브라우저에서 파일을 바로 열어 봅시다)

- 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/>

반복

반복 속성 (th:each) 속성이 있는 태그 내부의 내용을 반복한다. List, 배열, Iterable, Enumeration 모두 지원한다. Map도 지원하는데 변수에 담기는 값은 Map.Entry이다. 자바의 for(num : nums)와 비슷하니 설명 대신 예시로 보자.

두번째 파라미터는 반복 상태 값이 들어가고 생략도 가능한데, 생략하면 지정한 변수명(user) + Stat 이 변수명이 된다.

<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>

조건부 평가

if, unless(if의 반대)를 사용할 수 있다. 조건이 맞지 않으면 태그 자체를 렌더링하지 않는다.

switch 에서 * 는 만족하는 조건이 없을 때 들어오는 default이다.

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

<td th:switch="${user.age}">
  <span th:case="10">10살</span>
  <span th:case="20">20살</span>
  <span th:case="*">기타</span>
</td>

주석

타임리프의 주석은 렌더링 타임에 제거된다. 렌더링 타임에 제거 되므로 HTML 주석처럼 -->로 닫으려해도 닫히지 않는다. 예시의 렌더링하면 전부 주석으로 인지되어 삭제된다. (웹브라우저로) 네츄럴 템플릿으로 보면 보이겠다. 타임리프 프로토 타입 주석은 HTML 파일을 순수 HTML에서는 주석처리 되지만, 렌더링 후에만 보이는 기능인데 잘 안쓴다고 한다. 

<!--/* [[${data}]] */-->

<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->

<!--/*/
<span th:text="${data}">html data</span>
/*/-->

블록

"th:block"은 유일한 자체 태그이다. (나머지는 태그의 속성) 렌더링 후 <th:block>는 삭제된다.
예제에서는 <div> 두 개를 반복문으로 같이 돌릴 때 사용했다. 엔간하면 쓰지 않는 것이 좋다.

<th:block th:each="user : ${users}">
  <div>
    <span th:text="${user.username}">username</span>
    <span th:text="${user.age}">age</span>
  </div>
  <div>
    <span th:text="${user.username} + '/' + ${user.age}">username/age</span>
  </div>
</th:block>

자바스크립트 인라인

<script th:inline="javascript">
    ...
</script>

1. 문자 타입의 데이터  처리

문자 타입인 경우 " "를 자동으로 붙여주고, 이스케이프 처리도 해준다.

// userA -> "userA"
var username = [[${user.username}]];
var age = [[${user.age}]];


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

렌더링 전후 다른 값을 대입할 수 있다. 렌더링 전에는 주석으로 동작하고, 렌더링 후에는 주석이 제거되고 인라인 된 식을 출력한다.

var username2 = /*[[${user.username}]]*/ "test username";
// 인라인 사용 전 : var username2 = /*userA*/ "test username";
// 인라인 사용 후 : var username2 = "userA";

3. 객체는 JSON으로 변환해준다.

var user = /*[[${session.user}]]*/ null;
->
var user = {"age":null,"firstName":"John","lastName":"Apricot",
                "name":"John Apricot","nationality":"Antarctica"};

4. each 지원

<script th:inline="javascript">
  [# th:each="user, stat : ${users}"]
  var user[[${stat.count}]] = [[${user}]];
  [/]
</script>

// var user1 = {"username":"UserA","age":10};
// var user2 = {"username":"UserB","age":20};
// var user3 = {"username":"UserC","age":30};

템플릿 조각 (fragment)

웹 페이지의 공통되는 영역 (footers, headers, menus)들이 있다. 타임 리프는 th:fragment를 정의하여 조각들을 가져올 수 있는 기능을 지원한다.

// footer.html
<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
  <body>
  
    <footer th:fragment="copy">
      &copy; 2011 The Good Thymes Virtual Grocery
    </footer>
  
    <footer th:fragment="frag (onevar,twovar)">
      <p th:text="${onevar} + ' - ' + ${twovar}">...</p>
    </footer>
    
  </body>
</html>

하나의 파일에 여러 th:fragment가 있을 수 있다. 메서드처럼 이름으로 호출 할 수 있다.

가이드 문서보면 fragment 표현식 여러 개가 있지만 "~{templatename::fragmentname}" 하나만 써도 되겠다. ~{...} 을 사용하는것이 원칙이지만, 호출하는 코드가 단순하면 생략할 수 있다. 단순의 기준을 모르겠다. 그냥 생략하지 말고 써야겠다.

// 호출
<body>
  ...

  <div th:insert="~{template/fragment/footer :: copy}"></div>
  <div th:replace="~{template/fragment/footer :: copy}"></div>
  
</body>

<div th:replace="~{template/fragment/footer ::frag (${value1},${value2})}">...</div>
<div th:replace="~{template/fragment/footer ::frag (onevar=${value1},twovar=${value2})}">...</div>
  • th:insert : 현재 태그 내부에 추가
  • th:replace : 현재 태그를 대체

템플릿 레이아웃 1

앞서 배운 템플릿 조각을 조금 더 적극적으로 사용하는 방식이다. 레이아웃을 정의하고, 그 레이아웃에 필요한 코드 조각(태그)을 전달해서 완성한다.

여기서는 <head>의 layouy만 정의하여 사용했다.

// base.html
<head th:fragment="common_header(title,links)">

  <title th:replace="${title}">The awesome application</title>

  <!-- Common styles and scripts -->
  <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>

  <!--/* Per-page placeholder for additional links */-->
  <th:block th:replace="${links}" />

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

  <title>Awesome - Main</title>

  <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
  <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">

</head>
...

<title> 과 <link> 태그들이 호출하는 common_header의 파라미터로 넘어간다.

사용해보니 파라미터로 들어가는 태그가 없으면 에러가 났다. 없을 수 있는 태그는 Elvis No-Operation 해줘야겠다.
<th:block th:replace="${links}?: _" />

// base.html
<head th:fragment="common_header(title,links)">

  <title th:replace="${title}">The awesome application</title>

  <!-- Common styles and scripts -->
  <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>

  <!--/* Per-page placeholder for additional links */-->
  <th:block th:replace="${links}?: _" />

</head>

템플릿 레이아웃2

이번에는 <head> 만 적용하는게 아니라 <html> 전체를 적용한다.

// layoutFile.html
<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:replace="${title}">Layout Title</title>
</head>
<body>
    <h1>Layout H1</h1>
    <div th:replace="${content}">
        <p>Layout content</p>
    </div>
    <footer>
        Layout footer
    </footer>
</body>
</html>
<!DOCTYPE html>
<html th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Page Title</title>
</head>
<body>
<section>
    <p>Page content</p>
    <div>Included on page</div>
</section>
</body>
</html>

추가

form

<form action="item.html" th:action method="post">
  ...
</form>

th:action은 form 태그의 action 속성을 대체한다. HTML form에서는 action에 값이 없으면 현재 URL에 데이터를 전송한다.

상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 Mapping을 구분했다.

  • 상품 등록 폼 : GET "/basic/items/add"
  • 상품 등록 처리 : POST "/basic/items/add"