Thymeleaf 기본기능 정복하기[2편]

gwjeon·2021년 11월 15일
3



학습참조
인프런-스프링 MVC 2편 백엔드 웹 개발 활용 기술(김영한님)


이 글을 작성하고 난 뒤에는 내가 나중에 타임리프를 사용할 때에, 문법이 잘 기억이 나지 않을 때 이 글만 보아도 기본적인 타임리프 기능들을 사용할 수 있었으면 한다.


✅ 다루는 기능들

🚩 연산
🚩 속성값 설정
🚩 반복 - th:each
🚩 조건 - th:if, th:unless, th:switch
🚩 주석
🚩 블록
🚩 JavaScript 인라인 사용
🚩 레이아웃 템플릿

✅ 연산

타임리프 연산은 일반적인 프로그래밍 연산과 크게 다르지 않다.
하지만 HTML 안에서 사용하는 것이기 때문에 HTML 엔티티를 사용하는 부분을 주의 해야한다.
HTML 엔티티가 사용되는 것은 대부분이 비교 연산임으로 이때는 다음과 같은 문법을 사용할 것을 권장한다.

✔ 비교 연산 대체 키워드

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

물론 < , > 에 대한 엔티티만 조심한다면 ==, !, !=, >=, <= 와 같은 연산은 그냥 사용해도 무방하다.


✔ 삼항 연산자

타임리프에서는 Elvis 연산자를 지원하는데 예를 들어 서버로 부터 data라는 객체를 전달받았다고 가정 해보자.

<span th:text="${data} ? ${data} : '데이터가 없습니다.'"></span> <!-- (1) -->
<span th:text="${data} ?: '데이터가 없습니다.'"></span> <!-- (2) -->
  • (1): 일반적으로 사용하는 삼항 연산자이다. data 변수가 참이면 data를, 거짓이라면 데이터가 없습니다.를 출력한다.
  • (2): Elvis 연산자를 사용한 것으로 동작은 (1)과 동일하게 동작하며 참일때의 부분이 생략되어 있다.

✔ No-Operation

No-Operation이란 '_' 언더스코어(밑줄)을 사용하여 마치 타임리프가 실행되지 않는 것처럼 동작하게끔 한다.

<span th:text="${data} ?: '데이터가 없습니다.'"></span> <!-- (1) -->
<span th:text="${data} ?: _">데이터가 없습니다.</span> <!-- (2) -->

위 코드는 서로 다르지만 같은 결과를 가진다.

  • (1): 위에서 설명한 Elvis 연산자이다.
  • (2): Elvis 연산자에서 데이터가 없습니다. 문자가 아닌 '_' 언더스코어(밑줄)을 사용했다. 타임리프를 문법을 사용하지 않고 HTML 내용을 그대로 사용하겠다는 의미이다. 즉 data가 거짓이라면 span 내에 삽입된 데이터가 없습니다.가 출력된다.

이렇듯 No-Operation을 적절히 잘 확용 한다면 HTML의 내용을 그대로 사용할 수 있다.


✅ 속성값 설정

타임리프에서도 HTML 속성을 동적으로 설정이 가능한데 Tag에 th:* 속성을 지정하는 방식으로 사용한다. 속성을 적용하면 기존 속성이 대체되고 없다면 새로 만든다.

속성을 새로이 설정하는 방법과 속성을 추가하는 방법, checked 처리를 하는 방법을 알아보자.

✔ 속성 설정

<input type="text" name="mock" th:name="userA" />

name 속성 mock이 타임리프로 렌더링 되며 userA로 변경된다.


✔ 속성 추가

<input type="text" class="text" th:attrappend="class='large'" /> <!-- (1) --> 
<!-- @실제 렌더링시... <input type="text" class="textlarge" /> -->
<input type="text" class="text" th:attrprepend="class='large '" /> <!-- (2) -->
<!-- @실제 렌더링시... <input type="text" class="large text" /> -->
<input type="text" class="text" th:classappend="large" /> <!-- (3) -->
<!-- @실제 렌더링시... <input type="text" class="text large" /> -->
  • (1): attrappend는 해당 속성의 마지막에 속성을 추가하며 class 속성의 마지막에 large를 추가한다. 실제 렌더링시 아래 주석과 같이 생성된다. 공백이 없으므로 기존의 text와 large가 붙어서 나오게 된다.
  • (2): attrprepend는 해당 속성의 첫번째에 속성을 추가하며 class 속성의 첫번째에 large를 추가한다. 실제 렌더링시 아래 주석과 같이 생성된다. 공백이 있으므로 large와 text 2개의 class 선택자를 가진다.
  • (3): classappend는 class 속성의 마지막에 속성을 추가하며 class 속성의 마지막에 large를 추가한다. 실제 렌더링시 아래 주석과 같이 생성된다. attrappend가 아닌 클래스를 추가하는 append로써 공백이 없어도 large와 text 2개의 class 선택자를 가진다.

✔ checked 처리

<input type="checkbox" name="active" th:checked="true" /> <!-- (1) -->
<input type="checkbox" name="active" th:checked="false" /> <!-- (2) --> 
<input type="checkbox" name="active" checked="false" /> <!-- (3) -->

HTML은 checked 속성만 들어가 있어도 내부의 값을 상관하지 않고 체크상태로 표시하는데 타임리프에서는 true와 false로 이를 제어할 수 있다.

  • (1): check된 상태.
  • (2): check되지 않은 상태.
  • (3): check된 상태, HTML에서는 checked 속성만 들어가 있어도 내부의 값을 상관하지 않고 체크상태로 표시한다.

✅ 반복 - th:each

모든 템플릿에 반복이 존재하듯 타임리프에서도 반복을 수행할 수 있다. 반복문을 수행하는 타임리프 키워드는 다음과 같다.
th:each="item : ${items}"

✔ 3개의 User가 있는 List를 반복하는 출력

⭐ controller

@GetMapping("...")
public String ...(Model model) {
    addUser(model);
    return "...";
}

private void addUser(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);
}

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

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

userA, userB, userC 총 3개의 User를 가지는 list가 model에 담겨 전달되었다.

⭐ th:each 수행

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

th:each문은 java의 다음 코드와 동일하다.

for(User user : users) {
system.out.println(user.getUsername());
system.out.println(user.getAge());
}

즉 tr Tag를 users의 길이만큼 loop를 돌면서 생성하면서 내부에 td Tag를 생성한다. 실제 렌더된 결과는 다음과 같다.

출력


✔ 반복중 사용 가능한 반복상태 값

반복중 현재 몇번째 index가 반복되고 있는지, 그 index의 count는 몇개인지, 반복되는 List(컬렉션 등)의 총 사이즈는 얼마인지, 현재 짝수 반복인지, 홀수 반복인지 기타등 편리한 기능을 제공한다.

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

⭐ th:each 수행

<table border="1">
    <tr>
        <th>count</th>
        <th>username</th>
        <th>age</th>
        <th>etc</th>
    </tr>
    <tr th:each="user, userStat : ${users}"> <!-- userStat 생략 가능 -->
        <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>

userStat라는 두번째 파라미터를 지정하여 위와 같이 반복중에 필요한 상태를 확인할 수 있다.

출력

사실 userStat를 생략할 수 있다. 기본적으로 타임리프에서 user + 'Stat' 자동으로 만들어주기 때문이다. 다음과 같이 작성하여도 userStat를 사용했을때 문제없이 동작한다.

<tr th:each="user : ${users}">

✅ 조건 - th:if, th:unless, th:switch

타임리프에서는 3가지 조건부 평가가 가능하다.

⭐ controller

@GetMapping("...")
public String ...(Model model) {
    addUser(model);
    return "...";
}

private void addUser(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);
}

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

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

✔ th:if, th:unless

<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> <!-- (1) -->
            <span th:text="'미성년자'" th:unless="${user.age ge 20}"></span> <!-- (2) -->
        </td>
    </tr>
</table>
  • (1): th:if는 말 그대로 조건이 참일때 수행하며, 만약 조건이 거짓일 시 span 자체도 생성되지 않는다.
  • (2): th:unless는 if의 반대로 not을 의미한다. if(!(true))와 같은 의미이다. 해당 조건이 거짓일 때만 수행하며, 만약 조건이 참이라면 span 자체도 생성되지 않는다.
출력


✔ th:switch

<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}">  <!-- (1) -->
            <span th:case="10">10살</span>  <!-- (2) -->
            <span th:case="20">20살</span>
            <span th:case="*">기타</span> <!-- (3) -->
        </td>
    </tr>
</table>
  • (1): 일반적인 switch문과 동일하게 ${user.age}의 값과 맞는 값을 찾는 switch문 이다.
  • (2): ${user.age}의 값이 10일 경우 span 태그를 생성한다.
  • (3): java switch에서 default를 의미하며 *로 표시한다. ${user.age}의 값이 10,20이 아니라면 해당 span 태그가 생성될 것이다.
출력


✅ 주석

타임리프에서 사용가능한 주석은 3가지 타입이 있다.

✔ 표준 HTML 주석

<!-- <span>표준 HTML 주석</span> -->

표준 HTML 주석은 타임리프가 렌더링 하지 않고, 그대로 남겨둔다.

✔ 타임리프 파서 주석

<!--/* <span>표준 HTML 주석</span> */-->

타임리프 파서 주석은 타임리프의 진짜 주석이다. 타임리프 렌더링에서 주석 부분을 제거한다.

✔ 타임리프 프로토타입 주석

<!--/*/ <span>표준 HTML 주석</span> /*/-->

타임리프 프로토타입은 약간 특이한데, HTML 주석에 약간의 구문을 더했다. HTML 파일을 웹 브라우저에서 그대로 열어보면 <!-- -->을 포함하는 HTML 주석이기 때문에 이 부분이 웹 브라우저가 렌더링하지 않는다. 타임리프 렌더링을 거치면 이 부분이 정상 렌더링 된다. HTML 파일을 그대로 열어보면 주석처리가 되지만, 타임리프를 통해 렌더링 한 경우에만 출력된다.


✅ 블록

타임리프 블록 <th:block>은 HTML 태그가 아닌 타임리프의 유일한 자체 태그이다. 특정한 HTML 태그에 for loop를 지정하는것이 아닌 특정 구역에 대한 loop를 돌기 위하여 사용한다.

⭐ controller

@GetMapping("...")
public String ...(Model model) {
    addUser(model);
    return "...";
}

private void addUser(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);
}

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

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

✔ th:block

<th:block th:each="user : ${users}">
    <div>
        사용자 이름 <span th:text="${user.username}"></span>
        사용자 나이 <span th:text="${user.age}"></span>
    </div>
    <div>
        요약 <span th:text="|${user.username} / ${user.age}|"></span>
    </div>
</th:block>
출력
사용자 이름1 userA 사용자 나이1 10
요약 userA / 10
사용자 이름1 userB 사용자 나이1 20
요약 userB / 20
사용자 이름1 userC 사용자 나이1 30
요약 userC / 30

div 2개에 대한 loop를 돌기 위하여 th:block 이란 특수한 타임리프 태그를 만들어 반복하도록 했다. 사실 위 코드는 아래와 유사하다.

<div th:each="user : ${users}">
    <div>
        사용자 이름 <span th:text="${user.username}"></span>
        사용자 나이 <span th:text="${user.age}"></span>
    </div>
    <div>
        요약 <span th:text="|${user.username} / ${user.age}|"></span>
    </div>
</div>

위와 같이 바깥 loop에 th:block이 아닌 div로 사용하여도 똑같은 결과를 낼 수 있지만 HTML 태그가 추가로 필요한 다른 점이 존재한다. 가능하면 태그에 for loop를 넣는것을 추천하지만 해결하기 힘든 특별한 반복이 필요한 경우에 사용하도록 하자.


✅ JavaScript 인라인 사용

타임리프 HTML파일에서 자바스크립트 사용시 편리하게 사용할 수 있는 인라인 기능을 제공한다. 인라인 기능을 사용하고자 할때는 <script th:inline="javascript"></script> 처럼 선언해주어야 한다.

  • 일반적인 자바스크립트 사용 선언: <script></script>
  • 타임리프 자바스크립트 인라인 사용 선언: <script th:inline="javascript"></script>

⭐ controller

@GetMapping("...")
public String ...(Model model) {
    addUser(model);
    return "...";
}

private void addUser(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);
}

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

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

✔ 일반적인 자바스크립트 사용

<!-- 자바스크립트 인라인 사용 전 -->
<script>
 var username = [[${user.username}]];
 var age = [[${user.age}]];
 //자바스크립트 내추럴 템플릿
 var username2 = /*[[${user.username}]]*/ "test username";
 //객체
 var user = [[${user}]];
</script>

타임리프에서 컨텐츠를 출력하는 [[...]] 문법을 사용한다면 자바스크립트에서도 model의 데이터를 가질 수 있다.

⭐ 결과

<script>
var username = userA; //(1) Uncaught ReferenceError: userA is not defined
var age = 10;
//자바스크립트 내추럴 템플릿
var username2 = /*userA*/ "test username"; //(2)
//객체
var user = BasicController.User(username=userA, age=10); //(3)
</script>
  • (1): 앞서 [[..]] 문법을 통해 user.username을 username 변수에 저장하고자 했지만 Uncaught ReferenceError: userA is not defined 에러가 발생한다. 이유는 userA는 문자열이므로 ' ' 또는 " "로 감싸야 하지만 타임리프의 컨텐츠 출력 문법에서는 user.username 값인 userA를 그대로 저장하려 했으므로 에러가 발생한다. 이러한 문제를 해결하기 위해서는 var username = '[[${user.username}]]';로 선언해야 한다.
  • (2): 타임리프를 사용하기 되면 자바스크립트 역시 내추럴 템플릿을 지원하게끔 활용할 수 있다. 하지만 일반적인 자바스크립트 사용에서는 앞서 주석처리 된 부분은 무시되고 "test username"이 그대로 username2 변수에 저장된다.
  • (3): 원하던 결과는 user 객체를 자바스크립트 문법으로 사용하길 원했으나, user객체가 toString()된 결과이다.

위 3가지 문제를 인라인 자바스크립트를 사용함으로써 해결할 수 있다.


✔ 인라인 자바스크립트 사용

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

script 태그안에 th:inline="javascript"를 추가한다.

⭐ 결과

<script>
var username = "userA"; //(1)
var age = 10;
//자바스크립트 내추럴 템플릿
var username2 = "userA"; //(2)
//객체
var user = {"username":"userA","age":10}; //(3)
</script>
  • (1): user.username 값 userA가 자동으로 문자열로 변환되어 저장된다.
  • (2): 실제 작성한 var username2 = /*[[${user.username}]]*/ "test username"; 코드에서 타임리프에서 렌더링시 /*[[${user.username}]]*/ 부분이 "test username"부분과 치환되어 출력된다. 이렇듯 소스파일을 그대로 열었을 경우 내추럴한 결과를 유지하지만 타임리프 렌더링시 값이 치환되어 내추럴 템플릿을 가능하게끔 한다.
  • (3): 자동으로 json을 파싱하여 자바스크립트에서 사용가능한 객체로 만들어 저장된다.

⭐ 인라인 자바스크립트 th:each 사용

인라인 자바크스립트 사용에서 model로 부터 전달받은 데이터를 타임리프의 th:each를 통하여 특별한 반복을 수행할 수 있다.

<!-- 자바스크립트 인라인 each -->
<script th:inline="javascript">
 [# th:each="user, stat : ${users}"] //(1)
 var user[[${stat.count}]] = [[${user}]]; //(2)
 [/]
</script>
  • (1): 기존의 html body내에서 사용하던 타임리프 th:each와 유사하게 사용할 수 있다.
  • (2): stat.count를 이용하여 매 반복마다 동적인 변수 이름 user1, user2, user3 을 생성할 수 있으며 해당 변수에 user 객체를 저장한다.
결과
<script>
var user1 = {"username":"userA","age":10};
var user2 = {"username":"userB","age":20};
var user3 = {"username":"userC","age":30};
</script>

매 반복마다 동적인 변수 이름을 생성하고 각 객체들이 변수에 저장 된다.

  • 1회전: var user1 = {"username":"userA","age":10};
  • 2회전: var user2 = {"username":"userB","age":20};
  • 3회전: var user3 = {"username":"userC","age":30};

✅ 레이아웃 템플릿

타임리프에서는 각 페이지마자 공통되는, 예를들어 head, footer, 네비게이션바 등, HTML 태그들을 효율적으로 관리하기 위하여 템플릿 조각과 레이아웃 기능을 지원한다.

✔ 템플릿 조각

⭐ template/fragment/footer.html

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

<body>

<!-- 템플릿 조각으로 사용하고자 할때는 th:fragment="..." 이름을 선언 해준다. -->
<footer th:fragment="copy"> <!-- (1) -->
  푸터 자리 입니다.
</footer>

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

</body>
</html>

⭐ template/fragment/main.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>
<!-- insert: div안에 fragment name copy 삽입 -->
<div th:insert="~{template/fragment/footer :: copy}"></div> <!-- (3) -->
<h2>부분 포함 replace</h2>
<!-- replace: div와 fragment name copy와 교체, 즉 div가 사라짐 -->
<div th:replace="~{template/fragment/footer :: copy}"></div> <!-- (4) -->
<h2>부분 포함 단순 표현식</h2>
<!-- ~{...} 를 사용하는 것이 원칙이지만 템플릿 조각을 사용하는 코드가 단순하면 이 부분을 생략할 수 있다. -->
<div th:replace="template/fragment/footer :: copy"></div>
<h1>파라미터 사용</h1>
<!-- 파라미터 사용: div와 fragment name copyParam와 교체하며 파라미터 사용 가능 -->
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div> <!-- (5) -->
</body>
</html>
  • (1): footer tag에 th:fragment="copy"를 사용함으로써 조각 이름을 copy로 지정하였다.
  • (2): fragment를 메서드처럼 파라미터로도 구현할 수 있다. 조각 이름은 copyParam이며 param1, param2 파라미터가 있다.
  • (3): th:insert는 해당 div안에 tag를 삽입하겠다는 의미이며, 삽입하려는 태그는 footer.html에 fragment의 이름이 copy인 footer tag이다.
  • (4): th:replace는 해당 div를 fragment와 교체 하겠다는 의미이며, 교체하려는 태그는 footer.html에 fragment의 이름이 copy인 footer tag이다.
  • (5): 파라미터를 사용하므로 '데이터1', '데이터2'가 footer.html의 copyParam fragment 파라미터로 넘어가서 출력된다.

⭐ 출력 HTML


<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>부분 포함</h1>
<h2>부분 포함 insert</h2>
<!-- insert: div안에 fragment name copy 삽입 -->
<div><footer>
  푸터 자리 입니다.
</footer></div>
<h2>부분 포함 replace</h2>
<!-- replace: div와 fragment name copy와 교체, 즉 div가 사라짐 -->
<footer>
  푸터 자리 입니다.
</footer>
<h2>부분 포함 단순 표현식</h2>
<!-- ~{...} 를 사용하는 것이 원칙이지만 템플릿 조각을 사용하는 코드가 단순하면 이 부분을 생략할 수 있다. -->
<footer>
  푸터 자리 입니다.
</footer>
<h1>파라미터 사용</h1>
<!-- 파라미터 사용: div와 fragment name copyParam와 교체하며 파라미터 사용 가능 -->
<footer>
  <p>파라미터 자리 입니다.</p>
  <p>데이터1</p>
  <p>데이터2</p>
</footer>
</body>
</html>

✔ 템플릿 레이아웃 <head> 삽입

각 페이지마다 공통된 <head>를 사용할 수 있도록 구성 해보자.

⭐ template/layout/base.html

<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="common_header(title,links)"> <!-- (1) -->

  <!--매개변수로 전달받은 title을 replace 한다.-->
  <title th:replace="${title}">레이아웃 타이틀</title> <!-- (2) -->

  <!-- 공통 -->
  <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>

  <!-- 추가 -->
  <!--매개변수로 전달받은 title를 replace 한다.-->
  <th:block th:replace="${links}" /> <!-- (3) -->
</head>

⭐ template/layout/layoutMain.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- template/layout/base.html의 common_header를 replace로 사용하고 title와 link tag를 매개변수로 넘긴다. -->
<head th:replace="template/layout/base :: common_header(~{::title},~{::link})"> <!-- (4) -->
    <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>
  • (1): 파라미터 타입의 head tag fragment이다.
  • (2): (1)에서 파라미터로 전달받은 title tag와 교체된다.
  • (3): (1)에서 파라미터로 잔달받은 link tag와 교체된다.
  • (4): head tag를 base.html의 common_header이름을 가지는 fragment와 교체(th:replace) 하겠다는 의미이며, ~{::title}, ~{::link}은 title,link tag를 파라미터로 넘기겠다는 의미이다. 즉 layoutMain.html의 head tag는 base.html의 head tag로 교체되며, 단 title,link tag들은 파라미터로 넘겨서 base.html의 head tag에서 (2), (3)과 교체된다.

⭐ 출력 HTML

<!DOCTYPE html>
<html>
<!-- template/layout/base.html의 common_header를 replace로 사용하고 title와 link tag를 매개변수로 넘긴다. -->
<head>

  <!--매개변수로 전달받은 title을 replace 한다.-->
  <title>메인 타이틀</title>

  <!-- 공통 -->
  <link rel="stylesheet" type="text/css" media="all" href="/css/awesomeapp.css">
  <link rel="shortcut icon" href="/images/favicon.ico">
  <script type="text/javascript" src="/sh/scripts/codebase.js"></script>

  <!-- 추가 -->
  <!--매개변수로 전달받은 title를 replace 한다.-->
  <link rel="stylesheet" href="/css/bootstrap.min.css"><link rel="stylesheet" href="/themes/smoothness/jquery-ui.css">
</head>
<body>
메인 컨텐츠
</body>
</html>

✔ 템플릿 레이아웃 <html> 삽입

모든 레이아웃을 가지는 html 파일을 사용하고 내부의 title과 body만 교체할 수 있도록 구성해보자.

⭐ template/layout/layoutFile.html

<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org"> <!-- (1) -->
<head>
    <title th:replace="${title}">레이아웃 타이틀</title> <!-- (2) -->
</head>
<body>
<h1>레이아웃 H1</h1>
<div th:replace="${content}"> <!-- (3) -->
    <p>레이아웃 컨텐츠</p>
</div>
<footer>
    레이아웃 푸터
</footer>
</body>
</html>

⭐ template/layout/layoutExtendMain.html

<!DOCTYPE html>
<html th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title},~{::section})}" xmlns:th="http://www.thymeleaf.org"> <!-- (4) -->
<head>
    <title>메인 페이지 타이틀</title>
</head>
<body>
<section>
    <p>메인 페이지 컨텐츠</p>
    <div>메인 페이지 포함 내용</div>
</section>
</body>
</html>
  • (1): 파라미터 타입의 html tag fragment이다. 즉 html 자체를 교체한다.
  • (2): (1)에서 파라미터로 전달받은 title tag와 교체된다.
  • (3): (1)에서 파라미터로 잔달받은 section tag와 교체된다.
  • (4): html tag를 layoutFile.html의 layout이름을 가지는 fragment와 교체(th:replace) 하겠다는 의미이며, ~{::title}, ~{::section}은 title,section tag를 파라미터로 넘기겠다는 의미이다. 즉 layoutExtendMain.html의 html tag는 layoutFile.html의 html tag로 교체되며, 단 title,section tag들은 파라미터로 넘겨서 layoutFile.html에서 (2), (3) tag와 교체된다.

⭐ 출력 HTML

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

profile
ansuzh

0개의 댓글