thymeleaf 레이아웃
타임리프에서 레이아웃을 처리하려면, common.html과 같은 레이아웃 파일을 만들고, 별도 페이지에서 th:replace=”layout/common :: common_header(~{::title}, ~{::link}, ~{::script}, ~{::main})” 와 같은 방식으로 레이아웃을 가져올 수 있다.
common.html
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" th:fragment="common_header(title, links, scripts, content)">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- 타이틀 -->
<title th:replace="${title}">title</title>
<!-- 공통 -->
<!-- link, script -->
<th:block th:replace="${links}"></th:block>
<th:block th:replace="${scripts}"></th:block>
</head>
<body>
<main th:replace="${content}">
메인 콘텐츠
</main>
</body>
</html>
index.html
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout/common :: common_header(~{::title}, ~{::link}, ~{::script}, ~{::main})}">
<head>
<title>온라인 도서관</title>
<script defer type="module" src="../js/user.js" th:src="@{/js/user.js}"></script>
</head>
<body>
<main>
<!-- user가 없을 경우에 폼 보여줌 -->
<div id="login" th:if="${session.user == null}">
<form method="post" action="login">
<legend>로그인</legend>
<input type="text" name="username"/>
<input type="password" name="password"/>
<button type="submit">로그인</button>
</form>
<a href="join">회원가입</a>
</div>
<!-- user가 있을 경우에 인사 -->
<p th:if="${session.user != null}">안녕하세요, <span th:text="${session.user.username}">username</span>님!</p>
<!-- books가 있을 경우 목록 보여줌 -->
<ul th:if="${books != null}">
<li th:each="book : ${books}">
<span th:text="${book.name}">책 이름</span>
<span th:text="${book.isbn}">책 isbn</span>
<button th:value="${book.id}" onclick="rent(this.value)">대출</button>
<button th:value="${book.id}" onclick="unRent(this.value)">반납</button>
</li>
</ul>
</main>
</body>
</html>
link, script가 없을 경우
단, link, script는 개별 페이지에서 존재하지 않을 경우가 있다. 위의 인덱스 페이지의 경우에도 link 태그는 별도로 없다. 이럴 경우 어떻게 처리를 해줘야 할까? common.html에서는 한번 더 th:block으로 감싸서 조건부 처리를 해 주고, index.html에서는 ?: null로 null 처리를 해준다.
common.html
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" th:fragment="common_header(title, links, scripts, content)">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- 타이틀 -->
<title th:replace="${title}">title</title>
<!-- 공통 -->
<!-- link, script -->
<th:block th:if="${links != null}">
<th:block th:replace="${links}"/>
</th:block>
<th:block th:if="${scripts != null}">
<th:block th:replace="${scripts}"/>
</th:block>
</head>
<body>
<main th:replace="${content}">
메인 콘텐츠
</main>
</body>
</html>
index.html
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout/common :: common_header(~{::title}, ~{::link ?: null}, ~{::script ?: null}, ~{::main})}">
<head>
<title>온라인 도서관</title>
<script defer type="module" src="../js/user.js" th:src="@{/js/user.js}"></script>
</head>
<body>
<main>
<!-- user가 없을 경우에 폼 보여줌 -->
<div id="login" th:if="${session.user == null}">
<form method="post" action="login">
<legend>로그인</legend>
<input type="text" name="username"/>
<input type="password" name="password"/>
<button type="submit">로그인</button>
</form>
<a href="join">회원가입</a>
</div>
<!-- user가 있을 경우에 인사 -->
<p th:if="${session.user != null}">안녕하세요, <span th:text="${session.user.username}">username</span>님!</p>
<!-- books가 있을 경우 목록 보여줌 -->
<ul th:if="${books != null}">
<li th:each="book : ${books}">
<span th:text="${book.name}">책 이름</span>
<span th:text="${book.isbn}">책 isbn</span>
<button th:value="${book.id}" onclick="rent(this.value)">대출</button>
<button th:value="${book.id}" onclick="unRent(this.value)">반납</button>
</li>
</ul>
</main>
</body>
</html>
굳이 블록을 한 번 더 감싸야 할까?
<th:block th:if="${links != null}"
<th:block th:replace="${links}"/>
</th:block>
위 코드에서 의문이 들 수 있다. 굳이 두 번 감싸지 않고,
<th:block th:if="${links != null}" th:replace="${links}"/>
이렇게 해 주면 안 될까?! 라고 생각했지만, th:replace=””가 if와 함께 평가되기 때문에, 다음과 같은 에러가 발생한다.
org.thymeleaf.exceptions.TemplateInputException: Error resolving fragment: “${links}”: template or fragment could not be resolved (template: “layout/common” – line 14, col 40)
org.thymeleaf.exceptions.TemplateInputException
은 Thymeleaf가 템플릿이나 프래그먼트를 처리하는 동안 오류가 발생했을 때 던지는 예외이다. 즉, if를 먼저 평가하고 해당하지 않을 경우 다른 th를 평가하지 않는 게 아니므로 문제가 발생한다.
더 좋은 해결 방법이 있다면, 댓글로 남겨주길! 🙏