본문 바로가기
공부 기록

[스프링 부트로 게시판 만들기] 04. Thymeleaf + Bootstrap

by 매트(Mat) 2021. 7. 19.



Thymeleaf란

  • 회원가입을 진행하기 전에 먼저 메인 페이지를 만들어보도록 하겠습니다.
  • Thymeleaf란 HTML, XML, JavaScript, CSS 및 일반 텍스트까지 처리할 수 있는 웹 및 독립 환경을 위한 서버 사이드 Java 템플릿 엔진입니다.
  • JSP의 태그 라이브러리를 보면 브라우저가 이해할 수 없는 코드가 포함되어 있는 반면에, Thymeleaf는 브라우저가 이해할 수 있는 코드이기 때문에 퍼블리셔와 협업할 때도 좋은 시너지를 낼 수 있습니다.
  • 또한 스프링 부트가 Thymeleaf를 지원하기 때문에 사용하기가 좋습니다.
  • (출처: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html)



Thymeleaf 의존성 추가

  • Thymeleaf를 사용하기 위해서는 의존성을 추가해야 합니다.
dependencies {
    ...
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}
  • Thymeleaf 같은 템플릿 엔진은 resources 폴더에 templates 폴더안에 생성합니다.
  • JSP는 스프링 부트가 지원하지 않아서 따로 webapp > WEB-INF > views라는 폴더를 만들지만 Thymeleaf는 스프링 부트가 지원해서 따로 폴더 생성 없이 templates 폴더안에 생성합니다.
  • static 폴더는 정작 파일들(html, css, js 등등)을 두는 곳입니다.



메인 화면 만들기

  • Thymeleaf와 Bootstrap을 이용하여 만들어보도록 하겠습니다.
  • Bootstrap은 반응형 웹을 쉽고 빠르게 만들 수 있는 도구입니다.
  • 참고로 Community(무료버전)는 html, css, javascript 등을 지원해주지 않기 때문에 VSCode를 이용하겠습니다.
  • 이 부분은 html, css 등은 알고 있다는 전제하에 만들어서 따로 코드 설명은 하지 않겠습니다.
  • UI는 Bootstrap의 https://getbootstrap.com/docs/5.0/examples/Sticky footer with fixed navbar을 참고하여 만들었습니다.
  • 먼저 Bootstrap만으로 코드를 작성하겠습니다.
  • 폴더 구조는 다음과 같습니다.
캡처

index.html

<!doctype html>
<html lang="en" class="h-100">

<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <link href="./css/sticky-footer-navbar.css" rel="stylesheet">
    <link rel="stylesheet" href="./css/style.css">
    <title>블로그</title>

</head>

<body class="d-flex flex-column h-100">
<header>
    <!-- Fixed navbar -->
    <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
        <div class="container-fluid">
            <a class="navbar-brand" href="/">내가 만든 블로그</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarCollapse">
                <ul class="navbar-nav me-auto mb-2 mb-md-0">
                    <li class="nav-item">
                        <a class="nav-link" href="/user/login">로그인</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/user/save">회원가입</a>
                    </li>
                </ul>
                <form class="d-flex">
                    <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
                    <button class="btn btn-outline-success" type="submit">Search</button>
                </form>
            </div>
        </div>
    </nav>
</header>

<!-- Begin page content -->
<main class="flex-shrink-0">
    <div class="container">
        <div class="p-2"></div>
        <div class="row g-0 border rounded overflow-hidden flex-md-row mb-4 shadow-sm h-md-250 position-relative">
            <div class="col p-4 d-flex flex-column position-static">
                <a href="#" class=".a-title">
                    <h3 class="mb-0 title" style="padding-bottom: 10px;">제목</h3>
                </a>
                <p class="card-text mb-auto">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Officia repellendus dolore quibusdam voluptatibus libero consectetur, autem assumenda quis accusamus ratione qui. Nobis sit cumque deleniti, facilis praesentium magni voluptates
                    perspiciatis!LoremLoremLoremLoremLoremLoremLoremLorem
                </p>
                <div class="mb-1 text-muted" style="padding-top: 15px;">날짜</div>
            </div>
        </div>

        <div class="row g-0 border rounded overflow-hidden flex-md-row mb-4 shadow-sm h-md-250 position-relative">
            <div class="col p-4 d-flex flex-column position-static">
                <a href="#" class=".a-title">
                    <h3 class="mb-0 title" style="padding-bottom: 10px;">제목</h3>
                </a>
                <p class="card-text mb-auto">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Officia repellendus dolore quibusdam voluptatibus libero consectetur, autem assumenda quis accusamus ratione qui. Nobis sit cumque deleniti, facilis praesentium magni voluptates
                    perspiciatis!LoremLoremLoremLoremLoremLoremLoremLorem
                </p>
                <div class="mb-1 text-muted" style="padding-top: 15px;">날짜</div>
            </div>
        </div>

        <div class="row g-0 border rounded overflow-hidden flex-md-row mb-4 shadow-sm h-md-250 position-relative">
            <div class="col p-4 d-flex flex-column position-static">
                <a href="#" class=".a-title">
                    <h3 class="mb-0 title" style="padding-bottom: 10px;">제목</h3>
                </a>
                <p class="card-text mb-auto">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Officia repellendus dolore quibusdam voluptatibus libero consectetur, autem assumenda quis accusamus ratione qui. Nobis sit cumque deleniti, facilis praesentium magni voluptates
                    perspiciatis!LoremLoremLoremLoremLoremLoremLoremLorem
                </p>
                <div class="mb-1 text-muted" style="padding-top: 15px;">날짜</div>
            </div>
        </div>
    </div>
</main>

<footer class="footer mt-auto py-3 bg-light">
    <div class="container">
        <span class="text-muted">© azurealstn</span>
    </div>
</footer>

<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>

</html>
  • html 안에 데이터들은 모두 정적 데이터라 제가 일단 지어놓았습니다. (어느정도 화면 구성을 알기 위해)


style.css

.card-text {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    white-space: normal;
    overflow: hidden;
}

.a-title {
    text-decoration: none;
    color: #4e423b;
}

.a-title:hover {
    color: #15751b;
    text-decoration: underline;
}
  • 내용에서 3줄이 넘어가면 ...으로 표시되게 css의 clamp를 이용해주었습니다.
  • 나머지 style은 본인에 맞게 해주시면 됩니다ㅎㅎ..


sticky-footer-navbar.css

/* Custom page CSS
-------------------------------------------------- */
/* Not required for template or sticky footer method. */

main > .container {
  padding: 60px 15px 0;
}


화면

캡처
  • 대충 요런 화면이 나옵니다.
  • 이제 안에 검색, 회원가입, 로그인, 페이징 등등을 구현할 겁니다.



Thymeleaf 적용

  • JSP를 사용할 때 <%@ include file="layout/header.jsp"%>이런 코드를 해서 하나의 페이지안에 세분화 작업을 했습니다. (header, main, footer 이렇게..)
  • Thymeleaf도 fragment, replace 문법을 이용하여 똑같이 작업해주겠습니다.
  • 폴더 구조는 다음과 같이 만들겠습니다.
캡처

  • thymeleaf 공식 홈페이지에 보면 fragment 템플릿이 다음과 같습니다.
<!DOCTYPE html>

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

  <body>

    <div th:fragment="copy">
      &copy; 2011 The Good Thymes Virtual Grocery
    </div>

  </body>

</html>
  • 이 부분에 header 태그를 가져오겠습니다.

header.html

<!DOCTYPE html>

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

<head th:fragment="head(title)">
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <link href="../static/sticky-footer-navbar.css" rel="stylesheet">
    <link rel="stylesheet" href="../static/style.css">
    <title th:text="${title}">블로그</title>

</head>

<body>

    <header th:fragment="header">
        <!-- Fixed navbar -->
        <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
            <div class="container-fluid">
                <a class="navbar-brand" href="/">내가 만든 블로그</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarCollapse">
                    <ul class="navbar-nav me-auto mb-2 mb-md-0">
                        <li class="nav-item">
                            <a class="nav-link" href="/user/login">로그인</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="/user/save">회원가입</a>
                        </li>
                    </ul>
                    <form class="d-flex">
                        <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
                        <button class="btn btn-outline-success" type="submit">Search</button>
                    </form>
                </div>
            </div>
        </nav>
    </header>

</body>

</html>
  • <head th:fragment="head(title)"> head 태그 안에 반복되므로 적어줍니다.
  • 여기서는 title 태그가 페이지마다 바뀌는데 이 때는 파라미터로 받아서 사용하시면 됩니다. title 태그 안에 <title th:text="${title}"> 이렇게 적어주시면 파라미터로 받을 수 있습니다.
  • 그 다음 반복되는 부분인 header 태그 안에다 <header th:fragment="header">를 적어주시면 됩니다.
  • 이제 index.html에 돌아와 다음과 같이 고쳐줍니다.

index.html

<!doctype html>
<html lang="en" class="h-100">

<head th:replace="layout/header :: head ('블로그')"></head>

<body class="d-flex flex-column h-100">
    <header th:replace="layout/header :: header"></header>

    <!-- Begin page content -->
    <main class="flex-shrink-0">
        <div class="container">
            <div class="p-2"></div>
                <div class="row g-0 border rounded overflow-hidden flex-md-row mb-4 shadow-sm h-md-250 position-relative">
                <div class="col p-4 d-flex flex-column position-static">
                    <a href="#" class="a-title">
                        <h3 class="mb-0 title" style="padding-bottom: 10px;">제목</h3>
                    </a>
                    <p class="card-text mb-auto">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Officia repellendus dolore quibusdam voluptatibus libero consectetur, autem assumenda quis accusamus ratione qui. Nobis sit cumque deleniti, facilis praesentium magni voluptates
                        perspiciatis!LoremLoremLoremLoremLoremLoremLoremLorem
                    </p>
                    <div class="mb-1 text-muted" style="padding-top: 15px;">날짜</div>
                </div>
            </div>

            <div class="row g-0 border rounded overflow-hidden flex-md-row mb-4 shadow-sm h-md-250 position-relative">
                <div class="col p-4 d-flex flex-column position-static">
                    <a href="#" class="a-title">
                        <h3 class="mb-0 title" style="padding-bottom: 10px;">제목</h3>
                    </a>
                    <p class="card-text mb-auto">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Officia repellendus dolore quibusdam voluptatibus libero consectetur, autem assumenda quis accusamus ratione qui. Nobis sit cumque deleniti, facilis praesentium magni voluptates
                        perspiciatis!LoremLoremLoremLoremLoremLoremLoremLorem
                    </p>
                    <div class="mb-1 text-muted" style="padding-top: 15px;">날짜</div>
                </div>
            </div>

            <div class="row g-0 border rounded overflow-hidden flex-md-row mb-4 shadow-sm h-md-250 position-relative">
                <div class="col p-4 d-flex flex-column position-static">
                    <a href="#" class="a-title">
                        <h3 class="mb-0 title" style="padding-bottom: 10px;">제목</h3>
                    </a>
                    <p class="card-text mb-auto">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Officia repellendus dolore quibusdam voluptatibus libero consectetur, autem assumenda quis accusamus ratione qui. Nobis sit cumque deleniti, facilis praesentium magni voluptates
                        perspiciatis!LoremLoremLoremLoremLoremLoremLoremLorem
                    </p>
                    <div class="mb-1 text-muted" style="padding-top: 15px;">날짜</div>
                </div>
            </div>
        </div>
    </main>

    <footer class="footer mt-auto py-3 bg-light">
        <div class="container">
            <span class="text-muted">© azurealstn</span>
        </div>
    </footer>

    <!-- Option 1: Bootstrap Bundle with Popper -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>

</html>
  • head 태그안에 <head th:replace="layout/header :: head ('블로그')"></head> 이렇게 적어주시고 뒤에 파라미터를 적어주시면 됩니다.
  • header 태그안에 <header th:replace="layout/header :: header"></header> 이렇게 써주고, 그 안에 내용은 모두 지웠습니다.
  • th:replace를 쓰고 그 안에 경로를 넣고, fragment 선언했던 header를 넣어주면 됩니다.
  • footer.html도 똑같이 작업해줍니다.

footer.html

<!DOCTYPE html>

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

<body class="d-flex flex-column h-100">

<footer class="footer mt-auto py-3 bg-light" th:fragment="footer">
    <div class="container">
        <span class="text-muted">© azurealstn</span>
    </div>
</footer>

</body>

</html>

index.html

<!doctype html>
<html lang="en" class="h-100" xmlns:th="http://www.thymeleaf.org">
<head>
    ...
    <link th:href="@{./css/sticky-footer-navbar.css}" rel="stylesheet">
    <link rel="stylesheet" th:href="@{./css/style.css}">
    <title>블로그</title>

</head>

...

<footer th:replace="layout/footer :: footer"></footer>

</html>
  • link 태그에도 th:href 문법을 사용해 th:href="@{./css/style.css}"와 같이 변경해줍니다.






다음으로

  • 이 파트는 좀... 코드가 보기 힘들 것 같고.. 따로 Thymeleaf, bootstrap을 간단하게 배워서 직접 UI를 짜는 게 더 효율적일 것 같다는 생각이 듭니다.
  • Spring Boot와 Thymeleaf 적용에 대한 좋은 유튜브 강의가 있으니 이를 참고해주세요. (좋은 영상)
  • 역시 테스트 작성한게 없지만 마무리는 테스트 전체 실행해주시고, 문제가 없다면 정말 다음 번에 회원 가입을 진행해보도록 하겠습니다.






References

댓글