본문 바로가기

Spring

thymeleaf 사용법 (thymeleaf layout 으로 기본 골격 만들기)

spring boot 에 친화적이라 하여 thymeleaf 를 사용했던 경험을 정리해보고자 한다.

 

thymeleaf 란?

서버사이드렌더링 방식의 템플릿 엔진 중의 하나로 JSP 역시 동일한 SSR 방식이다.

서버사이드렌더링은 클라이언트에서 요청 시 서버에서 사용자에게 표출할 페이지를 완전히 구성하여 전체 페이지를 렌더링하는 것을 뜻하며 페이지를 이동할 때마다 요청이 이루어진다. 

thymeleaf 의 가장 큰 특징은 순수 HTML로 유지되는 Natural Template 이라는 점이다. JSP의 경우 전용 문법이 있기 때문에 화면을 보기 위해서 서버가 필요하지만 thymeleaf 는 HTML 형태로 되어 있어 서버의 도움 없이도 프로토 타입의 화면을 볼 수 있다. 웹 브라우저가 th 태그 같은 속성은 무시하고 HTML로 구성하여 띄워주기 때문이다.

 

thymeleaf layout 이란?

보통 페이지를 구성할 때 공통 영역으로 분류되는 Header, Footer, Navigation, Sidebar 에 대한 코드의 재사용이 가능할 수 있도록 layout을 구성해주는 라이브러리이다.

 

 

 

다음은 spring boot에서 thymeleaf 를 사용하기 위한 설정이다. maven 프로젝트 기준이다.

 

1. 의존성 추가

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

 

Maven Repository에서 버전별로 선택할 수 있는데 spring boot starter를 사용하면 버전 관리를 알아서 해주기 때문에 편하다. 

더불어 layout 을 사용할 것이기 때문에 이에 해당하는 dependency도 추가해준다.

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

 

 

 

2. 기본경로 확인 및 생성

별로도 설정하지 않으면 기본경로는 src/main/resources/templates 이다. 디렉토리를 생성해준다.

만약 기본경로를 변경하고 싶다면 application.properties 파일에 아래 설정을 추가해준다.

spring.thymeleaf.prefix = classpath:/other/

classpath 는 src/main/resources 를 가리킨다.

이후 controller 에서 String으로 .html 파일의 파일명만 return 해주면 앞에 경로가 붙어서 해당 페이지로 이동하게 된다.

ModelAndView를 사용하여 setViewName에 파일명을 파라미터로 준 후 ModelAndView 객체를 return해도 동일하게 적용된다.

 

@GetMapping("/list")
public ModelAndView list(ModelAndView mav) {
    /* src/main/resources/templates/pages 디렉토리의 list.html로 이동 */
    mav.setViewName("/pages/list");
    return mav;
}

 

 

 

3. 폴더 구성 및 html 파일 생성

기본경로 아래에 layout 구성을 위한 폴더를 생성한다.

  • fragments : Header, Footer, Navigation, Sidebar와 같은 공통 영역에 들어갈 내용으로 구성된 파일을 넣는 폴더
  • layouts : fragments 를 하나로 묶어주는 기본 파일을 넣는 폴더
  • pages : 페이지마다 다르게 들어갈 content 파일을 넣는 폴더

pages 폴더를 제외하고 일단 공통 부분만 생성해두었다.

 

 

 

fragments

<!DOCTYPE html>
<html lang="ko" xmlns:th="httpl://www.thymeleaf.org">

<footer th:fragment="footerFragment" class="footer">
    <ul class="list-inline">
        <li class="list-inline-item">
            <a class="text-muted" href="#">Support</a>
        </li>
        <li class="list-inline-item">
            <a class="text-muted" href="#">Help Center</a>
        </li>
        <li class="list-inline-item">
            <a class="text-muted" href="#">Privacy</a>
        </li>
        <li class="list-inline-item">
            <a class="text-muted" href="#">Terms of Service</a>
        </li>
    </ul> 
</footer>

</html>

footer.html

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="httpl://www.thymeleaf.org">

<nav th:fragment="navFragment" class="navbar">
    <div class="navbar-collapse">
        <ul class="navbar-nav navbar-align">
            <li class="nav-item dropdown">
                <a class="nav-icon dropdown-toggle" href="#" data-toggle="dropdown">
                    <i class="align-middle" data-feather="settings"></i>
                </a>
                <a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown">
                    <span class="text-dark" th:text="${session.user}"></span>
                </a>
                <div class="dropdown-menu dropdown-menu-right">
                    <a class="dropdown-item" href="/logout">Sign out</a>
                </div>
            </li>
        </ul>
    </div>
</nav>

</html>

nav.html

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="httpl://www.thymeleaf.org">

<nav th:fragment="sideFragment" id="sidebar" class="sidebar">
    <div class="sidebar-content js-simplebar">
        <a class="sidebar-brand" href="/">            
            <span class="align-middle mr-3">system</span>
        </a>
        <ul class="sidebar-nav">
            <li class="sidebar-header separator">
                System Manager
            </li>
            <li class="sidebar-item" th:classappend="${menu}=='a'?'active'">
                <a href="/list_a" class="sidebar-link">
                    <i class="align-middle" data-feather="list"></i> 
                    <span class="align-middle">A List</span>
                </a>
            </li>
            <li class="sidebar-item" th:classappend="${menu}=='b'?'active'">
                <a href="/list_b" class="sidebar-link">
                    <i class="align-middle" data-feather="folder"></i> 
                    <span class="align-middle">B List</span>
                </a>
            </li>
        </ul>
    </div>
</nav>

</html>

side.html

 

 

세 개의 html에 공통으로 들어가는 부분은 다음과 같다.

<html lang="ko" xmlns:th="httpl://www.thymeleaf.org">

해당 html이 thymeleaf를 사용한다고 인식할 수 있도록 상단에 태그를 추가해준다.

<div th:fragment="fragment명">
</div>

해당 부분을 fragment로 사용하겠다는 태그를 추가해준다.

 

 

 

layouts

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

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <title th:text="${title}"></title>

    <link rel="shortcut icon" th:href="@{/img/favicon.ico}">
    <link href="/css/light.css" rel="stylesheet">
</head>

<body data-theme="default" data-layout="fluid" data-sidebar-position="left" data-sidebar-behavior="sticky">
    <div class="wrapper">
        <!-- Navigation -->
        <nav th:replace="~{fragments/side :: sideFragment}"></nav>
        
        <div class="main">
            <!-- Search -->
            <nav th:replace="~{fragments/nav :: navFragment}"></nav>
            <!-- Main Content -->
            <main layout:fragment="content"></main>
            <!-- Footer -->
            <footer th:replace="~{fragments/footer :: footerFragment}"></footer>
        </div>
    </div>
</body>

</html>

default.html

 

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

마찬가지로 thymeleaf 사용 선언을 해주면서 layout 도 함께 추가한다.

<div th:replace="~{파일경로/파일명 :: fragment명}"></div>

그리고 body 부분에 각 fragment를 배치해준다. 위의 표현식은 파일 내부의 fragment 를 가져와서 replace 한다는 의미다.

 

 

여기까지 구성하면 기본 골격이 완성된 상태이다.

 

<!DOCTYPE html>
<html xmlns:th="httpl://www.thymeleaf.org" 
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/default}">

<head>
    <style type="text/css">
        /* CSS */
    </style>
</head>

<body>
    <main layout:fragment="content" class="content">
        <h1>A LIST</h1>
    </main>

    <script th:inline="javascript">
        /* javascript */
    </script>
</body>

</html>

/pages/list.html

 

<html xmlns:th="httpl://www.thymeleaf.org" 
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/default}">

content 에 해당하는 html 파일에는 layout:decorate 를 추가하여 구성한 레이아웃을 불러와서 사용한다.

그리고 본문에 default.html 에 작성했던 layout:fragment="content" 를 태그에 추가해주면 해당 영역에 표출되는 것을 확인할 수 있다.

 

thymeleaf 에서 사용하는 표현식을 좀 더 공부하면 더 자유롭고 편리하게 코드를 작성할 수 있을 것 같다!