스프링부트 공부기록(20) - 타임리프(Thymeleaf) 반복문, 제어문, 레이아웃 템플릿, 부트스트랩 사용하기
my code archive
article thumbnail
반응형

✅타임리프 실습 이유

  • JSP와 유사하게 ${ } 이용 가능
  • Model에 담긴 객체를 화면에서 JS로 처리하기 편리함.
  • 연산이나 포맷과 관련된 기능을 지원함.
  • .html 파일 생성 시 문제가 없고 별도의 확장자를 이용하지 않음.

🤍Thymeleaf 이용 프로젝트 생성

Spring Boot DevTools / Lombok / Spring Web / Thymeleaf 추가

 

타임리프를 이용하는 프로젝트는 변경 후 만들어진 결과를 보관(캐싱)하지 않도록 설정해두는 것이 편리함.

application.properties 파일에 아래 코드 추가

spring.thymeleaf.cache=false

 

SampleController 클래스 생성

@Controller
@RequestMapping("/sample")
@Log4j2
public class SampleController {

    @GetMapping("/ex1")
    public void ex1(){
        log.info("ex1...........");
    }
 }

 

ex1.html 생성

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 th:text="${'Hello World'}"></h1>
</body>
</html>

 

/sample/ex1 실행

 

SampleDTO 클래스 생성

import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@Builder(toBuilder = true)
public class SampleDTO {

    private Long sno;
    private String first;
    private String last;
    private LocalDateTime regTime;
}

🤍타임리프 기본 사용법 (1) 반복문

 @GetMapping("/ex2")
    public void exModel(Model model){
        List<SampleDTO> list = IntStream.rangeClosed(1,20).asLongStream().mapToObj(i->{
            SampleDTO dto = SampleDTO.builder().sno(i).first("First.."+i).last("Last.."+i).regTime(LocalDateTime.now()).build();
            return dto;
        }).collect(Collectors.toList());
        //SampleDTO 타입의 객체를 20개 추가하고 이를 Model에 담아 전송함.
        model.addAttribute("list",list);
    }

 

Thymeleaf에서 반복문은 th:each라는 속성을 이용함.

th:each = "변수 : ${목록}"

 

ex2.html 파일 생성

  • <li> 태그 내에 dto라는 변수를 만들고 컨트롤러 Model로 전달된 데이터는 ${list}를 이용해서 처리함.
  • [[ ]] : 인라인 표현식으로 별도의 태그 속성으로 지정하지 않고 사용할 때 유용하게 쓸 수 있다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <ul>
        <li th:each="dto:${list}">
            [[${dto}]]
        </li>
    </ul>
</body>
</html>

 

/sample/ex2 실행 결과

반복문의 상태(state) 객체

  • 순번, 인덱스 번호, 홀수&짝수 등을 지정할 수 있음.
  • index는 0부터 시작하고 count는 1부터 시작함.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <ul>
        <li th:each="dto, state :${list}">
            [[${state.index}]] --- [[${dto}]]
        </li>
    </ul>
</body>
</html>

 

실행 결과를 보면 0부터 시작하는 번호가 추가된 것을 확인할 수 있다.

🤍타임리프 기본 사용법 (2) 제어문

타임리프의 제어문 처리는 th:if ~ unless 등을 이용할 수도 있고 삼항연산자 스타일을 사용할 수도 있음.

 

ex) sno의 값이 5의 배수인 것만 출력하고 싶을 때

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <ul>
        <li th:each="dto, state :${list}" th:if="${dto.sno %5 ==0}">
           [[${dto}]]
        </li>
    </ul>
</body>
</html>

 

출력 결과

 

th:if와 th:unless를 이용하면 상황에 맞게 다른 내용을 출력할 수 있다.

 

ex)sno가 5로 나눈 나머지가 0인 경우 sno만을 출력하고 그렇지 않다면 SampleDTO의 first를 출력하고 싶을 때

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <ul>
        <li th:each="dto, state :${list}">
            <span th:if="${dto.sno %5 ==0}" th:text="${'-------------'+dto.sno}"></span>
            <span th:unless="${dto.sno %5 ==0}" th:text="${dto.first}"></span>
        </li>
    </ul>
</body>
</html>

 

실행 결과

🤍타임리프 기본 사용법 (3) inline 속성

 

  • 인라인 속성은 주로 자바스크립트 처리에서 유용함.
  • 내부적으로 RedirectAttributes를 이용하여 /ex3으로 result와 dto라는 이름의 데이터를 심어서 전달함. result는 단순한 문자열이지만 dto는 SampleDTO의 객체임.
GetMapping({"/exInline"})
    public String exInline(RedirectAttributes redirectAttributes){
        log.info("exInline...........");

        SampleDTO dto = SampleDTO.builder().sno(100L).first("First..100").last("Last..100").regTime(LocalDateTime.now()).build();
        redirectAttributes.addFlashAttribute("result","success");
        redirectAttributes.addFlashAttribute("dto",dto);

        return "redirect:/sample/ex3";
    }

 

<script> 태그에 사용된 th:inline 속성이 핵심임.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 th:text="${result}"></h1>
    <h1 th:text="${dto}"></h1>

    <script th:inline="javascript">
        var msg = [[${result}]];
        var dto = [[${dto}]];
    </script>
</body>
</html>

 

실행 결과를 보면

별도의 처리 없이 자바스크립트 부분이 만들어짐, dto는 JSON 포맷의 문자열이 된 것을 볼 수 있다.

 

🤍타임리프 기본 사용법 (4) 링크 처리

 

먼저 exModel()을 재사용하도록 아래와 같이 코드 수정.

  @GetMapping({"/ex2", "/exLink"})
    public void exModel(Model model){
        List<SampleDTO> list = IntStream.rangeClosed(1,20).asLongStream().mapToObj(i->{
            SampleDTO dto = SampleDTO.builder().sno(i).first("First.."+i).last("Last.."+i).regTime(LocalDateTime.now()).build();
            return dto;
        }).collect(Collectors.toList());
        //SampleDTO 타입의 객체를 20개 추가하고 이를 Model에 담아 전송함.
        model.addAttribute("list",list);
    }

 

exLink.html 생성

  • @{ } 로 구성된 링크를 처리함.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <ul>
        <li th:each="dto : ${list}">
            <a th:href="@{/sample/exView}">[[${dto}]]</a>
        </li>
    </ul>
</body>
</html>

 

실행 결과

🤍타임리프 기본 사용법 (5) 레이아웃

 

✅타임리프의 레이아웃 기능은 크게 2가지 방법이 있다.

  • JSP의 include와 같이 특정 부분을 가져와서 포함하는 방식
  • 특정 부분을 파라미터로 전달해서 내용에 포함하는 방식

SampleController 코드 작성

@GetMapping("/exLayout1")
public void exLayout1(){
    log.info("exLayout................");
}

 

fragment1.html 생성

특정 부분을 다른 내용으로 변경하려면 th:insert or th:replace를 사용할 수 있다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>Fragment Test</h1>

    <h1>Layout 1 - 1</h1>
    <div th:replace="~{/fragments/fragment1 :: part1}"></div>

    <h1>Layout 1 - 2</h1>
    <div th:replace="~{/fragments/fragment1 :: part2}"></div>

    <h1>Layout 1 - 3</h1>
    <th:block th:replace="~{/fragments/fragment1 :: part3}"></th:block>
</body>
</html>

 

exLayout1.html 생성 (exLayout.thml 내용을 fragment1.html의 th:fragment에서 가져다 쓸 예정)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>Fragment Test</h1>

    <h1>Layout 1 - 1</h1>
    <div th:replace="~{/fragments/fragment1 :: part1}"></div>

    <h1>Layout 1 - 2</h1>
    <div th:replace="~{/fragments/fragment1 :: part2}"></div>

    <h1>Layout 1 - 3</h1>
    <th:block th:replace="~{/fragments/fragment1 :: part3}"></th:block>
</body>
</html>

 

실행 결과

  • th:replace를 이용할 때 '::' 뒤에는 fragment의 이름을 지정하거나 CSS의 #ID같은 선택자를 이용할 수 있다, 만약 '::' 이하를 생략하면 해당 파일 전체 내용을 가져올 수 있다.

fragment2.html 작성

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div>
        <hr/>
        <h2>Fragment2 File</h2>
        <h2>Fragment2 File</h2>
        <h2>Fragment2 File</h2>
        <hr/>
    </div>
</body>
</html>

 

fragment2.html 전체를 가져오도록 exLayout1.html 수정

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>Fragment Test</h1>
    <div style="border: 1px solid blue">

    <th:block th:replace="~{/fragments/fragment2}"></th:block>

    </div>
    <h1>Layout 1 - 1</h1>
    <div th:replace="~{/fragments/fragment1 :: part1}"></div>

    <h1>Layout 1 - 2</h1>
    <div th:replace="~{/fragments/fragment1 :: part2}"></div>

    <h1>Layout 1 - 3</h1>
    <th:block th:replace="~{/fragments/fragment1 :: part3}"></th:block>
</body>
</html>

 

실행 화면

🤍타임리프 기본 사용법 (6) 레이아웃 템플릿 만들기

 

layout1.html 작성

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

<th:block th:fragment="setContent(content)">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <style>
        *{
            margin: 0;
            padding: 0;
        }
        .header{
            width: 100vw;
            height: 20vh;
            background-color: aqua;
        }
        .content{
            width: 100vw;
            height: 70vh;
            background-color: lightgray;
        }
        .footer{
            width: 100vw;
            height: 10vh;
            background-color: green;
        }
    </style>

    <div class="header">
        <h1>HEADER</h1>
    </div>
    <div class="content">
        <th:block th:replace="${content}">
        </th:block>
    </div>
    <div class="footer">
        <h1>FOOTER</h1>
    </div>
</body>
</th:block>
</html>

 

exTemplate.html 작성

<!DOCTYPE html>
<html lang="en"  xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/layout1 :: setContent(~{this::content})}">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <th:block th:fragment="content">
        <h1>exTemplate Page</h1>
    </th:block>
</body>
</th:block>
</html>

 

컨트롤러 수정

@GetMapping({"/exLayout1", "/exLayout2","/exTemplate"})
    public void exLayout1(){
        log.info("exLayout................");
    }

 

실행 결과

 

🤍부트스트랩 템플릿 적용하기

Simple Sidebar 템플릿 다운받기

 

basic.html 작성

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

<th:block th:fragment="setContent(content)">

    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
        <meta name="description" content="" />
        <meta name="author" content="" />
        <title>Simple Sidebar - Start Bootstrap Template</title>
        <!-- Favicon-->
        <link rel="icon" type="image/x-icon" th:href="@{assets/favicon.ico}" />
        <!-- Core theme CSS (includes Bootstrap)-->
        <link th:href="@{/css/styles.css}" rel="stylesheet" />

        <!-- Bootstrap core JS-->
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js"></script>
        <!-- Core theme JS-->
        <script th:src="@{/js/scripts.js}"></script>
    </head>
    <body>
    <div class="d-flex" id="wrapper">
        <!-- Sidebar-->
        <div class="border-end bg-white" id="sidebar-wrapper">
            <div class="sidebar-heading border-bottom bg-light">Start Bootstrap</div>
            <div class="list-group list-group-flush">
                <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Dashboard</a>
                <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Shortcuts</a>
                <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Overview</a>
                <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Events</a>
                <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Profile</a>
                <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Status</a>
            </div>
        </div>
        <!-- Page content wrapper-->
        <div id="page-content-wrapper">
            <!-- Top navigation-->
            <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
                <div class="container-fluid">
                    <button class="btn btn-primary" id="sidebarToggle">Toggle Menu</button>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
                    <div class="collapse navbar-collapse" id="navbarSupportedContent">
                        <ul class="navbar-nav ms-auto mt-2 mt-lg-0">
                            <li class="nav-item active"><a class="nav-link" href="#!">Home</a></li>
                            <li class="nav-item"><a class="nav-link" href="#!">Link</a></li>
                            <li class="nav-item dropdown">
                                <a class="nav-link dropdown-toggle" id="navbarDropdown" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
                                <div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
                                    <a class="dropdown-item" href="#!">Action</a>
                                    <a class="dropdown-item" href="#!">Another action</a>
                                    <div class="dropdown-divider"></div>
                                    <a class="dropdown-item" href="#!">Something else here</a>
                                </div>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>
            <!-- Page content-->
            <div class="container-fluid">
                <th:block th:replace = "${content}"></th:block>
            </div>
        </div>
    </div>
</th:block>


</body>
</html>

 

컨트롤러 수정

@GetMapping({"/exLayout1", "/exLayout2","/exTemplate", "/exSidebar"})
    public void exLayout1(){
        log.info("exLayout................");
    }

 

exSidebar.html 작성

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic :: setContent(~{this::content})}">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <th:block th:fragment="content">
        <h1>exSidebar Page</h1>
    </th:block>
</body>
</th:block>
</html>

 

실행 결과를 보면 레이아웃이 유지되는 상황에서 아래쪽에는 exSidebar.html의 내용을 그대로 출력하고 있는 것을 볼 수 있다.

반응형
profile

my code archive

@얼레벌레 개발자👩‍💻

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

반응형