
레이아웃과 프래그먼트는 웹 애플리케이션의 템플릿 구조를 효율적으로 관리하고 일관성을 유지하기 위한 기능이다. 양 기능은 서로 보완적인 역할을 하며, 복잡한 웹 페이지를 설계하고 유지보수하는 데 자주 사용되는 방법이다.
레이아웃은 웹 애플리케이션의 공통된 페이지 구조를 정의하고 재사용할 수 있도록 도와주는 기능이다. 이를 통해 여러 페이지에서 일관된 레이아웃을 유지하면서도 페이지별로 고유한 콘텐츠를 쉽게 삽입할 수 있다.
웹 템플릿에서 재사용 가능한 부분적인 HTML 구성 요소를 정의하고 관리하는 강력한 기능이다. 프래그먼트는 웹 페이지의 구조와 디자인 요소를 모듈화하고, 템플릿의 재사용성과 유지보수성을 향상시키는 데 중대한 역할을 한다.
템플릿 내의 th:fragment 속성을 사용하여 정의된다.th:fragment는 HTML 요소에 대해 독립적인 구성 요소를 정의하며, 이를 통해 해당 요소의 특정 부분을 다른 템플릿에서 재사용할 수 있게 한다. 프래그먼트는 일반적으로 공통된 레이아웃 요소나 재사용 가능한 콘텐츠 블록을 정의하는 데 사용됩니다.
ul 태그에 붙였을 때
<ul th:each="item : ${shoppingList}">
<li>[[${item}]]</li>
</ul>
ul 태그에 th:each를 적용하면 ul 태그부터 반복된다.<ul>
<li>양파</li>
</ul>
<ul>
<li>감자</li>
</ul>
<ul>
<li>당근</li>
</ul>
li 태그에 붙였을 때
<ul>
<li th:each="item : ${shoppingList}">[[${item}]]</li>
</ul>
li 태그에 th:each를 적용하면 li태그부터 반복된다.<ul>
<li>양파</li>
<li>감자</li>
<li>당근</li>
</ul>
<form method="post" th:object="${post}">
<input type="hidden" value="user1" name="author">
<!--<input type="hidden" value="user1" th:field="*{author}">-->
author 에 value로 입력해둔 user1이 들어가지 않는다.th:field 는 value와 name을 자동 설정해주기 때문에 value를 덮어써버린다.입력한 값을 넘겨 받고 싶을 때는 @PostMapping 을 설정해두어야 한다.
@PostMapping("/4")
public String processSyntaxPage4(Post post) {
log.info("post.toString() = {}", post.toString());
return "/syntax/page4";
}
@GetMapping만 있었을 때는, 제출 버튼을 누르면 에러가 났다. 하지만, 이제는 요청 본문에 post 정보가 들어가서 전달된다.<a th:href="@{~/}">index</a>
~를 넣어주면 컨텍스트 루트(Context Root)를 기준으로 동적으로 경로를 넣어줄 수 있다.<li th:each="idx : ${#numbers.sequence(1,4)}">
<a th:href="@{~/page/{idx}(idx=${idx})}">/page/[[${idx}]]</a>
</li>
#numbers를 사용하여 숫자 리스트(시퀀스)를 생성하는 기능을 사용했다.{idx}에 동적으로 값을 넣어주고 그 값은 idx=${idx} 와 같이 th:each로 받아온 idx를 사용한다고 표시해두었다.템플릿 내에서 변수를 선언하고 사용할 때 쓰인다. 변수를 한 번 설정하면, 해당 블록 안에서 계속 사용할 수 있다.
<div th:with="name='구구단'">
타임리프에서 만들어주는 블록이다. html 태그 안에서 사용하기 애매한 경우에 이걸 넣어주면 알아서 바꿔준다.
<th:block th:each="idx : ${#numbers.sequence(1,9)}">
<p>[[${target}]] x [[${idx}]] = [[${target} * ${idx}]]</p>
</th:block>
HTML 태그를 생성하지 않고, 논리적으로 블록을 그룹핑할 때 사용한다. 주로 조건문, 반복문, 변수 정의 등을 깔끔하게 묶을 때 유용하다.
<input type="text" id="username" disabled>
<script th:inline="javascript">
const username =/*[[${username}]]*/ "hello";
const usernameEl = document.getElementById('username');
usernameEl.value = username;
</script>
/*[[${username}]]*/ 값이 있으면 주석을 해제하고 username 값을 담고, 없으면 주석이 아예 삭제되어 빈 상태가 된다.const username=;이 된다./*...*/을 HTML 렌더링 과정에서만 사용하고, 최종적으로 브라우저에 전달될 때는 제거한다.getElementByID로 input 태그를 객체로 가지고 있게 된다.usernameEl에 value라는 속성을 추가한 것이다.<html lang="en"
layout:decorate="~{layouts/basic_layout}">
layout:decorate는 Thymeleaf Layout Dialect라는 추가 라이브러리를 사용할 때 쓰는 문법이다.implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
common_headers.html
<!--common_headers.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<th:block th:fragment="commonHeader">
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</th:block>
</head>
</html>
header에 들어갈 부분이라, <body>부분을 지워도 상관이 없는 것이다.th:fragment로 지정해둔다.footer.html
<!--footer.html-->
<footer class="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10" th:fragment="footer">
... 생략
</footer>
footer에 필요한 부분이다. th:fragment로 지정해둔다.top_bar.html
<!--top_bar.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="navbar bg-base-200" th:fragment="top_bar">
<button th:if="${isLogin}" class="btn btn-ghost text-lg">Hello!</button>
<button th:unless="${isLogin}" class="btn btn-ghost text-lg">No Auth!</button>
</div>
</body>
</html>
top-bar에 필요한 부분을 구현하였고, th:fragment로 지정해두었다. basic_layout.html
<!DOCTYPE html>
<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<!--Thymeleaf Layout Dialect를 사용하기 위해 필요한 네임스페이스 선언이다.-->
<head>
<meta charset="UTF-8">
<title>Title</title>
<th:block th:insert="~{fragments/common_headers::commonHeader}"></th:block>
</head>
<body class="min-h-screen flex flex-col">
<div th:replace="~{fragments/top_bar::top_bar}"></div>
<div class="container mx-auto mt-4 flex-grow" layout:fragment="contents">
</div>
<footer th:replace="~{fragments/footer::footer}"></footer>
</body>
</html>
th:insert 는 기존 태그를 유지하되 태그 내부에 내용만 삽입한다.~{fragments/common_headers::commonHeader} : fragments 폴더에 common_headers 파일의 fragment commonHeader을 추가한다.th:replace 는 기존 태그가 제거되고 태그 자체를 대체한다.layout_page_2.html
<div layout:fragment="contents">
<p>레이아웃이 적용되었습니다!</p>
</div>
layout:fragment="이름" 을 사용하면 템플릿에서 해당 부분을 동적으로 변경할 수 있다.layout:fragment를 정의하고, 자식 파일(layout_page_2.html)에서 layout:decorate를 사용하여 layout:fragment를 교체하는 방식이다.<script th:inline="javascript">
const username =/*[[${username}]]*/"hello";
Q. 이 구문을 나는 username이 없을 때 "hello"를 기본값으로 넣어주기 위해 해두는 건 줄 알았다.. 근데, model에 username을 추가해주지 않았을 때 hello가 뜨지 않았고, 아무것도 뜨지 않았다.
왜지..?
A. Thymeleaf는 /*[[]]*/ 이 부분을 값이 있을 땐, 값을 넣어주고, 값이 없을 땐 주석으로 처리하고 아무것도 남기지 않는다. 즉, null이 되는 것이다.
Q. 그렇군.. 그럼 기본값으로의 역할은 하지 않는 거 아닌가? 저 "hello"는 왜 넣어주는 거지...?
A. 아직 잘 모르겠다.. AI에게 물어보니 저 구문은 애초에 기본값을 넣어주기 위한 구문이 아니라고 하던데,,, 너무 헷갈린다. 이건 다시 강사님께 여쭤보는 걸로..
A. 복습하면서 답을 알게됐다 ! 우선 포인트는 /[[]]/ 이걸 넣었을 때 타임리프가 값을 넣어준다. 그리고, 그 뒤의 구문은 "hello" 저걸 아예 빼버리면, IntelliJ에서 빨간 줄이 난다.. 또한, 정적 HTML을 실행시켰을 때 에러가 나버린다. 그렇게 되면 username을 사용하는 그 아래의 코드들은 실행이 되지 않는다. 그런 오류를 방지하기 위해 뒤에 붙여주는 거라고 한다.
오늘은 스크럼 시간에 평소보다 편하게 + 웃으면서 했던 것 같다. 서로 수업 시간 때 혹은 복습하면서 생긴 질문을 나누었다. 또, 팀원들 중 한 명이 피곤할 때만 강사님이 들어오시는데, 오늘 컨디션 좋았다고 하시면서 그런데 오늘 강사님 안 들어올 것 같아요.. 이러셨는데 ㅋㅋㅋㅋ 들어오셨다. 너무 웃겼음. 암튼 화기애애한 으쌰으쌰하는 스크럼 시간이었다 ! :)
타임리프를 어제 복습을 잘 해놔서 그런가 실습하면서 잘 따라갈 수 있었다. 그리고, 하시려는 걸 미리 만들어서 비교해볼 수도 있었다 ! 너무 뿌듯하잖아 ~ 근데 마지막 레이아웃, 프레그먼트에서 layout:fragment="contents" 이 부분에서 어디서 어디를 주입해주는 것인가.. 하는 것에서 좀 헷갈렸는데 정리하고 보니, 이해는 된다. 그치만, 바로바로 막 해석되지가 않아서 좀 더 익숙해져야 할 것 같다. 점점 스프링으로 들어오니 너무 재미있다 !!! 흡수해야 할 것은 많아지지만, 더 재밌어지는 것 같다. :) 그리고 오늘은 시간이 정말 빨리 갔다 ! 매일이 이러했으면.. ㅎ