티스토리 뷰

이번 포스팅은 바로 전 글에 이어서 타임리프의 기본 기능에 설명합니다.

2024.05.18 - [JSP | Thymeleaf] - [Thymeleaf] 타임리프 소개 및 기본 사용법(1/2) - 기본 기능

 

 


 

Thymeleaf -  주석

 

타임리프는 표준 HTML 주석, 타임리프 파서 주석, 타임리프 프로토타입 주석 등 여러 종류의 주석을 제공합니다.

 

 

1. 표준 HTML 주석

자바스크립트의 표준 HTML 주석은 타임리프가 렌더링 하지 않고 그대로 남겨둡니다.

<h1>1. 표준 HTML 주석</h1>
<!-- <span th:text="${data}">html data</span> -->

 

 

2. 타임리프 파서 주석

타임리프 파서 주석은 타임리프의 진짜 주석입니다.

렌더링에서 주석 부분을 제거합니다.

<h1>2. 타임리프 파서 주석</h1>
<!--/* [[${data}]] */--> 

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

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

 

위 코드에서 1번 방법과 2번 방법은 동일한 결과를 출력한다.

(블로그에서 볼때는 색깔이 표시되서 주석처리가 안된듯한 혼란을 일으킵니다.)

 

 

3. 타임리프 프로토타입 주석

타임리프 프로토타입은 약간 특이한데, HTML 주석에 약간의 구문을 더한 주석입니다.

 

HTML 파일을 웹 브라우저에서 그대로 열어보면 HTML 주석이기 때문에 이 부분이 웹 브라우저가 렌더링 하지 않습니다.

타임리프 렌더링을 거치면 이 부분이 정상 렌더링 됩니다.

 

즉, HTML 파일을 그대로 열어보면 주석처리가 되고, 타임리프를 렌더링 한 경우에는 보이는 주석입니다.

<h1>3. 타임리프 프로토타입 주석</h1>
<!--/*/
<span th:text="${data}">html data</span>
/*/-->

 

 

[예제코드 - 컨트롤러]

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

 

 

[뷰 템플릿]

<h1>예시</h1>
<span th:text="${data}">html data</span>
<h1>1. 표준 HTML 주석</h1>
<!--
<span th:text="${data}">html data</span>
-->

<h1>2. 타임리프 파서 주석</h1>
<!--/* [[${data}]] */-->
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->

<h1>3. 타임리프 프로토타입 주석</h1>
<!--/*/
<span th:text="${data}">html data</span>
/*/-->

 

 

[결과]

(왼) 렌더링 결과 페이지 / (오) 해당 페이지 소스 검사

 

 

추가로 위에서 설명한 타임리프 프로토타입 주석의 기능을 결과로 확인해보자.

 

아래 결과는 URL을 통해 타임리프 렌더링을 거치지 않고, 직접 파일 경로를 입력해 웹 브라우저에서 HTML 파일을 그대로 열어본 결과 화면이다. 타임리프 프로토타입 주석으로 감싼 "spring!" 문자열이 출력되지 않은것을 볼 수 있다.

 


 

블록

타임리프는 자체 태그인 <th:block> 태그를 제공합니다.

해당 태그는 HTML 태그가 아닌, 타임리프에서만 사용하는 태그로 해결하기 어려운 것들을 해결하기 위해 타임리프에서 제공하는 유일한 자체 태그인 블록을 제공합니다.

 

<th:block>은 렌더링시 제거되는 태그입니다.

안에 내용은 제거되지 않고, <th:block> 태그만 제거됩니다.

 

대표적으로 th:each를 통해 반복을 할 때, 반복의 대상이 한 요소가 아닌 동등한 레벨의 여러 요소를 그룹화하여 반복하고자 할때 <th:block>이 유용합니다.

 

 

[예제 코드 - 컨트롤러]

@GetMapping("/block")
public String block(Model model) {
    addUsers(model);
    return "basic/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>

 

 

[결과]

 

위 코드에서 <div> 태그를 두 개로 묶어서 반복을 하려고 할 때 <th:block> 블록을 사용하였다.

 

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

 

 

여기서 의문점이 들 수 있습니다.

 

"굳이 <th:block> 태그를 사용해야 하나요? 그냥 <div> 태그로 반복 해야 하는 2개의 <div> 태그를 묶어서 반복하면 되지 않을까요?" 라는 생각을 하실 수 있습니다.

 

블록을 사용하는 여러가지 이유가 있습니다.

마크업 유지, 반복 구문 및 조건문 처리, 복잡한 표현식 처리 등이 있으며 추가로 중복된 태그를 피한다는 장점이 있습니다.

 

<div> 태그를 <th:block> 대신 사용하게 되면 HTML 구조에 불필요한 중첩이 일어날 수 있습니다.

특히, CSS를 같이 사용하는 경우 특정 스타일링이나 레이아웃에 영향을 줄 수도 있기 때문에 <th:block> 사용을 지향하는 게 좋습니다.

 


 

자바스크립트 온라인

되게 게임이름 같은 기능ㅇ..

 

타임리프는 자바스크립트에서 타임리프를 편리하게 사용할 수 있는 자바스크립트 인라인 기능을 제공합니다.

 

타임리프의 자바스크립트 인라인 기능을 이용하면 자바스크립트(Javascript)에 변수를 담거나, 객체를 담는 경우 쉽게 사용이 가능합니다.

 

문자열은 쌍따옴표(")를 자동으로 추가하여 대입해주고, 객체인 경우 JSON 형태로 변환하여 대입해줍니다.

 

또한, 변수에 포함된 쌍따옴표(") 같은 자바스크립트에 문제가 될 수 있는 문자가 있으면, 이스케이프(escape) 처리도 자동으로 해줍니다. (예, " -> \")

 

쉽게 이해하기 위해 예제를 확인해보자.

 

 

[예제코드 - 컨트롤러]

@GetMapping("/javascript")
public String javascript(Model model) {
    model.addAttribute("user", new User("UserA", 10));
    addUsers(model);
    return "basic/javascript";
}

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

 

 

[뷰 템플릿]

<!-- 자바스크립트 인라인 사용 전 -->
<script>
    var username = [[${user.username}]];
    var age = [[${user.age}]];

    //자바스크립트 내추럴 템플릿
    var username2 = /*[[${user.username}]]*/ "test username";

    //객체
    var user = [[${user}]];
</script>

<!-- 자바스크립트 인라인 사용 후 -->
<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;
    • 인라인 사용 후 -> var username = "userA";
  • 인라인 사용 전 렌더링 결과를 보면 userA라는 변수 이름이 그대로 남아있습니다. 하지만 개발자가 기대한 것은 "userA"라는 문자입니다. 결과적으로 userA가 변수명으로 사용되어서 자바스크립트 오류가 발생합니다. 다음으로 숫자 age의 경우에는 "(쌍따옴표)가 필요 없기 때문에 정상 렌더링 됩니다.
  • 인라인 사용 후 렌더링 결과를 보면 문자 타입인 경우 "(쌍따옴표)를 포함해줍니다. 추가로 위에서 설명했듯이 자바스크립트에서 문제가 될 수 있는 문자가 포함되어 있다면 이스케이프 처리도 해줍니다.

 

 

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

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

자바스크립트 인라인 기능을 사용하면 주석을 활용해서 이 기능을 사용할 수 있습니다.

  • var username2 = /*[[${user.username]]*/ "test username";
    • 인라인 사용 전 -> var username2 = /*userA*/ "test username";
    • 인라인 사용 후 -> var username2 = "userA";
  • 인라인 사용 전 결과를 보면 정말 순수하게 그대로 해석이 된 것을 볼 수 있습니다. 따라서 내추럴 템플릿 기능이 동작하지 않고, 심지어 렌더링 내용이 주석처리 되어 버립니다.
  • 인라인 사용 후 결과를 보면 주석 부분이 제거되고, 기대한 "userA"가 정확하게 적용됩니다.

 

 

객체

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

  • var user = [[${user}]];
    • 인라인 사용 전 -> var user = BasicController.User(username=userA, age=10);
    • 인라인 사용 후 -> var user = {"username" : "userA", "age":10};
  • 인라인 사용 전은 객체의 toString()이 호출된 값입니다.
  • 인라인 사용 후는 객체를 JSON으로 변환해줍니다.

 

 

자바스크립트 인라인 each

자바스크립트 인라인은 반복으로 each를 지원하는데, 다음과 같이 사용합니다.

<!-- 자바스크립트 인라인 each -->
<script th:inline="javascript">
    [# th:each="user, stat : ${users}"]
    var user[[${stat.count}]] = [[${user}]];
    [/]
</script>

 

 

[결과]

 


 

템플릿 조각

웹 페이지를 개발을 하게 되면 상단 영역(Header), 하단 영역(Footer) 등 공통 영역이 많습니다.

HTML 파일마다 이렇게 공통되는 부분을 복사해서 사용한다면 변경 시, 모든 HTML 파일을 수정해야 하므로 상당히 비효율적입니다. 타임리프는 이런 문제를 해결하기 위해 템플릿 조각과 레이아웃 기능을 지원합니다.

  • th:fragment="이름" 속성을 추가하면 템플릿 조각이 되어, 다른 곳에서 불러와 사용이 가능합니다.
  • th:insert, th:replace 템플릿 조각을 가져와 사용할 수 있습니다.

 

[예제코드 - 컨트롤러]

@Controller
@RequestMapping("/template")
public class TemplateController {

    @GetMapping("/fragment")
    public String template() {
        return "template/fragment/fragmentMain";
    }
}

 

 

[뷰 템플릿 - footer.html]

<footer th:fragment="copy"> 푸터 자리 입니다.
</footer>
<footer th:fragment="copyParam (param1, param2)">
    <p>파라미터 자리 입니다.</p>
    <p th:text="${param1}"></p>
    <p th:text="${param2}"></p>
</footer>

 

th:fragment 가 있는 태그는 다른곳에 포함되는 코드 조각으로 이해하면 된다.

 

 

[뷰 템플릿 - fragmentMain.html]

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

 

 

 

부분 포함 insert

  • "~{템플릿 HTML 경로 :: 조각이름}"
    • 해당 파일에 있는 조각(fragment) 중에 조각이름에 해당하는 템플릿을 가져와 현재 태그(div) 내부에 추가합니다.
<h2>부분 포함 insert</h2>
<div>
<footer>
푸터 자리입니다.
</footer>
</div>

 

 

부분 포함 replace

  • "~{템플릿 HTML 경로 :: 조각이름}"
    • 해당 파일에 있는 조각(fragment) 중에 조각이름에 해당하는 템플릿을 가져와 해당 태그와 교체합니다.
<h2>부분 포함 replace</h2>
<footer>
푸터 자리입니다.
</footer>

 

 

부분 포함 단순 표현식

  • "~템플릿 HTML 경로 :: 조각이름
    • ~{...}를 사용하는 것이 원칙이지만, 템플릿 조각을 사용하는 코드가 단순하면 이 부분을 생략가능 합니다.

 

파라미터 사용

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

  • "~{템플릿 HTML 경로 :: 조각이름 ("데이터1", "데이터2")}"
<h2>부분 포함 replace</h2>
<footer>
<p>파라미터 자리 입니다.</p>
<p>데이터1</p>
<p>데이터2</p>
</footer>

 


 

템플릿 레이아웃

이전 코드로는 JSP의 include 처럼 다른 템플릿의 특정 영역을 가져와 사용하였습니다.

이번에는 여기서 더 확장하여 코드 조각을 레이아웃 개념으로 사용하는 방법을 알아보겠습니다.

 

예를 들어, <head>에 공통으로 사용하는 css, javascript 같은 정보들이 있는데, 이러한 공통 정보들을 한 곳에 모아두고 공통으로 사용하지만, 각 페이지마다 필요한 정보를 추가해서 사용하고 싶다면 다음과 같이 템플릿 레이아웃을 사용할 수 있다.

 

 

[예제 코드 - 컨트롤러]

@GetMapping("/layout")
public String layout() {
    return "template/layout/layoutMain";
}

 

 

[뷰 템플릿 - base.html]

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

 

 

[뷰 템플릿 - layoutMain.html]

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

 

 

[결과]

 

 

common_header(~{::title}, ~{::link}) 이 부분이 복잡해보이지만, 핵심이니 눈여겨 보자.

  • ::title 은 현재 페이지의 title 태그들을 전달합니다.
  • ::link 는 현재 페이지의 link 태그들을 전달합니다.

 

결과를 보면 메인 타이틀이 전달한 부분으로 교체 된 것을 확인할 수 있다.

공통 부분은 그대로 유지되고, 추각 부분에 전달한 <link> 들이 포함된 것을 확인할 수 있습니다.

 

어떻게 보면, 이 방식은 앞서 배운 코드 조각을 좀더 적극적으로 사용하는 방식입니다.

쉽게 이야기해서 레이아웃 개념을 두고, 그 레이아웃에 필요한 코드 조각을 전달해서 완성하는 것으로 이해하면 됩니다.

 

 

템플릿 레이아웃 확장

앞서 이야기한 개념을 <head> 정도에만 적용하는게 아닌, <html> 전체에도 적용가능 합니다.

 

[예제코드 - 컨트롤러]

@GetMapping("/layoutExtend")
public String layoutExtends() {
    return "template/layoutExtend/layoutExtendMain";
}

 

 

[뷰 템플릿 - layoutFile.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>

 

 

[뷰 템플릿 - layoutExtendMain.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>

 

 

[결과]

 

 

layoutFile.html 을 보면 기본 레이아웃을 가지고 있는데, <html>에 th:fragment 속성이 정의되어 있습니다.

이 레이아웃 파일을 기본으로 하고 여기에 필요한 내용을 전달해서 부분부분 변경하는 것으로 이해하면 됩니다.

 

layoutExtendMain.html 은 현재 페이지인데, <html> 자체를 th:replace 를 사용해서 변경하는 것을 확인 할 수 있습니다. 결국 layoutFile.html 에 필요한 내용을 전달하면서 <html> 자체를 layoutFile.html 로 변경합니다.

 


 

출처(참고)

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

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

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