티스토리 뷰

Thymeleaf 란?

Thymeleaf(타임리프)는 웹 및 독립 실행형 환경 모두를 위한 Java 템플릿 엔진입니다.

Thymeleaf로 작성된 HTML 템플릿은 애플리케이션을 통한 실행이 아니더라도 브라우저에서 열였을 때, HTML 내용을 그대로 확인이 가능하여 내추럴 템플릿(Natual Templates)이라고 불립니다.

스프링을 사용할 때, 백엔드 서버에서 뷰 렌더링이 필요할 경우에 JSP를 사용하는 경우가 있는데, JSP보다 편리한 기능을 제공하며 스프링에서도 적극적으로 권장하고 있는 기술이 바로 Thymeleaf 입니다.

 


Thymeleaf 특징

 

서버 사이드 HTML 렌더링(SSR)

타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용됩니다.

난이도가 높지 않아 금방 배우기가 가능하며, 페이지가 어느 정도 동적이고 빠른 생산성이 필요한 화면을 만드는 경우에 좋은 선택지가 될 수 있겠습니다.

 

 

내추럴 템플릿

타임리프는 순수 HMTL을 최대한 유지하는 특징이 있습니다.

타임리프로 작성한 파일은 HTML을 유지하기 때문에 웹 브라우저에서 파일을 직접 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있습니다.

 

JSP를 포함한 다른 뷰 템플릿들은 해당 파일들을 열면, 예를 들어 JSP 파일 자체를 그대로 웹 브라우저에서 열어보면 JSP 소스코드와 HTML 이 뒤죽박죽 섞여서 웹 브라우저에서 정상적인 HTML 결과를 확인할 수 없습니다.

(JSP 파일은 자바 코드와 HTML이 섞여서 작성되어 있기 때문이다.)

그렇기 때문에 오직 서버를 통해 JSP가 렌더링 되고 HTML 응답 결과를 받아야 화면을 확인할 수 있습니다.

 

반면에 타임리프로 작성된 파일은 해당 파일을 그대로 웹 브라우저에서 열어도 정상적인 HTML 결과를 확인할 수 있습니다. 

물론 이 경우 동적으로 결과가 렌더링이 되지는 않습니다.

하지만 HTML 마크업 결과가 어떻게 되는지 파일만 열어도 바로 확인이 가능합니다.

 

이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿(Natural Templates)이라고 합니다. 

 

 

스프링 통합 지원

타임리프는 스프링과 자연스럽게 통합되고, 스프링의 다양한 기능을 편리하게 사용할 수 있게 지원합니다.

오해하시는 분들이 있을 수 있는데, 타임리프는 스프링이랑만 사용이 가능한 것이 아닌 단독으로 사용이 가능한 기술입니다.

 


 

타임리프 기본 기능

 

우선, 타임리프를 사용하려면 아래와 같이 사용 선언을 해야 한다.

  • <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 태그의 속성에 기능을 정의해서 동작합니다.

HTML의 콘텐츠(content)에 데이터를 출력할 때는 다음과 같이 th:text를 사용하면 됩니다.

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

 

 

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

<span>hello [[${data}]]</span>

 

 

주의해야 할 사항

th:text [[...]] 기본적으로 이스케이프(escape)를 제공합니다.

즉, 서버에서 데이터로 HTML 태그 내용을 담아서 보낸 경우 '&gt', '&lt'와 같이 치환되어서 내용이 출력됩니다.

이러한 경우 타임리프는 Unescape 하게 사용하여 HTML 태그 자체로 출력하기 위한 th:utext와 [(...)]을 제공합니다.

 

HTML 엔티티 vs 이스케이프(escape)

웹 브라우저는 < 를 HTML 태그의 시작으로 인식합니다.

따라서 < 를 태그의 시작이 아니라 문자로 표현할 수 있는 방법이 필요한데, 이것을 HTML 엔티티라고 합니다.

그리고 이렇게 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것을 이스케이프(escape)라 합니다.

타임리프가 제공하는 th:text, [[...]] 는 기본적으로 이스케이프(escape)를 제공합니다.

 

 

쉽게 이해하기 위해 아래 예제 코드를 확인해 봅시다.

 

[컨트롤러]

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

    @GetMapping("/text-basic")
    public String textBasic(Model model) {
        model.addAttribute("data", "Hello <b>Spring!<b>"); //뷰 템플릿으로 Model 넘김
        return "basic/text-basic";
    }
}

 

 

[뷰 템플릿]

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <!--타임리프 사용 선언-->
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>text vs utext</h1>
<ul>   <!-- 컨트롤러에서 Model로 넘어온 data라는 키(${data})의 값 출력-->
    <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>
</html>

 

[결과]

 

위 결과를 보면 th:text와 [[...]]는 문자로 표현할 수 있는 방식인 이스케이프를 제공하기 때문에, < >를 특수문자로 본 것이 아닌 단순히 문자로 보고 출력해 주었습니다.

 

반면에, utext와 [(...)]는 이스케이프 기능을 사용하지 않아, <br> 자체를 문자가 아닌 특수문자로 해석하여 (HTML에서 굵게 표시를 의미) 해당 태그에 적용된 Spring 이 굵게 표시된 것을 확인할 수 있다.

 

참고로 < 기호는 &lt; > 기호는 &gt;를 의미합니다. 

이외에도 수많은 HTML 엔티티가 있으니, 더 자세한 내용이 궁금하면 찾아보도록 합시다.

 

참고!

위 뷰 템플릿 코드의 th:inline="none"은 타임리프는 [[...]]를 자동으로 해석하기 때문에, 화면에 [[...]]를 보여줄 수 없다.

따라서 화면에 렌더링 하기 위해서 작성한 코드인데, 이 태그 안에서는 타임리프가 해석하지 말라는 옵션입니다.

 


변수 - SpinrgEL

타임리프에서 Model에 담은 값을 꺼내거나, 타임리프 내부에 선언된 변수를 사용할 때는 변수 표현식을 사용합니다.

이러한 변수 표현식에는 스프링 EL이라는 스프링이 제공하는 변수 표현식을 사용할 수 있습니다.

 

[컨트롤러]

@GetMapping("/variable")
public String variable(Model model) {
    User userA = new User("userA", 10);
    User userB = new User("userB", 20);

    List<User> list = new ArrayList<>();
    list.add(userA);
    list.add(userB);

    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";
}

 

 

[뷰 템플릿] (이후부터 <body> 태그 내부 내용만 작성합니다.)

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

 

 

[결과]

 

표현식의 방법은 다를 뿐 결과는 동일합니다.

 

SpringEL 다양한 표현식은 다음과 같습니다.

단순 변수
    - data
    
Object
    - user.username : user의 username을 프로퍼티 접근 -> user.getUsername()
    - user['username'] : 위와 같음
    - 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를 사용하면 지역 변수를 선언해서 사용할 수 있습니다.

지역 변수는 선언한 태그 안에서만 사용할 수 있습니다.

 

아래코드는 first라는 변수에 users [0]의 데이터를 담아 사용하는 예제입니다.

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

 


 

기본 객체들

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

타임리프에서 제공하는 기본 객체들은 스프링 부트 3.0부터는 제공하지 않는다.

 

  • ${#request}, ${#response}, ${#session}, ${#servletContext}, ${#locale} 등의 기본 객체가 존재한다.
  • ${#locale} 은 사용가능

 

스프링 부트 3.0부터는 위의 기본 객체들은 사용이 불가능하기 때문에 #request 같은 경우 HttpServletRequest 객체 그대로 제공되기 때문에 조회하려면 request.getParameter("data")처럼 불편하게 요청 데이터에 접근해야 했습니다.

 

 

하지만, 다행이도 이런 점을 위해 타임리프는 편의 객체를 제공합니다.

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

 

[컨트롤러]

@GetMapping("/basic-objects")
public String basicObjects(Model model, HttpServletRequest request, HttpServletResponse response, HttpSession session) {
    session.setAttribute("sessionData", "Hello Session");
    model.addAttribute("request", request);
    model.addAttribute("response", response);
    model.addAttribute("servletContext", request.getServletContext());
    return "basic/basic-objects";
}
    
@Component("helloBean")
static class HelloBean {
    public String hello(String data) {
        return "Hello " + data;
    }
}

 

 

[뷰 템플릿]

<h1>식 기본 객체 (Expression Basic Objects)</h1> 
<ul>
    <li>request = <span th:text="${request}"></span></li>
    <li>response = <span th:text="${response}"></span></li>
    <li>session = <span th:text="${session}"></span></li>
    <li>servletContext = <span th:text="${servletContext}"></span></li>
    <li>locale = <span th:text="${#locale}"></span></li>
</ul>

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

 

 

[결과]

 

타임리프는 이러한 편의 객체를 통해 요청 파라미터나 세션의 속성 값 혹은 스프링 빈에 쉽게 접근할 수 있습니다.

 


 

유틸리티 객체와 날짜

타임리프는 문자, 숫자, 날짜, 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 날짜

타임리프에서 자바8 날짜인 LocalDate, LocalDateTime, Instant를 사용하려면 추가 라이브러리가 필요합니다.

스프링 부트 타임리프를 사용한다면 해당 라이브러리가 자동으로 추가됩니다.

 

타임리프 자바8 날짜 지원 라이브러리

  • thymeleaf-extras-java8time

 

자바 8 날짜 예제코드를 보며 이해해 보자.

 

 

[컨트롤러]

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

 

 

[뷰 템플릿]

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

<h1>LocalDateTime - Utils</h1>
<ul>
    <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>
</ul>

 


 

URL 링크

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

 

예제를 보고 난 후, 설명하도록 하겠습니다.

 

 

[컨트롤러]

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

 

 

[뷰 템플릿]

<h1>URL 링크</h1> <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>

 

딱 코드를 처음 보면 이게 뭐지.. 무엇을 의미하는지 이해하기 힘듭니다.

타임리프의 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 : 상대 경로

 

처음 접하시는 분들은 도통 이해가 잘 되지 않으실 수 있습니다.

하지만 복습해서, 한 번 딱 이해하면 "단순했구나" 하며 쉬워지실 겁니다.

 


 

리터럴

리터럴이란 소스 코드상에 고정된 값을 말하는 용어입니다.

 

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

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

예를 들어 다음 코드에서 "Hello"는 문자 리터럴이며 10, 20은 숫자 리터럴입니다.

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

 

 

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

<span th:text=" 'hello' ">  <!--잘 보이기 위해 " ' 띄워쓰기-->

 

 

하지만 문자를 항상 '(작은따옴표)로 감싸는 것은 너무 귀찮은 일일 것입니다.

공백 없이 쭉 이어진다면 하나의 의미 있는 토큰으로 인지해서 다음과 같이 작은따옴표를 생략할 수 있습니다.

  • 규칙: A-Z, a-z, 0-9, [], ., -, _
<span th:text="hello">

<span th:text="test_code">

<span th:text="123456">

<span th:text="hello world"> //오류 발생 -> 문자 내에 공백 존재하기 때문

 

위 예제에서 중간에 공백이 있다면 하나의 의미 있는 토큰으로 인식되지 않아 오류가 발생합니다.

 

이럴 땐 원칙상 " 'hello world' " 이렇게 '(작은따옴표)로 감싸면 정상 동작합니다.

 

 

리터럴 대체 문법(Literal substitutions)

위 예제처럼 공백이 있다면 '|'를 감싸서 사용하면 리터럴 대체가 가능하여 마치 템플릿을 사용하는 것처럼 편리하게 사용할 수 있습니다.

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

 

 

[예제 코드 - 컨트롤러]

@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 안에서 사용하기 때문에 HTML 엔티티를 사용하는 부분만 주의하면 될 것 같습니다.

 

  • 비교 연산 : HTML 엔티티를 사용해야 하는 부분을 주의하자
    • >(gt), <(lt), >=(ge), <=(le), !(not), ==(eq), !=(neq, ne)
  • 조건식 : 자바의 조건식과 유사하다.
  • Elvis 연산자 : 조건식의 편의 버전
  • No-Operation : _ 인 경우 마치 타임리프가 실행되지 않는 것처럼 동작한다. 이것을 잘 사용하면 HTML의 내용 그대로 활용할 수 있습니다. 아래 예제코드의 마지막 예를 보면 "데이터가 없습니다." 부분이 그대로 출력합니다.

 

[예제코드 - 컨트롤러]

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

 

 

[뷰 템플릿]

<li>산술 연산 
    <ul>
        <li>10 + 2 = <span th:text="10 + 2"></span></li>
        <li>10 % 2 == 0 = <span th:text="10 % 2 == 0"></span></li>
    </ul>
</li>

<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> 
</li>
    
<li>조건식 
    <ul>
        <li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)? '짝수':'홀수'"></span></li>
    </ul> </li>

    <li>Elvis 연산자 <ul>
        <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 태그에 th:* 속성을 지정하는 방식으로 동작합니다.

th:* 로 속성을 적용하면 기존 속성을 대체합니다.

기존 속성이 없다면 새로 만듭니다.

 

우리가 위에서 계속 보던 th:text가 이에 해당한다고 보시면 됩니다.

-> 기존 코드 <span text="TestA", th:text="TestB"/>

-> 타임리프 렌더링 후 <span text="TestB"/>

 

 

[예제코드 - 컨트롤러]

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

 

 

[뷰 템플릿]

<h1>속성 설정</h1>
<input type="text" name="mock" th:name="userA" />
<h1>속성 추가</h1>
- 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/>
<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/>

 

 

[결과]

(왼) 렌더링 된 화면 / (오) 해당 페이지 소스 보기

 

 

속성 추가

th:attrappend : 속성 값의 뒤에 값을 추가합니다.

th:attrprepend : 속성 값의 앞에 값을 추가합니다.

th:classappend : class 속성에 자연스럽게 추가합니다.

 

 

checked 처리에 대하여

HTML에서는 

  • <input type="checkbox" name="active" checked="false"/>

위 경우에도 checked 속성이 false이지만, checked 속성이 존재한다는 이유만으로도 checked 처리가 됩니다.

 

이런 부분이 true, false 값을 주로 사용하는 개발자 입장에서는 불편할 수도 있다.

 

타임리프의 th:checked는 값이 false 인 경우 checked 속성 자체를 제거합니다. (위 사진 참고)

 


 

반복

타임리프에서 반복은 th:each를 사용합니다.

추가로 반복에서 사용할 수 있는 여러 상태 값을 지원합니다.

 

 

기본 사용법

반복 시 오른쪽 컬렉션의 값을 하나씩 꺼내서 왼쪽 변수에 담아서 태그를 반복 실행합니다.

th:each는 List 뿐만 아니라 배열, java.util.Iterable, java.util.Enumeration을 구현한 모든 객체를 반복에 사용할 수 있다. 

또한, Map도 사용할 수 있는데 이 경우 Map.Entry가 사용됩니다.

<!--예시-->
<tr th:each="user : ${users}">
    <td th:text="${user.username}">default value</td>
    <td th:text="${user.age}">default value</td>
</tr>

 

 

반복 상태 유지 기능

th:each를 사용하여 반복을 할 때, 두 번째 파라미터를 설정해서 반복의 상태를 확인할 수 있습니다.

두 번째 파라미터는 생략이 가능한데, 생략하면 지정한 변수명(user) + Stat 가 됩니다.

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

정확히 어떠한 값을 의미하는지 이해가 되지 않으니 아래 예제 코드를 참고하자.

(아래 예제에서는 userStat를 추가했으나, 생략해도 무관합니다.)

 

 

[컨트롤러]

@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);
}

 

 

[뷰 템플릿]

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

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

 

 

[결과]


 

조건부 평가

타임리프에서는 if, unless(if의 반대), 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>

<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 문에서 th:case="*"의 * 는 만족하는 조건이 없을 때 사용하는 디폴트이다.

자바 문법 switch에서 default 절?이라고 생각하면 됩니다.

 

이렇게 타임리프의 기본기능에 대해 알아보았고 다음 글에서는 주석과 블록, 자바스크립트 인라인, 템플릿 조각과 템플릿 레이아웃에 대해 공부하고 정리해 보는 시간을 가져보도록 하겠습니다.

 


 

참고(출처)

https://m.blog.naver.com/PostView.naver?blogId=hj_kim97&logNo=223031615864&fromRecommendationType=category&targetRecommendationDetailCode=1000

인프런 김영한님의 스프링 MVC 2편 강의