관리 메뉴

나구리의 개발공부기록

타임리프 - 기본기능, 프로젝트 생성 및 타임리프소개, 텍스트(text/utext), 변수(SpringEL), 기본 객체들, 유틸리티 객체와 날짜 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술

타임리프 - 기본기능, 프로젝트 생성 및 타임리프소개, 텍스트(text/utext), 변수(SpringEL), 기본 객체들, 유틸리티 객체와 날짜

소소한나구리 2024. 5. 15. 16:12

  출처 : 인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/GMo43


1. 프로젝트 생성 및 타임리프 소개

1) 프로젝트 생성

(1) Project

  • Project : Gradle - Groovy Project
  • Language : Java
  • Spring Boot : 3.2.3

(2) Project Metadata

  • Group : hello
  • Artifact, Name : thymeleaf-basic
  • Package Name : hello.thymeleaf
  • Packaging : jar
  • java : 21 or 17

(3) Dependencies

  • Spring Web
  • Lombok
  • Thymeleaf

(4) index.html 생성은 생략

2) 타임리프 소개

(1) 타임리프 특징

  • 서버 사이드 HTML 렌더링 (SSR): 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용
  • 네츄럴 템플릿
    - 타임 리프는 순수 HTML을 최대한 유지하는 특징이 있음

    - 타임리프로 작성한 HTML 파일은 웹 브라우저에서 파일을 직접 열어도 내용을 확인할 수 있고 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있음
    - 이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿(natural templates)라고 함
    - JSP를 포함한 다른 뷰 템플릿들은 해당 파일을 열면 정상적인 HTML 결과를 확인할 수 없고(JSP 등) 서버를 통해 렌더링 되고 HTML 응답 결과를 받아야 화면을 확인할 수 있음
  • 스프링 통합 지원: 타임리프는 스프링과 자연스럽게 통합되고 스프링의 다양한 기능을 편리하게 사용할 수 있는데, 이부분은 뒤에서 자세히 다룸

(2) 기본 표현식

  • 간단한 표현
    - 변수 표현식: ${...}

    - 선택 변수 표현식: *{...}
    - 메시지 표현식: #{...}
    - 링크 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: _

(3) 타임리프 사용 선언

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

2. 텍스트 - text, utext

1) text, utext

(1) 데이터 출력

  • 타임리프는 기본적으로 HTML 태그의 속성에 기능을 정의해서 동작함
  • HTML의 콘텐츠에 데이터를 출력할 때는 th:text 사용하면 됨
  • HTML 태그의 속성이 아니라 HTML 콘텐츠 영역 안에서 직접 데이터를 출력하고 싶다면 [[...]] 사용하면 됨
<span th:text="${data}">

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

 

(2) BasicController

  • basic 패키지를 생성 후 작성
package hello.thymeleaf.basic;

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

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

 

(3) text-basic.html

  • /resources/templates/basic 경로에 작성
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

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

</body>
</html>

 

(4) Escape

  • HTML문서는 <,> 같은 특수 문자를 기반으로 정의되므로 뷰 템플릿으로 HTML화면을 생성할 때는 출력하는 데이터에 특수 문자를 주의해서 사용해야 함
  • 만일 위 컨트롤러에 hello Spring 이렇게 볼드 처리를 하고 싶어서 "Hello <b>Spring!</b>" 입력하면 웹브라우저에 Hello <b>Spring!</b>가 그대로 출력되고, 소스보기를 하면 Hello &lt;b&gt;Spring!&lt;/b&gt; 이렇게 나옴
  • < 부분이 &lt; 으로 > 부분이 &gt 이런식으로 변경된 것을 확인할 수 있음

(5) HTML 엔터티

  • 웹 브라우저는 <를 HTML 태그의 시작으로 인식하기 때문에 <를 태그의 시작이 아니라 문자로 표현 할 수 있는 방법을 HTML 엔터티라고 함
  • HTML에서 사용하는 특수 문자를 HTML 엔터티로 변경하는 것을 이스케이프(escape)라고 하며 타임리프가 제공하는 th:text, [[...]]는 기본적으로 이스케이프를 제공함
  • 수많은 HTML 엔터티가 있으므로 궁금한 사항은 인터넷 검색 해볼 것

(6) Unescape

  • 이스케이프를 사용하지 않고자 할 때는 타임리프가 제공하는 th:utext와 [(...)] 를 사용하면 됨

(7) BasicController - textUnescaped() 추가

@GetMapping("text-unescaped")
public String textUnescaped(Model model) {
    model.addAttribute("data", "Hello <b>Spring!</b>"); // 스프링에 볼드 처리
    return "basic/text-unescaped";
}

 

(8) text-unescaped.html

  • th:inline="none": [[...]]을 타임리프가 해석하지말고 그대로 출력하라는 뜻
  • 실행해보면 th:utext와 [(...)]을 사용한 곳은 볼드가 적용되어 의도하고자 했던 Hello Spring! 으로 출력되고 소스보기를 하면 <b>Hello <b>Spring!</b>로 나옴
<!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>
    <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>

 

** 주의!

  • 실제 서비스 개발하다보면 escape를 사용하지 않아서 HTML이 정상 렌더링이 되지 않는 수많은 문제가 발생함
  • 예를들어 게시판의 경우 사용자가 < , > , / 등의 다양한 특수기호들을 써버릴 텐데 escape가 되지 않는다면 HTML이 모두 깨지게 됨
  • escape를 기본으로 하고 꼭 필요한 곳에만 unescape를 사용

3. 변수 - SpringEL

1) SpringEL

(1) 변수 표현식

  • 타임리프 변수 표현식: ${...}
  • 이 변수 표현식에는 스프링 EL이라는 스프링이 제공하는 표현식을 사용할 수 있음

(2) BasicController - variable() 추가

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

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

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

 

(3-1) variable.html

  • basic 경로에 생성
  • SpringEL 표현식으로 인스턴스 변수와 콜렉션에 담긴 값을 프로퍼티 접근으로 꺼낼 수 있음
<!DOCTYPE html>
<!-- 타임리프 선언 및 head태그 -->

<body>
<h1>SpringEL 표현식</h1>
<!-- user의 username을 프로퍼티 접근 -> user.getUsername() -->
<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 <!-- 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 <!-- 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>

</body>
</html>

 

(3-2) SpringEL 다양한 표현식

  • 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를 찾고 메서드를 직접 호출

(3-3) 지역 변수 선언 - variable.html 하단에 추가

  • th:with를 사용하면 지역 변수를 선언해서 사용할 수 있는데, 지역 변수는 선언한 태그 안에서만 사용할 수 있음
<!-- `th:with` 를 사용하면 지역 변수를 선언해서 사용할 수 있음, 지역 변수는 선언한 태그 안에서만 사용 가능. -->
<h1>지역 변수 - (th:with)</h1>
<div th:with="first=${users[0]}"> <!-- users[0] 이 userA가 되고 그 값이 first에 들어감-->
    <p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>

4. 기본 객체들

1) 타임리프가 제공하는 기본 객체들

(1) 기본 객체

  • ${#locale}

**주의! - 아래부터는 스프링 부트 3.0 부터 제공하지 않음

  • ${#request}
  • ${#response}
  • ${#session}
  • ${#servletContext}
  • 만약 스프링 부트 3.0 이상에 사용하면 java.lang.IllegalArgumentException 에러가 발생함
  • 사용하고자 하면 model에 해당 객체를 직접 추가해서 사용해야함

(2) request - 편의 객체 제공

  • request(#request 포함)는 HttpServletRequest 객체가 그대로 제공되기 때문에 데이터를 조회하려면 request.getParameter("data") 처럼 불편하게 접근해야 하는데, 이런 점을 해결하기 위해 편의 객체를 제공함
  • HTTP 요청 파라미터 접근: param
    - ex) ${param.paramData}
  • HTTP 세션 접근: session
    - ex) ${session.sessionData}
  • 스프링 빈 접근: @
    - ex) ${@helloBean.hello('Spring!')}

(2) BasicController - basicObjects 추가

  • 스프링 부트 3.0 이상은 model에 직접 객체를 추가해야함
  • 스프링 부트 3,0 미만은 타임리프가 기본으로 객체를 지원하므로 model에 추가하지 않아도 사용할 수 있었음
  • @Component로 helloBean이라는 객체를 스프링 빈으로 등록
// 스프링 부트 3.0 이상 -> model에 직접 객체를 추가
@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;
    }

// 스프링 부트 3.0 미만
public String basicObjects(HttpSession session) {
	session.setAttribute("sessionData", "Hello Session");
    return "basic/basic-objects";
}

 

(3) basic-objects.html

  • 스프링 부트 3.0 미만에서는 타임리프가 기본으로 제공하는 객체를 #으로 접근할 수 있으나 스프링 부트 3.0에서는 #locale만 제공하므로 나머지는 #이 없이 model에 담은 attributeName으로 접근해야함
  • request편의 객체를 이용하여 요청 파라미터, HTTP 세션, 스프링빈의 데이터에 편리하게 접근할 수 있음
<!DOCTYPE html>
<!-- 타임리프 선언 및 head태그 -->

<body>
<h1>식 기본 객체(Expression Basic Object)</h1>
<!-- 스프링 부트 3.0 이상은 #locale만 기본으로 제공 됨 -->
<ui>
    <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>
</ui>
<!-- 스프링 부트 3.0 미만에서는 #으로 객체가 기본으로 제공 됨 
<ui>
    <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>
</ui>
-->
<h1>편의 객체</h1>
<ui>
    <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>
</ui>
</body>
</html>

5. 유틸리티 객체와 날짜

1) 타임리프 유틸리티 객체들

(1) 다양한 유틸리티 객체들

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

(4-1) 자바8 날짜

  • 다른건 매뉴얼이 있지만 자바8 날짜에 대한 매뉴얼이 없어서 작성
  • 타임리프에서 자바8 날짜인 LocalDate, LocalDateTime, Instant를 사용하려면 추가 라이브러리가 필요함
  • 스프링 부트에서 타임리프를 추가하여 사용하면 해당 라이브러리(thymeleaf-extras-java8time)가 자동으로 추가되고 통합됨

(4-2) BasicController - data() 추가

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

 

(4-3) date.html

<!DOCTYPE html>
<!-- 타임리프 선언 및 head태그 -->

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