본문 바로가기

Spring/MVC

[Spring MVC] Thymeleaf - Text & Expression

이번 장에서는 타임리프텍스트표준 표현식 구문에 대해서 알아본다.
글의 하단부에 참고한 강의와 공식문서의 경로를 첨부하였으므로 자세한 내용은 강의나 공식문서에서 확인한다.
모든 코드는 깃 허브(링크) 에 올려두었다.


개요

타임리프는 서버에서 HTML 파일을 동적으로 렌더링해주는 SSR(Server Side Rendering) 용도로 사용된다.
네츄럴 템플릿으로 순수 HTML을 최대한 유지하는 특징이 있다. 순수하다는 의미는 타임리프로 작성한 파일이 HTML을 유지하기 때문에 웹 브라우저에서 파일을 직접 열어도 내용을 확인할 수 있다는 의미이다.
또한 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다.
JSP와 유사한 템플릿은 생성된 웹 브라우저를 통해 소스코드를 확인해보면 HTML이 뒤죽박죽 섞여서 웹 즈라우저에서 정상적인 HTML 결과를 확인할 수 없다.
서버를 통해서 렌더링 되어야지만 화면에 표시되는 내용을 알 수 있다.
반면 타임리프로 작성된 파일은 해당 파일을 그대로 웹 브라우저에서 열면 렌더링 되지는 않았지만 정상적인 HTML 결과를 확인할 수 있다.
이렇게 순수한 HTML 파일을 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿이라고 한다.

타임리프는 스프링을 위해서 나왔다고 해도 될 정도로 스프링과 자연스럽게 통합되고 다양한 편의 기능을 지원한다.


기본 표현식

타임리프는 다음과 같은 기본 표현식들을 제공한다.

- 간단한 표현:
  - 변수 표현식: ${...}
  - 선택 변수 표현식: *{...}
  - 메시지 표현식: #{...}
  - 링크 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}"></span>

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

yourVariable = [[${data}]]

예제

Controller

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

text-basic.html

<!--생략...-->
<h1>컨텐츠에 데이터 출력</h1>
<ul>
    <li>th:text 사용 <span th:text="${data}"></span></li>
    <li>컨텐츠 안에서 직접 츌력하기 = [[${data}]]</li>
</ul>
<!--생략...-->

Result


Escape

HTML 문서는 <, >와 같은 특수 문자를 기반으로 정의된다.
뷰 템플릿으로 HTML 화면을 생성할 때는 출력하는 데이터에 이런 문자가 들어가지 않도록 주의해서 사용해야한다.

HTML 엔티티: 웹 브라우저는 <를 HTML 태그의 시작으로 인식한다. 따라서 <를 태그의 시작이 아니라 문자로 표현할 수 있는 방법을 HTML 엔티티라 한다.
이렇게 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것을 이스케이프(escape)라 한다.
또한 타임리프에서 제공하는 th:text는 기본적으로 이스케이프(escape)를 제공한다. HTML에 대한 자세한 사항 은 링크를 확인하도록 한다.
만약 이스케이프(escape)기능을 비활성화 하고 싶은 경우에는 th:text가 아니라 th:utext를 사용하면 된다.

escape를 사용하지 않으면 HTML이 렌더링 되지 않는 수 많은 문제가 발생하므로 기본적으로 사용하고, 꼭 필요한 경우에만 unescape를 사용하도록 한다.

예제

Controller

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

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>

Result


변수 - SpringEL

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

예제

Controller

@GetMapping("/variable")
public String variable(Model model) {
    List<User> userList = List.of(
            new User("userA", 10),
            new User("userB", 20)
    );
    Map<String, User> userMap = new HashMap<>(
            Map.of("userA", userList.get(0), "userB", userList.get(1))
    );
    model.addAttribute("user", userList.get(0));
    model.addAttribute("users", userList);
    model.addAttribute("userMap", userMap);
    return "basic/variable";
}
@Data
@AllArgsConstructor
static class User {
    private String username;
    private int age;
}

variable.html

<h1>SpringEL 표현식</h1>
<ul>Object
    <!--user.username: user의 username을 프로퍼티 접근 -> user.getUsername()-->
    <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']}"></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>

Result


지역 변수 사용법

th:with를 사용하여 지역 변수를 선언해서 사용할 수 있으며 해당 태그 안에서만 사용할 수 있다.

variable.html

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

Result


기본 객체들

타임리프는 아래와 같은 기본 객체들을 제공한다.

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

${#request}의 경우 HttpServletRequest 객체를 그대로 제공하기 때문에 데이터를 조회하기 불편하다.
이러한 문제를 해결하기 위해 편의 객체도 제공한다.

  • HTTP 요청 파라미터 접근: ${param}
  • HTTP 세션 접근: ${session}
  • 스프링 빈 접근: @{@royBean.roy('Roy')}

예제

Controller

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

@Component("royBean")
static class RoyBean {
    public String roy(String data) {
        return String.format("Hello %s", data);
    }
}

basic-objects.html

<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="${@royBean.roy('Roy!')}"></span></li>
</ul>

Result


유틸리티 객체와 날짜

타임리프는 아래와 같이 문자, 숫자, 날짜, URI등을 편리하게 다루는 유틸리티 객체를 제공한다.

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

참고 자료

예제

Controller

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

date.html

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

Result


URL

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

예제

Controller

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

link.html

<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>
    <!-- 경로 변수: URL 경로상에 변수가 있으면 () 부분은 경로 변수로 처리된다. -->
    <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>

Result


리터럴 (Literals)

리터럴은 소스 코드상에서 고정된 값을 의마한다.
아래의 코드에서 리터럴은 총 5개가 있다.

String charLiteral = "Hello";
int numLiteral = 10 * 20;
boolean boolLiteral = true;
String nullLiteral = null;
  • 문자: "Hello"
  • 숫자: 10, 20
  • 불린: true
  • null: null

주의 사항
타임리프에서 문자 리터럴은 항상 공백이 있는 문자 리터럴은 '로 감싸야한다.
만약 감싸지 않은 문자 리터럴에 공백이 있는 경우 오류가 발생한다.

예제

Controller

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

literal.html

<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>
        <!-- 리터럴 대체(Literal substitutions)를 사용하면 마치 템플릿을 사용하는 것 처럼 편리하다. -->
        <li>리터럴 대체 |hello ${data}| = <span th:text="|hello ${data}|"></span></li>
</ul>

Result


연산

타임리프의 연산은 자바와 크게 다르지 않으며 HTML 엔티티를 사용하는 부분만 주의하면 된다.

  • 비교연산: >(gt), <(lt), >=(ge), <=(le), !(not), ==(eq), !=(neq, ne)
  • 조건식: 자바의 조건식과 유사
  • Elvis 연산자(?:): 조건식의 편의 버전
  • No-Operation: _인 경우 마치 타임리프가 실행되지 않는 것 처럼 동작한다.

예제

Controller

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

operation.html

<ul>
    <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 == 1 = <span th:text="1 == 10"></span></li>
            <li>1 != 1 = <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>
</ul>

Result


지금까지 타임리프의 텍스트표준 표현식 구문에 대해서 알아보았다.
다음 장에서는 이외의 기능들에 대해서 알아본다.


참고한 강의:

참고한 문서:

'Spring > MVC' 카테고리의 다른 글

[Spring MVC] Thymeleaf - Fragment & Layout  (0) 2022.05.04
[Spring MVC] Thymeleaf - Other Function  (0) 2022.05.04
[Spring MVC] PRG  (0) 2022.04.15
[Spring MVC] Handler Adapter  (0) 2022.04.15
[Spring MVC] HTTP Message Converter  (0) 2022.04.14