반응형
05-14 05:47
Today
Total
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
관리 메뉴

개발하는 고라니

[Spring Boot] Thymeleaf 본문

Framework/Spring Boot

[Spring Boot] Thymeleaf

조용한고라니 2021. 6. 12. 17:04
반응형

Java로 웹 개발을 접해보았다면 Servlet/JSP로 입문을 했을 가능성이 높고, Spring과 MyBatis, JSP로 그 다음을 접해보았을 것이라 기대된다. JSP는 Java Server Page의 약자로 그간 많이 사용되어왔고, 현재도 많이 쓰이는 뷰 엔진(View Engine)이다. 하지만 최근 스프링 부트로 넘어오면서 JSP 대신 Thymeleaf라는 비교적 새로운 뷰 템플릿 엔진이 등장하였고 많은 사람들이 이것을 사용한다. 물론 Thymeleaf말고도 Mustache, Groovy, FreeMarker ... 등 많은 뷰 엔진이 있다.

 

이번 포스팅에서 알아볼 Thymeleaf은 Spring Framework(특히 Spring MVC)에 통합시킬 수 있는 뷰 엔진이다.

Thymeleaf

출처: https://www.thymeleaf.org/

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

Thymeleaf는 웹뿐만 아니라 다른 환경을 위한 최신의 서버-사이드 자바 Template Engine이며, HTML, CSS, XM, JS 및 Text까지 수용(이는 다른 템플릿 엔진과 동일)한다.

 

타임리프의 주 목표는 유지관리가 쉬운 템플릿 생성 방법을 제공하는 것이며, 실제로 템플릿에 영향을 주지 않는(HTML의 구조를 깨지 않는, 기존 HTML 코드를 변경하지 않고 덧붙이는 코드) 방식을 사용한다. 즉, Natural Templates 개념을 기반으로 한다. 이를 통해 디자인 팀과 개발 팀간 갈등, 격차 해소가 기대된다.

<html xmlns:th="www.thymeleaf.org">
    <head>
        ...
    </head>
    <body>
        <div>${data}</div> (x)
        
        <div th:text="${data}">Origin text</div> (o)
    </body>
</html>

Dialect - Standard Dialect

기본적으로 Thymeleaf의 핵심 라이브러리는 Standard Dialect라는 dialect를 제공하며 대부분 사용자에게 만족된다.

Dialect는 프로세서가 없고 전체적으로 다른 종류의 아티팩드들로 구성될 수 있지만, 프로세서는 확실히 가장 일반적인 사용 사례이다.

공식 Thymeleaf-spring 3 및 Thymeleaf-spring 4 통합 패키지는 모두 "SpringStandard Dialect"라는 dialect를 정의한다. 이 dialect는 Standard Dialect와 거의 동일하나, Spring Framework의 일부 기능을 더 잘 사용하기 위해 약간의 조정이 있다. 예를 들어, OGNL 대신 Spring Expression Language 또는 Spring EL을 사용한다.

 

Standard Dialect의 대부분으 프로세서는 'Attribute Processor'이다. 이를 통해 브라우저는 단순히 추가 속성을 무시하기 때문에 처리되기 전에도 HTML 템플릿 파일을 올바르게 표시할 수 있다.

 

예를 들어, Tag Library를 사용하는 JSP에는 다음과 같이 브라우저에서 직접 표시할 수 없는 코드 조각이 포함될 수 있다.  이하는 JSP와의 비교이다.

# JSP

<input type="text" name="userName" value="${user.name}">

# Thymeleaf Standard Dialect

<input type="text" name="userName" value="Gorany" th:value="${user.name}">

이렇게 하면 디자이너와 개발자가 동일한 템플릿 파일에서 작업하고 정적인 HTML을 동적인 HTML로 변환하는데 필요한 노력을 줄일 수 있다. 이를 수행하는 기능이 "Natural Templating"이다.

 

Template Engine의 구성

Spring을 사용했었다면 XML또는 Java Config로 ViewResolver를 다뤄보았던 경험이 있을 것이다. 나도 다뤄보았지만 그 때는 뭔지 몰라도 대충 넘겼었다. 근데 스프링 부트로 넘어와서 Thymeleaf를 쓸 때, ViewResolver를 설정한 적이 없는데 resources/template 밑에 HTML 파일을 넣어두면 Controller에서 포워딩 해주는 것을 경험했다. 이를 토대로 스프링 부트 내부에서 자동으로 ViewResolver를 생성해주었을 것이리라 믿어왔다. 

 

그 내부를 Thymeleaf 공식 사이트에서 일부 공개하고 있으나, 우리는 세세하게 알 필요는 없다. 일부만 슬쩍 보고 아 그렇구나 하고 넘어가면 될 것 같다.

 

Template Engine을 멤버로 갖는 어플리케이션이 초기화(생성)될 때, ServletContextTemplateResolver 객체가 생성되며, 이 templateResolver에 prefix(접두사)와 suffix(접미사)를 설정하고, Template Engine 객체를 생성해 엔진에 템플릿 리졸버를 꽂아주는 식이다.

 

코드를 들여다보면 대충 다음과 같다. ( WEB-INF == resources )

public class GTVGApplication {
  
    
    ...
    private final TemplateEngine templateEngine;
    ...
    
    
    public GTVGApplication(final ServletContext servletContext) {

        super();

        ServletContextTemplateResolver templateResolver = 
                new ServletContextTemplateResolver(servletContext);
        
        // HTML is the default mode, but we set it anyway for better understanding of code
        templateResolver.setTemplateMode(TemplateMode.HTML);
        // This will convert "home" to "/WEB-INF/templates/home.html"
        templateResolver.setPrefix("/WEB-INF/templates/");
        templateResolver.setSuffix(".html");
        // Template cache TTL=1h. If not set, entries would be cached until expelled
        templateResolver.setCacheTTLMs(Long.valueOf(3600000L));
        
        // Cache is set to true by default. Set to false if you want templates to
        // be automatically updated when modified.
        templateResolver.setCacheable(true);
        
        this.templateEngine = new TemplateEngine();
        this.templateEngine.setTemplateResolver(templateResolver);
        
        ...

    }

}

Using Text

Thymeleaf에서 Text를 사용하는 방법은 조금 특이할 수 있다. 위에서 언급했듯, Standard Dialect는 속성에 꽂아넣는 식으로 동작하기 때문이다. 이것의 장점은 .html파일이 HTML이 아닌 태그를 포함하지 않기 때문에 모든 브라우저에서 올바르게 표시될 수 있다.(브라우저는 이해하지 못하는 모든 속성을 무시한다. 예를 들어 th:text)

 

th:text는 분명 HTML의 속성이 아니다. 그러면 이를 어떻게 쓰는걸까? 우리는 html파일 위에 namespace를 꽂는다.

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

표준 표현식 구문

# 간단한 표현

  • 변수(Variable) 표현식                     : ${...}
  • 선택 변수(Selection Variable) 표현식 : *{...}
  • 메세지(Message) 표현                    : #{...}
  • 링크 URL(Link URL) 표현식              : @{...}
  • 단편(Fragment) 표현식                   : ~{...}

 

# 리터럴

  • 텍스트 리터럴 : 'one text', 'Another one!', ...
  • 숫자 리터럴 : 0, 34, 3.0, 12.3, ...
  • 부울 리터럴 : true,false
  • 널 리터럴 : null
  • 리터럴 토큰 : one, sometext, main, ...

 

# 텍스트 작업

  • 문자열 연결 : +
  • 리터럴 대체 : |The name is ${name}|
    • '|' 을 사용 (shift + \)

 

# 산술 연산

  • 이진 연산자 : +, -, *, /,%
  • 빼기 기호 (단항 연산자) : -

 

# Bool 연산

  • 이항 연산자 : and,or
  • 부울 부정 (단항 연산자) : !,not

 

# 비교 및 평등

  • 비교기 : >, <, >=, <=( gt, lt, ge, le)
  • 같음 연산자 : ==, !=( eq, ne)

 

# 조건부 연산자

  • if-then : (if) ? (then)
  • if-then-else : (if) ? (then) : (else)
  • Default : (value) ?: (defaultvalue)

위의 모든 기능을 결합 및 중첩할 수 있다.

'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

Message Expression - #{...}

외부 파일을 의미한다. '.properties'라는 파일 자체가 Properties 컬렉션과 연관되있다. 이는 Map의 형태로써 Key와 Value의 형태로 구성된다. XML의 형태 또는 .properties 형태로 save와 load를 할 수 있다. 즉, xml / properties 파일에서 값을 읽어 사용할 수 있다. (단, application.properties에서 설정을 해줘야됨)

 

※ Summary 

${...}는 Controller로부터 받은 model에서 값을 꺼내 사용하는 것.

#{...}는 properties파일에서 메세지(값)을 꺼내 사용하는 것.

 

그럼 properties 파일을 생성해서 메세지를 저장하고 그것을 불러와보자. 경로는 /resources/messages/message.properties로 파일을 만들었다.

#message.properties

title=안녕하세요. ~ message
abc.title=<h1>제목태그</h1>

 

다음은 application.properties에 이 파일에 대해서 설정을 한다.

#application.properties..

#message.properties setting
spring.messages.basename=messages/message
spring.messages.encoding=UTF-8

사용 ->      <h1 th:text="#{title}">example</h1>

 

변수

${...} 표현식은 컨텍스트에 포함된 변수 맵에서 실행되는 OGNL(Object-Graph Navigation Language) 표현식이다.

컨텍스트에 set으로 변수를 담아, get으로 꺼내서 사용한다. 자바에서 exam.getName()으로 썼다면, JSP의 EL에서 ${exam.name} 처럼 꺼내쓰던 것을 생각하면 될 것 같다.

 

※ 식 기본 개체

Context 변수에서 OGNL 표현식을 평가할 때 일부 객체는 더 높은 유연성/정확성을 위해 표현식에 사용할 수 있다. 이는 마치 JSP에서 ${request.xxx}, ${session.xxx}와 유사한 듯 하다.

  • #ctx: 컨텍스트 객체.
  • #vars: 컨텍스트 변수.
  • #locale: 컨텍스트 로케일.
  • #request: (웹 컨텍스트에서만) HttpServletRequest객체.
  • #response: (웹 컨텍스트에서만) HttpServletResponse객체.
  • #session: (웹 컨텍스트에서만) HttpSession객체.
  • #servletContext: (웹 컨텍스트에서만) ServletContext객체.
Established locale country: <span th:text="${#locale.country}">US</span>.

 

※ 식 유틸리티 개체

  • #execInfo: 처리중인 템플릿에 대한 정보입니다.
  • #messages: # {…} 구문을 사용하여 얻을 수있는 것과 동일한 방식으로 변수 표현식 내에서 외부화 된 메시지를 얻는 방법.
  • #uris: URL / URI의 일부를 이스케이프하는 방법
  • #conversions: 구성된 변환 서비스 를 실행하는 방법 (있는 경우).
  • #dates: java.util.Date개체에 대한 방법 : 서식 지정, 구성 요소 추출 등
  • #calendars:과 유사 #dates하지만 java.util.Calendar객체에 사용됩니다.
  • #numbers: 숫자 개체 서식 지정 방법.
  • #strings: String객체에 대한 메소드 : contains, startsWith, prepending / appending 등
  • #objects: 일반적으로 개체에 대한 메서드입니다.
  • #bools: 부울 평가 방법.
  • #arrays: 배열 방법.
  • #lists: 목록 방법.
  • #sets: 세트에 대한 방법.
  • #maps:지도 방법.
  • #aggregates: 배열 또는 컬렉션에 집계를 만드는 방법입니다.
  • #ids: 반복 될 수있는 id 속성을 처리하기위한 메소드 (예 : 반복의 결과).
날짜 바꾸기 예시 ->

- Date : #calendars 사용
<td th:text="*{#calendars.format(regDate, 'yyyy-MM-dd')}"> 2019-08-18</td>
<td th:text="${#calendars.format(notice.regDate, 'yyyy년MM월dd일')}"> 2019-08-18</td>
<td th:text="${#calendars.format(today, 'yyyy/MM/dd')}"> 2019-08-18</td>

- LocalDateTime / LocalDate : #temporals 사용
<span th:text="*{#temporals.format(updatedAt, 'yyyy/MM/dd')}"></span>

선택 변수 표현식 - *{...}

${...}뿐 아니라 *{...}도 변수 표현식으로 쓸 수 있다. 하지만 *{...}는 중요한 차이점이 존재한다. 그것인 전체 Context가 아닌 선택한 개체에 대한 표현식을 평가한다. 즉, 선택된 개체가 없는 한 $ 및 * 구문은 동일하다.

 

선택한 개체는 또 무엇인가..? 하니 th:object속성을 사용하는 표현식의 결과이다. 다음 form 태그에서 예를 들어보자.

<!-- 회원가입 폼에서 member라는 개체를 선택한 상황을 가정 -->
<form th:object="${member}" method="post" action="/signup">

    <label>Nickname</label>
    <input type="text" th:value="*{nickname}" name="nickname">
    
    <label>Password</label>
    <input type="text" th:value="*{password} name="password">
</form>

이는 다음과 같이 쓸 수 있다.

<!-- 회원가입 폼에서 member라는 개체를 선택한 상황을 가정 -->
<form method="post" action="/signup">

    <label>Nickname</label>
    <input type="text" th:value="${member.nickname}" name="nickname">
    
    <label>Password</label>
    <input type="text" th:value="${member.password} name="password">
</form>

물론 혼용도 가능하다.

<!-- 회원가입 폼에서 member라는 개체를 선택한 상황을 가정 -->
<form th:object="${member}" method="post" action="/signup">

    <label>Nickname</label>
    <input type="text" th:value="*{nickname}" name="nickname">
    
    <label>Password</label>
    <input type="text" th:value="${member.password} name="password">
</form>

Link URL

Thymeleaf의 URL 표현식은 '@{...}'처럼 쓰며, 설명하기 보다 예시를 몇 개 드는 것이 더 효과적일 것 같다.

 

# Examples

<a href="www.naver.com" th:href="@{www.naver.com">Naver</a>
<!-- www.naver.com -->

<a th:href="/board/list(field=${field}, query=${query})}">Search</a>
<!-- /board/list?field=title&query=abc -->

<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>
<!-- /order/3/details?orderId=3 -->

URL의 QueryString

/board/detail?id=123과 같이 표기하려면 어떻게 해야할까? --> ${param.id}

<a th:href="@{/board/detail(id=${id})}">상세보기</a>
<!-- /board/detail?id=123 -->

<a th:href="@{/board/detail(id=${id}, test=${test})}">상세보기</a>
<!-- /board/detail?id=123&test=test -->

<!-- Rest API -->
<a th:href="@{/order/{orderId}/details(orderId=${order.id})}">주문 조회</a>
<!-- /order/3/detail -->

리터럴

비교적 간단하여 예시가 필요하다면 레퍼런스를 참고 요망.

<, >, == 등

>, <, >=, <=, ==, !=

<div th:if="${prodStat.count} &gt; 1">
<span th:text="'Execution mode is ' + ( (${execMode} == 'dev')? 'Development' : 'Production')">

XML은 < 및 >를 속성 값에 사용할 수 없으므로 &lt; , &gt;로 대체한다.

조건식

th:if를 사용하거나 상항연산자를 사용할 수 있다.

 

삼항연산자에서는 else 부분을 생략할 수 있다.

<tr th:class="${row.even}? 'even' : 'odd'">
  ...
</tr>

<tr th:class="${row.even}? (${row.first}? 'first' : 'even') : 'odd'">
  ...
</tr>

<tr th:class="${row.even}? 'alt'">
  ...
</tr>

기본 표현식 - Elvis Operator

Groovy 같은 일부 언어에 존재하는 Elvis 연산자와 동일하며, 두 개의 표현식을 지정할 수 있다. 첫번째 표현식이 null이 아니라면 첫번째 표현식을 사용하고, 반대로 null이라면 두번째 표현식을 사용한다. 마치 간소화된 삼항 연산자와 유사하다.

<div th:object="${session.user}">
  ...
  <p>Age: <span th:text="*{age}?: '(no age specified)'">27</span>.</p>
</div>

위 아래 표현이 동일

<p>Age: <span th:text="*{age != null}? *{age} : '(no age specified)'">27</span>.</p>
<!------------------------------------------------------------------------->

<p>
  Name: 
  <span th:text="*{firstName}?: (*{admin}? 'Admin' : #{default.username})">Sebastian</span>
</p>

 

 

 

 

Local Variables( 지역 변수 )

JSTL의 <c:set /> 같이 변수를 만들어 사용하고 싶을 땐 다음과 같이 사용한다. 단, 해당 태그가 끝나기 전 범위 내에서만 사용이 가능하다.

<th:block th:with="temp = ${data}, example = ${foo}">

    <span th:text="${temp}">text</span>

</th:block>

<div th:text="${example}"></div> (X)

th:text / th:utext

  • th:text -> Model에 HTML 태그가 담겨있어도 '문자열'로써 출력
  • th:utext -> Model에 HTML 태그가 담겨있으면 HTML의 태그로써 출력 (u : unescaped)
<!--
model.addAttribute("data", "Hello World~~ <b>How are you?</b>");
-->

<div th:text="${data}"></div>
<!-- Hello World~~ <b>How are you?</b> -->

<div th:utext="${data}"></div>
<!-- Hello World~~ How are you? --> (How are you 부분 bold)

반복 - each

반복을 사용하는 것 또한 크게 어렵지 않다. 다음은 Notice객체가 담긴 List를 모델에 담았다고 가정한 상황에서 사용해보자.

<!-- model.addAttribute("noticeList", list); -->

<tr th:each="notice : ${noticeList}" th:object="${notice}">
    <td th:text="*{id}">8</td>
    <td class="title indent text-align-left"><a th:text="*{title}" th:href="@{detail(id=*{id})}" href="detail.html">스프링 8강까지의 예제 코드</a></td>
    <td th:text="*{writerId}">newlec</td>
    <td th:text="*{regDate}">
        2019-08-18
    </td>
    <td th:text="*{hit}">146</td>
    <td><input type="checkbox" th:name="open"></td>
    <td><input type="checkbox" th:name="del"></td>
</tr>

th:each="[변수명] : ${모델에 담긴 키}"로 받아서, ${[변수명].title} 이런식으로 쓰면 된다. 나는 th:object로 한번 더 선언해서 [변수명]. 하는 과정을 생략했다.

Template Layout

JSP에서 뷰의 공통영역을 넣을 떄 <%@ include="header.jsp" %> 처럼 인클루드 하거나, tiles 라이브러리를 사용해서 레이아웃을 구성했다. Thymeleaf에서는 어떻게 하는지 간단히 살펴보자.

  • th:insert : 파일(태그)의 내용이 삽입
  • th:replace : 파일(태그)의 내용으로 대체
  • th:include : 파일(태그)의 내용을 포함

 

 # Example (insert / replace / include) 

<body>

  ...

  <div th:insert="footer :: copy"></div>

  <div th:replace="footer :: copy"></div>

  <div th:include="footer :: copy"></div>
  
</body>

<!-- --------------------------------------------------------- -->

<body>

  ...

  <div> //insert
    <footer>
      &copy; 2011 The Good Thymes Virtual Grocery
    </footer>
  </div>

  <footer> //replace
    &copy; 2011 The Good Thymes Virtual Grocery
  </footer>

  <div> //include
    &copy; 2011 The Good Thymes Virtual Grocery
  </div>
  
</body>

 

예를 들어 include폴더 밑에 header.html라는 문서를 두었다.

<!-- header.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<header id="header" th:fragment="hd">

    <div class="content-container">
        <!-- ---------------------------<header>--------------------------------------- -->

        <h1 id="logo">
            <a href="/index.html">
                <img src="/images/logo.png" alt="OOO 온라인" />

            </a>
        </h1>

        <section>
            <h1 class="hidden">헤더</h1>

            <nav id="main-menu">
                <h1>메인메뉴</h1>
                <ul>
                    <li><a href="/guide">학습가이드</a></li>

                    <li><a href="/course">강좌선택</a></li>
                    <li><a href="/answeris/index">AnswerIs</a></li>
                </ul>
            </nav>

            <div class="sub-menu">

                <section id="search-form">
                    <h1>강좌검색 폼</h1>
                    <form action="/course">
                        <fieldset>
                            <legend>과정검색필드</legend>
                            <label>과정검색</label>
                            <input type="text" name="q" value="" />
                            <input type="submit" value="검색" />
                        </fieldset>
                    </form>
                </section>

                <nav id="acount-menu">
                    <h1 class="hidden">회원메뉴</h1>
                    <ul>
                        <li><a href="/index.html">HOME</a></li>
                        <li><a href="/member/login.html">로그인</a></li>
                        <li><a href="/member/agree.html">회원가입</a></li>
                    </ul>
                </nav>

                <nav id="member-menu" class="linear-layout">
                    <h1 class="hidden">고객메뉴</h1>
                    <ul class="linear-layout">
                        <li><a href="/member/home"><img src="/images/txt-mypage.png" alt="마이페이지" /></a></li>
                        <li><a href="/notice/list.html"><img src="/images/txt-customer.png" alt="고객센터" /></a></li>
                    </ul>
                </nav>

            </div>
        </section>

    </div>

</header>
</html>

조금 특이한 점은, 완벽한 HTML 구조를 갖는다는 것이다. <html> 태그가 존재한다. 이 태그는   xmlns:th  을 추가해주기 위해서 필요한 것이다.

 

이제 저 header.html을 가져다가 끼워넣는 작업을 해보자.

 ※ ~{...} 

- 단편 조각의 경로(Path)를 의미한다. 위에서 잠깐 언급했듯이, Fragment Expression으로 파일의 경로를 의미하고자 할 때 사용한다. 

- ~{}표시는 생략이 가능하다.

- @{...}랑은 조금 다르다. @{...}는 URL을 의미한다.
<!-- list.html -->

...생략
<body>
	<!-- header 부분 -->
    <th:block th:replace="~{include/header}"></th:block> <!-- 루트를 의미할 때 '/'를 사용하지 않아도 된다. -->
    <!-- <div th:replace="~{include/header}"></div>라고 써도 동일함 -->

	<!-- --------------------------- <visual> --------------------------------------- -->
    
...생략

 

하지만... 파일 자체(header.html)를 끼워넣는 것은 조금 의아하기도 하고, 실제로 HTML 구조가 깨진다. 이를 극복하려면 어떻게 해야할까?

 

A. Fragment를 이용한다.

위의 내용을 토대로 하면, 다음과 같이 수정할 수 있다.

 

<!-- list.html -->

...생략
<body>
	<!-- header 부분 -->
    <th:block th:replace="~{include/header :: #header}"></th:block> <!-- CSS 선택자 사용 -->
    <!-- <div th:replace="~{include/header :: hd}"></div> fragment 이름 사용 -->
    <!-- <div th:replace="include/header :: hd"></div> fragment 이름 사용 -->
    <!-- <div th:replace="include/header :: #header"></div> CSS 선택자 사용 -->

	<!-- --------------------------- <visual> --------------------------------------- -->
    
...생략

 

Template Layout Pro.ver

다음은 레이아웃을 조금 더 난이도 있게 구성해보는 시간을 갖는다. 위의 방법으로도 충분하지만, 모든 파일에서 th:replace="~{include/header :: header}", th:replace="~{include/footer :: footer}" 같은 작업을 해주어야 한다.

 

만약 이를 하나의 파일(이하 layout)이라는 곳에 정의해두고, main 부분만 작성해서 layout에 꽂아넣고 싶다면, 어떻게 해야할까?

 

먼저 방법은, 인자(parameter)를 이용하는 것이다. 코드 먼저 보면 다음과 같다.

<!-- layout.html -->

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

<body th:fragment="body(main)">
<!--header-->
<th:block th:replace="~{include/header :: header}"></th:block>
<!--visual-->
<div id="visual">
    <div class="content-container"></div>
</div>
<!--body-->
<div id="body">
    <div class="content-container clearfix">

        <!--aside-->
        <aside class="aside">
            <h1>ADMIN PAGE</h1>

            <nav class="menu text-menu first margin-top">
                <h1>마이페이지</h1>
                <ul>
                    <li><a href="/admin/index.html">관리자홈</a></li>
                    <li><a href="/teacher/index.html">선생님페이지</a></li>
                    <li><a href="/student/index.html">수강생페이지</a></li>
                </ul>
            </nav>

            <nav class="menu text-menu">
                <h1>알림관리</h1>
                <ul>
                    <li><a href="/admin/board/notice/list.html">공지사항</a></li>
                </ul>
            </nav>

        </aside>
        
        <!--main-->
        <th:block th:replace="${main}"></th:block>
        
    </div>
</div>

<!--footer-->
<th:block th:replace="~{include/footer :: footer}"></th:block>

</body>
</html>

 

<body> 태그를 보면 th:fragment="body(main)" 과 같은 형태를 띄고, 밑에 main 주석 부분에

<th:block th:replace="${main}"></th:block>

과 같이 "변수"를 받아 대체하는 형태를 띈다.

 

이를 사용하려면 main을 작성한 곳에서는 아래와 같이 코드를 작성한다,

<!-- main 이 있는 파일 -->

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

<head>
	...
</head>

<!-- admin/include/layout 파일의 th:fragment="body"로 대체를 해달라 -->
<!-- 그런데 대체 할 때 인자를 넘길건데, 이 파일의 th:fragment="main"을 넘긴다. -->
<!-- layout파일에서 th:fragment="body(var)로 선언하고, 하부에서 th:replace="${var}"를 쓰면 현재 파일의 main이 끼워지게 된다. -->

<th:block th:replace="~{admin/include/layout :: body(~{this :: main})}">
    <main class="main" th:fragment="main">
    
    ...

라이브러리 사용해 레이아웃 설정

먼저 thymeleaf 레이아웃 라이브러리를 추가한다.

        <dependency>
            <groupId>nz.net.ultraq.thymeleaf</groupId>
            <artifactId>thymeleaf-layout-dialect</artifactId>
        </dependency>

그다음 레이아웃으로 쓸 페이지(파일)에 xml namespace를 추가한다.

<!-- layout.html -->

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

 

layout 파일에서 main에 해당하는 조각(fragment)가 필요한 곳에 다음과 같이 선언한다.

        <!--main-->
        <th:block layout:fragment="main"></th:block>
        ...

 

이후 main을 갖고있는 파일(여기선 list.html)에서 다음과 같이 설정한다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="~{/admin/include/layout}">
<!-- layout:decorator는 현재 내 파일에 레이아웃에 끼워넣을 것이 있으니, 그 대상을 지정한다라는 의미 -->

<head>
	...
</head>

    <main class="main" layout:fragment="main">
    ...
    </main>

</html>
# <main layout:fragment="main">

- layout에서 "main"이라는 이름으로 쓰일 조각이라고 선언

# <html layout:decorator="~{/admin/include/layout}">

- 끼워넣을 layout 파일이 해당 경로에 있다고 알려줌

# 각 페이지마다 독립적으로 필요한 CSS, JS, Title등은 레이아웃에 어떻게 추가하나요?

- 끼워넣으려고 하는 파일(예를 들어 list,html)에 기입해두면 저절로 포함된다.

 

# References 

https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

Newlec 선생님 강의

 

 

반응형
Comments