기존 MVC 패턴 방식에는 JSP를 사용하였다.
서블릿이 자바 소스이다 보니 HTML 코드에서 JAVA 코드를 넣어 동적 웹페이지를 구성한다.
의존성 추가
🔖thymeleaf-spring6

🔖thymeleaf - java8time // JDK8 Date & TIME API
-> #temporals : 형식화

타임리프는 레이아웃 기능이 없다.
🔖thymeleaf layout :레이아웃 기능 추가

implementation 'org.thymeleaf:thymeleaf-spring6:3.1.2.RELEASE'
implementation 'org.thymeleaf.extras:thymeleaf-extras-java8time:3.0.4.RELEASE'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0'
Natural Template
참고로 jsp를 포함한 뷰 템플릿에서는 jsp코드와 html코드가 섞여있다.
원래 HTML과 서버사이드 렌더링 결과 거의 동일하게 보이는 효과
번역 기술
캐시 기능 제공
캐시 비활성화 (setCacheable(false))
목적: 개발 중에는 템플릿 파일을 자주 수정하고, 그 변경 사항을 즉시 확인하고자 합니다. 이 설정은 서버를 재시작하지 않고도 HTML 파일의 변경 사항이 즉시 반영되도록 합니다.
동작: 이 설정을 적용하면, 각 요청 시마다 템플릿 파일이 다시 로드됩니다. 즉, 변경된 템플릿 파일을 서버가 매번 읽어와서 최신 상태를 반영합니다.
캐시 활성화 (setCacheable(true)) //배포시
목적: 배포 환경에서는 템플릿 파일이 자주 변경되지 않으며, 성능 최적화가 중요합니다. 이 설정은 템플릿 파일을 캐시에 저장하여, 서버 성능을 최적화합니다.
동작: 이 설정을 적용하면, 템플릿 파일이 최초 요청 시에만 로드되고, 이후 요청부터는 캐시된 버전을 사용합니다. 따라서 변경된 템플릿 파일을 반영하려면 서버를 재시작해야 합니다.

개발 중에는 templateResolver.setCacheable(false)로 설정하여, 템플릿 파일이 변경될 때마다 다시 로드되도록 한다. 배포 중에는 templateResolver.setCacheable(true)로 설정



한글 안깨진다!
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입</title>
</head>
<body>
<h1>회원가입</h1>
<form method="post" autocomplete="off">
<dl>
<dt>이메일</dt>
<dd>
<input type="text" name="email" value="user01@test.org">
</dd>
</dl>
<dl>
<dt>비밀번호</dt>
<dd>
<input type="password" name="password">
</dd>
</dl>
<dl>
<dt>비밀번호 확인</dt>
<dd>
<input type="password" name="confirmPassword">
</dd>
</dl>
<dl>
<dt>회원명</dt>
<dd>
<input type="text" name="userName" value="사용자01">
</dd>
</dl>
<div>
<input type="checkbox" name="agree" value="true" id="agree">
<label for="agree">회원가입 약관에 동의합니다.</label>
</div>
<button type="submit">가입하기</button>
</form>
</body>
</html>

문서에서 파일 그대로 넣어도 똑같이 동작한다
디자이너가 건내준 파일 그 자체를 로드할 수 있다!
👩🏫태그 알고가기
dl, dt, dd
dl - Definition List : 정의 목록 (용어 - 정의 쌍)
dt - Definition Term : 용어
dd - Definition Description : 용어에 대한 정의
타임리프는 기존 디자인 파일 그대로 데이터와 틀을 유지 하면서 번역기술을 사용할 수 있다.

기존값 그대로 유지하면서 태그에 속성 추가한다. 서버가 번역할때 이 값(th:text)으로 교체함
th가 붙은 속성 -> 번역이 되는 속성임

서버측에서는 th 태그를 사용해서 동적으로 내용을 변경할 수 있다. th 속성을 기반으로 데이터를 주입하거나 조건에 따라 html 내용을 동적으로 생성함
클라이언트 측에서는 th속성이 포함된 원본 html을 그대로 볼 수 있다. 그렇기 때문에 디자이너와 개발자가 같은 파일을 사용하더라도 문제없이 작업 할 수 있다.
참고) jsp파일은 서버에서 최초 요청 시 서블릿으로 컴파일됩니다. 이후에는 이 컴파일된 서블릿이 클라이언트의 요청을 처리합니다. JSP 파일이 수정되면, 서버는 이를 감지하여 다시 컴파일한다.
-> 서버 측에서 자바 코드로 변환되어 실행되기 때문에, 별도의 캐시 설정 없이도 변경 사항이 즉시 반영
${식...}
#{메세지 코드}
👩💻참고)
fmt:setBundle
<fmt:message key="메세지 코드">
@{링크}
👩💻참고)
<c:url value="...." />
th:object="${객체}"
*{속성명}
th:text="*{속성명}" //${객체.속성명}
<div th:object="${member}">
<span th:text="*{name}">name</span>
</div>
th:block태그
-> 번역되면 삭제
-> 기능만 필요한 경우
-> 출력되지 않고 값만 유지

값만 유지하고 번역되고 나면 제거됨
🔸 회원정보 출력 예시
MemberController
예시데이터 작성

🔹변수식
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!--번역하기 위한 시그니처 네임스페이스 입력해야함-->
<body>
<h1>회원정보</h1>
<dl>
<dt>이메일</dt>
<dd th:text="${member.email}"></dd>
<!--타임리프는 태그 내부 작성이 아닌 속성으로 입력해야함 내용이 있어도 번역될때 th에 지정한 값으로 대체가 된다.-->
</dl>
<dl>
<dt>회원명</dt>
<dd th:text="${member.userName}"></dd>
</dl>
<dl>
<dt>가입일시</dt>
<dd th:text="${member.regDt}"></dd>
</dl>
</body>
</html>

el식과 동일하게 간단한 연산과 간단한 메서드 호출 가능
🔹선택변수식 - 더 짧게 쓸 수 있는 문법
<body>
<h1>회원정보</h1>
<div th:object="${member}">
<dl>
<dt>이메일</dt>
<!--<dd th:text="${member.email}"></dd>-->
<dd th:text="*{email}"></dd>
</dl>
<dl>
<dt>회원명</dt>
<!-- <dd th:text="${member.userName}"></dd>-->
<dd th:text="*{userName}"></dd>
</dl>
<dl>
<dt>가입일시</dt>
<!-- <dd th:text="${member.regDt}"></dd>-->
<dd th:text="*{regDt}"></dd>
</dl>
</div>
</body>
🔹 메시지식

<body>
<h1>회원정보</h1>
<div th:object="${member}">
<dl>
<dt th:text="#{이메일}">이메일</dt>
<dd th:text="*{email}"></dd>
</dl>
<dl>
<dt th:text="#{회원명}">회원명</dt>
<dd th:text="*{userName}"></dd>
</dl>
<dl>
<dt th:text="#{가입일시}">가입일시</dt>
<dd th:text="*{regDt}"></dd>
</dl>
</div>
</body>

🔹 링크식

같은 속성이 있으면 th 값을 # 대신 교체해줌
href="#" 기존속성 제거해줘도 문제 없다.



th block 안에 있지 않아도 동작할 수 있다 대신 ${}로 member.email 이 형식으로 해줘야함
🔽



앞에 이름으로 위치 찾고 뒤에 *{}로 값 교체
두개 교체도 가능함


#locale
context
#session - jakarta.servlet.http.HttpSession
#request - jakarta.servlet.http.HttpServletRequest
#response - jakarta.servlet.http.HttpServletResponse
${@빈이름.메서드명()}
*{@빈이름.메서드명()}

🔼문자열 결합 Strings.concat


숫자에 대한 형식화


순서대로 범위 내에서 만들때 사용

증감단위는 세번째 있는 매개변수의 단위


🔼제일 많이 쓰는 형태 식객체로 날짜 형식화!

-> 내장 식객체에 없는 기능? 스프링 빈으로 생성
${@빈이름.메서드명(...)}





th:utext - HTML 태그도 해석 O

HTML 태그는 해석되지 않아서 문자 그대로 출력한다.
🐣 th:utext 로 해야함!!
🔽


[[${.. }]] : 태그 안쪽에서도 출력 / 문자열만 인식, HTML 태그는 해석 X👩🏫태그 알고가기
<table> : 테이블 생성을 위한 컨테이너 태그
<thead> : 테이블 머리글부분 정의( 테이블 헤더 행)
<tbody> : 테이블 본문 정의 ( 실제 데이터 행 포함)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<table border="1">
<thead>
<tr>
<th>이메일</th>
<th>회원명</th>
<th>가입일시</th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td th:text="${item.email}"></td>
<td th:text="${item.userName}"></td>
<td th:text="${item.regDt}"></td>
</tr>
</tbody>
</table>
</body>
</html>


<c:forEach ... varStatus="status">
📌순번에 관련된 객체
<tbody>
<tr th:each="item, status : ${items}" th:object="${item}">
<td th:text="*{email}"></td>
<td th:text="*{userName}"></td>
<td th:text="*{regDt}"></td>
</tr>
</tbody>


🔽 상태값 활용 🔽


status는 단지 변수명이기 때문에 굳이 status로 안쓰고 s로 바꿔서 짧게 써도 된다.
status는 *{..} 불가..
th:if : 조건식
th:if="${....}"


th:unless="${...}" : 조건식이 false -> 노출, true -> 노출 X
조건식이 참일때 노출
<th:block th:if="${s.even}">짝수</th:block>
조건식이 거짓일때 노출
<th:block th:unless="${s.even}">홀수</th:block>
true, false -> 상수로 바로 인식

노출 여부 결정 가능

th:href
th:src
th:action
Global Error 출력 용도
Errors::reject("에러코드")
👩🏫 참고) <form:errors path=".."/> 와 동일한 역할
#이 붙어있는 경우 -> 식 객체(내장객체)
클래스를 조건에 따라 추가 제거하는 문법
th:classappend="${조건식 ? '참일때 추가될 클래스명':'거짓일때 추가될 클래스명'}"
th:classappend= ${조건식} ? "참일때":"거짓일때"


th:field="*{속성명}
join.html


...

requestJoin은 선택 변수 형태로..
value 속성 쓰지말고 th:field 사용하자!
🔸 회원가입 form 에러 내용 추가


에러메시지가 여러개일 수 있기 때문에 each 사용

🔸로그인 폼
<html xmlns:th="http://www.thymeleaf.org">
<body>
<form method="post" th:action="@{/member/login}" autocomplete="off"
th:object="${requestLogin}">
<dl>
<dt th:text="#{이메일}"></dt>
<dd>
<input type="text" name="email" th:field="*{email}">
<div th:each="err : ${#fields.errors('email')}" th:text="${err}"></div>
</dd>
</dl>
<dl>
<dt th:text="#{비밀번호}"></dt>
<dd>
<input type="password" name="password" th:field="*{password}">
<div th:each="err : ${#fields.errors('password')}"
th:text="${err}"></div>
</dd>
</dl>
<div>
<input type="checkbox" name="saveEmail" value="true" id="saveEmail"
th:field="*{saveEmail}">
<label for="saveEmail" th:text="#{이메일_기억하기}"></label>
</div>
<!-- <div th:each="err : ${#fields.errors('global')}" th:text="${err}"></div>-->
<div th:each="err : ${#fields.globalErrors()}" th:text="${err}"></div>
<button type="submit" th:text="#{로그인}"></button>
</form>
</body>
</html>


th:replace : 템플릿 파일 치환
th:fragment



templates2 - main - index.html
templates2가 기준 경로임

header의 common쪽으로 내용 치환

footer의 common쪽으로 내용 치환
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<header th:replace="~{outlines/header::common}"></header>
<main>
<h1>
내용..!!
</h1>
</main>
<footer th:replace="~{outlines/footer::common}"></footer>
</body>
</html>


📌공통기능 빼고 출력되는 내용만 치환될 수 있게끔 레이아웃 기능 구성해주자

🔼공통 레이아웃 구성할 파일
공통 레이아웃

바뀌는 내용 부분은 layout이 치환해준다
index.html


동일한 이름으로 하게되면 해당 영역을 찾아서 치환하게 된다.
index.html의 main layout 부분 내용이 main.html의 main layout 태그 안에 내용으로 치환됨


공통 css적용



<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<!--네임스페이스를 통해서 별도로 번역-->
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}">
<!--th가 있어야 링크식 형태로 번역됨-->
<th:block th:if="addCss != null"}>
<link rel="stylesheet" type="text/css" th:each="cssFile: ${addCss}"
th:href="@{/css/{file}.css(file=${cssFile})}">
</th:block>
<script th:src="@{/js/common.js}"></script>
<th:block th:if="${addScript != null}">
<script th:each="jsFile : ${addScript}"
th:src="@{/js/{file}.js(file=${jsFile})}"></script>
</th:block>
</head>
<body><!--헤더 푸터 고정-->
<header th:replace="~{outlines/header::common}"></header>
<!--내용 교체는 xmlns:layout이 해줌-->
<main layout:fragment="content"></main>
<footer th:replace="~{outlines/footer::common}"></footer>
</body>
</html>
MemberController

레이아웃 추가

body쪽 태그 지우고 main 태그로 교체



📢 메인페이지는 Model로 속성을 추가할 addCss, addScript를 사용할 수 없다. 모델 주입하려면 컨트롤러 메서드에 주입해야해야하는데(model.addAttrubute) 메인페이지는 컨트롤러 없이 만든거라 사용할 수 없음! (주소에 바로 템플릿 뷰 연동함)
addCss, addScript를 정의하지 못하는 상황을 대비해서 코드 추가
명칭을 가지고 내부 내용 치환



컨트롤러 없이 사용되는 뷰의 경우에는 내용 치환영역을 더 잡아주면 된다.


iframe -> 페이지 이동 없이 한 페이지 내에서 처리할때 사용
레이아웃쪽에 추가하기
main.html
