스프링 MVC 2편 - 타임리프 - 기본 기능

링딩·2022년 8월 9일
0

스프링 MVC

목록 보기
8/18

이 글은 김영한 강사님의 강의를 참고하여 작성하였습니다.


Chap 1. 타임리프 - 기본 기능

들어가기 앞서 gradle과 lombok 확인 잊지말자

왜 타임리프를 쓸까?

물론 이전에 뷰 템플릿에는 xml도 jsp도 있었다. 현재는 스프링과 가장 적용 가능하고 좋은 타임리프를 쓴다.

  • 서버 사이드 HTML 렌더링 (SSR)
    • 타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용된다.

  • 네츄럴 템플릿
    • 타임리프는 순수 HTML을 최대한 유지하는 특징이 있다.
    타임리프로 작성한 파일은 HTML을 유지하기 때문에 웹 브라우저에서 파일을 직접 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다.
    -> 물론 이 경우 동적으로 결과가 렌더링 되지는 않는다.
    • JSP를 포함한 다른 뷰 템플릿들은 해당 파일을 열면
    -> JSP의 경우, 웹 브라우저에서 열어보면 JSP 소스코드와 HTML이 뒤죽박죽 섞여서 웹 브라우저에서 정상적인 HTML 결과를 확인할 수 없다.
    -> 오직 서버를 통해서 JSP가 렌더링 되고 HTML 응답 결과를 받아야 화면을 확인할 수 있다.
    • 이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿 (natural templates)이라 한다.

  • 스프링 통합 지원
    타임리프는 스프링과 자연스럽게 통합되고, 스프링의 다양한 기능을 편리하게 사용할 수 있게 지원한다. 이 부분은 스프링 통합과 폼 장에서 자세히 알아보겠다.



타임리프 기본 기능

들어가기 전에 타임리프는 미리 html 위에 선언 해주어야 합니다.

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

기본 표현식

참고할 수 있는 곳

  • 간단한 표현:
    - 변수 표현식: ${...}
    - 선택 변수 표현식: *{...}
    - 메시지 표현식: #{...}
    - 링크 URL 표현식: @{...}
    - 조각 표현식: ~{...}
  • 리터럴
    • 텍스트: 'one text', 'Another one!',…
    • 숫자: 0, 34, 3.0, 12.3,…
    • 불린: true, false
    • 널: null
    • 리터럴 토큰: one, sometext, main,…
  • 문자 연산:
    ◦ 문자 합치기: +
    ◦ 리터럴 대체: |The name is ${name}|
  • 산술 연산:
    ◦ Binary operators: +, -, *, /, %
    ◦ Minus sign (unary operator): -
  • 불린 연산:
    ◦ Binary operators: and, or
    ◦ Boolean negation (unary operator): !, not
  • 비교와 동등:
    ◦ 비교: >, <, >=, <= (gt, lt, ge, le)
    ◦ 동등 연산: ==, != (eq, ne)
  • 조건 연산:
    ◦ If-then: (if) ? (then)
    ◦ If-then-else: (if) ? (then) : (else)
    ◦ Default: (value) ?: (defaultvalue)
  • 특별한 토큰:
    ◦ No-Operation: _


텍스트 - text, utext

  • HTML의 콘텐츠(content)에 데이터를 출력할 때 th:text
    ex) <span th:text="${data}">

  • HTML 테그의 속성이 아니라 HTML 콘텐츠 영역 안에서 직접 데이터를 출력하고 싶으면 [[...]] 를 사용하면 된다.
    => 컨텐츠 안에서 직접 출력하기 = [[${data}]]

[예제]

<li>th:text 사용 <span th:text="${data}"></span></li>
 <li>컨텐츠 안에서 직접 출력하기 = [[${data}]]</li>

* 컨텐츠 안에서 직접 출력하기 = [[${data}]] 는 이렇게 직접 출력된다.



Escape vs UnEscape

들어가기 전에 HTMl 문서는 < > 같은 특수문자를 기반으로 정의.
=> 따라서 뷰 템플릿으로 HTML 화면을 생성할 때는 출력하는 데이터에 이러한 특수 문자가 있는 것을 주의해서 사용해야 한다.

  • HTML 엔티티
    - 웹 브라우저는 < 를 HTML 테그의 시작으로 인식한다. 따라서 < 를 테그의 시작이 아니라 문자로 표현할 수 있는 방법이 필요
    => 이것을 HTML 엔티티라 한다.
    => 그리고 이렇게 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것 을 "이스케이프(escape)"라 한다.
    - 그리고 타임리프가 제공하는 th:text ,
    [[...]] 는 기본적으로 이스케이스(escape)를 제공

<상황제시>

우리는 <b> 태그를 이용해 Spring! 을 만들고 싶다.

[문제] : Escape 가 발생해서 <b> 태그를 HTML 엔티티로 변경시킴..

링딩: 아이고 <&lt로 바꾸네 😢💦💦

.
.
.
방법은 있다.

A: " 이스케이프가 아니라 UnEscape를 사용하자"

th:text th:utext
[[...]] [(...)] 으로 변경해주면 된다.


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

th:inline="none" : 여기서는 타임리프르 쓰지 않겠다는 말이다.


주의!

  • 실제 서비스를 개발하다 보면 escape를 사용하지 않아서 HTML이 정상 렌더링 되지 않는 수 많은 문제가 발생한다. escape를 기본으로 하고, 꼭 필요한 때만 unescape를 사용하자.


변수 -- SpringEL

타임리프에서 변수를 사용할 때는 변수 표현식을 사용한다.
변수 표현식 : ${...}
그리고 이 '변수 표현식'에는 '스프링 EL'이라는 스프링이 제공하는 표현식을 사용할 수 있다.


SpringEL 다양한 표현식 사용

  • 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

<h1>지역 변수 - (th:with)</h1>
<div th:with="first=${users[0]}">
    <p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
  • th:with : 지역변수 선언
  • 지역변수는 선언한 범위 내에서만 사용 가능



기본객체


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

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

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

  • HTTP 요청 파라미터 접근: param
    예) ${param.paramData}
  • HTTP 세션 접근: session
    예) ${session.sessionData}
  • 스프링 빈 접근: @
    예) ${@helloBean.hello('Spring!')}



유틸리티 객체와 날짜


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

보통 유틸리티 객체들은 대략 이런 것들이 있다는 것만 파악하고, 필요 시 찾아서 사용한다. 그러니 더 자세히 참조하려면 이 곳에서 참조


타임리프 유틸리티 객체들

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

타임리프 자바8에서 날짜 사용

  • 라이브러리
    thymeleaf-extras-java8time

  • 자바8 날짜용 유틸리티 객체
    #temporals

  • 사용예시

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



URL 링크


  • URL을 생성할 때는 @{...}

  • 단순한 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 : 상대 경로



리터럴


정의 : 리터럴은 소스 코드 상에 고정된 값을 뜻함

ex) 여기선 "Hello", 10,20 이렇게 리터럴임.

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

1. 문자 리터럴은 항상 ' (작은 따옴표)로 감싸야 한다

<전> : <span th:text="'hello'">

=> 그러나 공백 없이 쭉 이어진다면 하나의 의미있는 토큰으로 인지해서 다음과 같이 작은 따옴표를 생략 (ㅇ)
<후>: <span th:text="hello">


2. 리터럴 대체(Literal substitutions)

<span th:text="|hello ${data}|">
=> 리터럴 대체 문법을 사용하면 마치 템플릿을 사용하는 것 처럼 편리하다.


연산


  • 비교연산: HTML 엔티티를 사용해야 하는 부분을 주의하자,

    > (gt), < (lt), >= (ge), <= (le), ! (not), == (eq), != (neq, ne)

<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>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)? 
'짝수':'홀수'"></span></li>
  • Elvis 연산자: 조건식의 편의 버전
<li>${data}?: '데이터가 없습니다.' = <span th:text="${data}?: '데이터가 없습니다.'"></span></li>

이 경우 ${data}? 이 부분에 데이터가 들어오면, 본인을 출력하고
아닐 경우 : '데이터가 없습니다.'이 뒤의 '데이터가 없습니다'가 출력된다.

  • No-Operation: _인 경우 마치 타임리프가 실행되지 않는 것 처럼 동작한다. 이것을 잘 사용하면 HTML의 내용 그대로 활용할 수 있다.
<li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</
span></li>
 <li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가
없습니다.</span></li>


속성 값 설정


타임리프는 th:* 속성을 지정하면(*=어떤 속성), 타임리프는 렌더링 후, 기존 속성에서 th:* 로 지정한 속성으로 대체한다.
만일 기존 속성이 없다면 새로 만든다.

<input type="text" name="mock" th:name="userA" />
(타임리프 렌더링 후...)
<input type="text" name="userA" />

📢 속성 추가
th:attrappend : 속성 값의 에 값을 추가한다.
th:attrprepend : 속성 값의 앞에 값을 추가한다.
th:classappend : class 속성에 자연스럽게 추가한다.


checked 처리

HTML에서 checked 속성은 checked 속성의 값과 상관없이 checked 라는 속성만 있어도 체크가 된다.
이런 부분이 true , false 값을 주로 사용하는 개발자 입장에서는 불편하다.
타임리프의 th:checked 값이 false 인 경우 checked 속성 자체를 제거한다.
전 : <input type="checkbox" name="active" th:checked="false" />
타임리프 렌더링 후: <input type="checkbox" name="active" />



반복 th:each


<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>
  • 오른쪽 컬렉션( ${users} )의 값을 하나씩 꺼내서 왼쪽 변수( user )에 담아서 태그를 반복
  • 반복의 두번째 파라미터를 설정해서 반복의 상태를 확인 할 수 있습니다


조건부 평가 _ if , unless ( if 의 반대)


``` 1 username 0 ```
  • if, unless
    타임리프는 해당 조건이 맞지 않으면 태그 자체를 렌더링하지 않는다.
    만약 다음 조건이 false 인 경우 ... 부분 자체가 렌더링 되지 않고 사라진다.
    <span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
  • switch
<td th:switch="${user.age}">
 <span th:case="10">10살</span>
 <span th:case="20">20살</span>
 <span th:case="*">기타</span>
 </td>

*은 만족하는 조건이 없을 때 사용되는 '디폴트 값'



주석


1. 표준 HTML 주석

타임리프가 렌더링 하지 않고, 그대로 남겨둔다


2. 타임리프 파서 주석, 3. 프로토타입 주석

  • 2. 타임리프의 진짜 주석이다. 렌더링에서 주석 부분을 제거한다.
  • 3. 타임리프 프로토타입 주석
    타임리프 프로토타입은 약간 특이한데, HTML 주석에 약간의 구문을 더했다.
    => 쉽게 이야기해서 HTML 파일을 그대로 열어보면 주석처리가 되지만, 타임리프를 렌더링 한 경우에만 보이는 기능이다.


블록 <th:block>

HTML 태그 안에 속성으로 기능을 정의해서 사용하는데, 위 예처럼 이렇게 사용하기 애매한 경우에 사용하면 된다. <th:block> 은 렌더링시 제거된다.


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


자바스크립트 인라인

자바스크립트에서 타임리프를 편리하게 사용할 수 있는 자바스크립트 인라인 기능이다.
<script th:inline="javascript">

<script th:inline="javascript">
 var username = [[${user.username}]];
 var age = [[${user.age}]];
 //자바스크립트 내추럴 템플릿
 var username2 = /*[[${user.username}]]*/ "test username";
 //객체
 var user = [[${user}]];
</script>

텍스트 렌더링

var username = [[${user.username}]];
사용 (후) : var username = "userA"; 으로 문자 타입으로 출력 해준다.

+) 자바스크립트에서 문제가 될 수 있는 문자가 포함되어 있으면 이스케이프 처리

=> 예) " \"

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

타임리프는 HTML 파일을 직접 열어도 동작하는 내추럴 템플릿 기능을 제공

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

    그 전에는 '내추럴 템플릿' 기능 작동X, -> 순수하게 그대로 해석

객체

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

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



자바스크립트 인라인 each

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

[/] 자바 세미콜론처럼 타임리프 끝난다는 뜻.


템플릿 조각

웹 페이지를 개발할 때는 공통 영역이 많이 있다. 예를 들어서 상단 영역이나 하단 영역, 좌측 카테고리 등등 여러 페이지에서 함께 사용하는 영역들이 있다.
이런 부분을 코드를 복사해서 사용한다면 변경시 여러 페이지를 다 수정해야 하므로 상당히 비효율 적이다. 타임리프는 이런 문제를 해결하기 위해 ₁템플릿 조각과 ₂레이아웃 기능을 지원한다



/resources/templates/template/fragment/footer.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<footer th:fragment="copy">
 푸터 자리 입니다.
</footer>
<footer th:fragment="copyParam (param1, param2)">
 <p>파라미터 자리 입니다.</p>
 <p th:text="${param1}"></p>
 <p th:text="${param2}"></p>
</footer>
</body>
</html>
  • th:fragment 가 있는 태그는 다른곳에 포함되는 코드 조각으로 이해

/resources/templates/template/fragment/fragmentMain.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <meta charset="UTF-8">
 <title>Title</title>
</head>
<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>
<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터
2')}"></div>
</body>
</html>
  • template/fragment/footer :: copy :
    -> template/fragment/footer.html 템플릿에 있는
    th:fragment="copy" 라는 부분을 ,템플릿 조각으로 가져와
    사용한다는 의미

부분 포함 insert

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

=> 현재 태그( div ) 내부에 추가


부분 포함 replace

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

=> 현재 태그( div )를 대체한다.

+) ~{...} 생략

  • 사용하는 것이 원칙이지만 템플릿 조각을 사용하는 코드가 단순하면 이 부분을 생략할 수 있다

파라미터 사용

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

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



템플릿 레이아웃1

코드 조각을 레이아웃에 넘겨서 사용하는 방법에 대해서 알아보자.
공통으로 사용하는 css , javascript 같은 정보들이 있는데, 이러한 공통정보들을 한 곳에 모아두고, 공통으로 사용하지만, 각 페이지마다 필요한 정보를 더 추가해서 사용하고싶다면 다음과 같이 사용하면 된다.


/resources/templates/template/layout/base.html

<html xmlns:th="http://www.thymeleaf.org">
<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>

/resources/templates/template/layout/layoutMain.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<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>
</html>
  • common_header(~{::title},~{::link}) 이 부분이 핵심이다.
    - ::title 은 현재 페이지의 title 태그들을 전달한다.
    - ::link 는 현재 페이지의 link 태그들을 전달한다.


템플릿 레이아웃2

템플릿 레이아웃 확장

앞서 이야기한 개념을 <head> 정도에만 적용하는게 아니라 <html> 전체에 적용할 수도 있다.

/resources/templates/template/layoutExtend/layoutFile.html

<!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>th:fragment 속성이 정의되어 있다.
    -> 이 레이아웃 파일을 기본으로 하고, 여기에 필요한 내용을 전달해서 부분부분 변경하는 것 으로 이해하면 된다.

/resources/templates/template/layoutExtend/layoutExtendMain.html

<!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 는 현재 페이지인데,
    <html> 자체를 th:replace 를 사용해서 변경하는 것을 확인할 수 있다.
    => 결국 layoutFile.html필요한 내용을 전달하면서 <html> 자체를 layoutFile.html 로 변경

결과

<!DOCTYPE html>
<html>
<head>
<title>메인 페이지 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
<footer>
레이아웃 푸터
</footer>
</body>
</html>
profile
초짜 백엔드 개린이

0개의 댓글