프레임워크/Spring

#12 Thymeleaf 3

Scala0114 2022. 5. 10. 01:01
  • 프론트엔드 개발을 하다보면 여러 페이지에서 공통적으로 사용하게 되는 코드가 다수 존재한다.  예를 들면 직접 구현한 select box라던가 header와 footer, 공통 css파일이나 js파일 등을 추가하는 코드 등이 그렇다. 이런 중복 코드를 한 번만 작성하여 재사용 가능하도록 thymeleaf에서는 fragment 기능을 제공한다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <style>
        ul, li {
            list-style: none;
        }
        .select-box {
            display: inline-block;
        }
        .select-box-selected {
            border: 1px solid black;
            width: 100%;
            cursor: pointer;
        }
        .select-box-items {
            display: none;
            border: 1px solid black;
            width: 100%;
        }
        .select-box-open + .select-box-items {
            display: inline-block;
        }
    </style>
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div class="select-box" style="display: inline-block">
        <div class="select-box-selected" onclick="this.classList.toggle('select-box-open')" style="border: 1px solid black;">
            항목을 선택해주세요
        </div>
        <div class="select-box-items">
            <ul>
                <li>항목 1</li>
                <li>항목 1</li>
                <li>항목 1</li>
                <li>항목 1</li>
            </ul>
        </div>
    </div>

</body>
</html>

 

 

  • index.html에 위와 같이 간단하게 클릭하면 드롭다운 메뉴가 나타나는 기능을 구현해두고 여러 템플릿에서 반복적으로 사용한다고 해보자. 저 기능이 사용되는 모든 페이지에 위의 코드를 작성하는 것은 굉장히 비효율적일 것이다. 이 기능을 fragment화 하여 간단하게 재사용할 수 있도록 만들어보자.

 

  • 먼저 만들었던 기능의 html 코드를 별도의 html 파일에 옮긴 후 최상위 태그에 th:fragment="<fragment 이름>" 의 형태로 th:fragment 속성을 추가해준다. 만약 최상위 태그가 여러개인 경우 이전 글에서 배웠던 th:block 태그로 한번 감싼 뒤 th:block 태그에 th:fragment 속성을 추가해주면 된다. 

  • 여기서는 templates/fragment/simple_fragment.html 에 fragment를 작성하였다.

 

  • 다음으로 style 태그에 작성했던 stylesheet의 내용을 static/css/style.css 에 옮겨주었다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div th:replace="fragment/simple_fragment::simple-fragment"></div>
</body>
</html>
  • 남은건 index.html에서 fragment를 사용하는 것 뿐이다. head태그에서 css/style.css 를 추가해준 뒤 th:replace 속성을 사용하여 fragment를 사용해준다. th:replace="<fragment 경로>::<fragment 이름>" 의 형태로 사용할 수 있다.

 

  • 처음에 직접 index.html에 코드를 작성했을 때와 동일하게 동작하는 것을 볼 수 있다. 이제 이 기능은 어느 템플릿에서든 css추가와 th:replace 코드 한줄만 작성하면 재사용이 가능하다.  th:fragment 와 th:replace 를 사용하면 이처럼 자주 사용되는 코드를 손쉽게 재사용할 수 있다. 

 

 

2. th:fragment에서 변수 사용하기

  • 그런데 위에서 만든 fragment에는 조금 문제가 있다. 완벽하게 동일한 코드가 필요할 때만 사용 가능하다는 점이다. 실제로 개발을 하다보면 형식과 기능은 동일하지만 내용은 조금씩 바뀌어야하는 경우도 굉장히 많다.  이번에 만든 fragment의 경우 "항목을 선택해주세요" 부분이나 항목 1~4 부분을 다른 값으로 바꾸어 사용하고 싶을 수도 있을 것이다.  그렇게 내용에만 조금씩 차이가 있는 경우에도 매번 새로운 fragment를 만들어야한다면 굉장히 불편할 것이다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
    <div th:fragment="simple-fragment(defaultText, itemList)" class="select-box" style="display: inline-block">
        <div class="select-box-selected"
             onclick="this.classList.toggle('select-box-open')"
             style="border: 1px solid black;"
             th:text="${ defaultText }">
        </div>
        <div class="select-box-items">
            <ul>
                <li th:each="item: ${ itemList }" th:text="${ item }"></li>
            </ul>
        </div>
    </div>
</html>
  • thymeleaf의 fragment 기능은 fragment 사용시 매개변수를 넘겨주는 것으로 fragment의 내용을 어느정도 컨트롤하는 것이 가능하다. 먼저 simple_fragment.html에서 th:fragment="<fragment 이름>(<매개변수명 ...>)" 의 형태로 th:fragment 속성을 다시 지정해보자. 

  • 그리고나서 지금까지 배웠던 thymeleaf의 기능을 사용하여 "항목을 선택해주세요" 대신 defaultText 값을, 항목 1~4 대신 itemList 값을 사용하여 select-box-items 내의 리스트를 표현하도록 코드를 작성해주면 fragment의 수정은 끝이다.

 

package com.scala.personal.mycomm.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(Model model) {
        model.addAttribute("myItemList", List.of("myitem1", "myitem2", "myitem3", "myitem4"));
        return "index";
    }
}

 

  • 다음으로 Controller에서 itemList로 사용할 문자열 리스트를 myItemList라는 이름으로 model에 추가해준다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div th:replace="fragment/simple_fragment::simple-fragment(defaultText='please select item', itemList=${ myItemList })"></div>
</body>
</html>
  • 마지막으로 위와 같이 index.html의 th:replace를 사용한 부분에서 fragment가 요구하는 매개변수 값을 넘겨준다.

 

  • 서버를 구동하고 접속해보면 기대한 대로 fragment의 내용물이 설정된 것을 볼 수 있다.

 

 

3. 입력하지 않아도 되는 매개변수 - th:with

  • 그런데 th:fragment에서 사용할 변수값을 th:fragment="<fragment 이름>(<매개변수명 ...>)" 의 형태로 받을 경우 fragment 사용시 반드시 매개변수 값을 넣어줘야만 fragment가 정상적으로 동작한다. 매개변수를 전달해줄 수도 있지만 전달하지 않을 경우에도 default 값을 사용하여 정상적으로 작동하게 할 수는 없을까?

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
    <div th:fragment="simple-fragment(itemList)"
         th:with="defaultText=${ defaultText } ?: '항목을 선택해주세요.'"
         class="select-box"
         style="display: inline-block">
        <div class="select-box-selected"
             onclick="this.classList.toggle('select-box-open')"
             style="border: 1px solid black;"
             th:text="${ defaultText }">
        </div>
        <div class="select-box-items">
            <ul>
                <li th:each="item: ${ itemList }" th:text="${ item }"></li>
            </ul>
        </div>
    </div>
</html>
  • th:with 속성을 사용하여 이 문제를 해결할 수 있다. th:with="<매개변수 이름>=${ <매개변수 이름> } ?: '항목을 선택해주세요.'" 의 형태로 이전에 알아본 thymeleaf의 연산자중 하나인 default 조건 연산자를 사용하여 만약 매개변수를 입력받았다면 그 매개변수의 값을, 입력받지 않았다면 default 값을 사용하도록 할 수 있다. 

  • 여기서 왼쪽의 <매개변수 이름>은 fragment 내에서 사용하기 위한 변수 이름이며 오른쪽의 <매개변수 이름>은 th:replace에서 매개변수를 전달하기 위해 사용할 이름이기 때문에 꼭 같아야할 필요는 없다.

  • simple-fragment의 defaultText 를 default 값을 가지는 변수로 바꿔보자. 

 

  • 그리고나서 다시 서버를 구동하고 접속해보면 아직은 index.html이 defulatText 매개변수를 전달하고있기 때문에 이전처럼 select-box-selected의 텍스트가 "please select item" 이라고 표시되어 있는 것을 볼 수 있다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div th:replace="fragment/simple_fragment::simple-fragment(itemList=${ myItemList })"></div>
</body>
</html>
  • 이번엔 index.html에서 fragment 사용시 defaultText 매개변수를 넘기지 않도록 해보자.

 

  • 의도한 대로 default 값인 "항목을 선택해주세요"가 나타나는 것을 볼 수 있다. 이제 fragment에서 사용할 변수를 선택적으로 매개변수로 받아올 수 있게 되었다.

 

 

4. th:insert

  • th:replace는 이름대로 해당 속성이 적용된 태그 전체를 fragment로 대체하는 형태로 fragment를 적용시켜준다. 그런데 속성이 적용된 태그 자체는 내버려둔 채로 내부에 fragment를 삽입할 수는 없을까?

  • 이 문제를 해결하려면 th:insert를 사용할 수 있다. th:insert는 이름에서도 유추할 수 있듯이 해당 속성이 적용된 태그 내부에 fragment를 삽입하는 형태로 fragment를 적용시켜준다. 

  • 3.0 이전에 사용되던 th:include 라는 속성도 있지만 이 속성의 사용은 thymeleaf 3.0 이후로 권장되고 있지 않기 때문에(th:insert로 충분히 대체 가능하기 때문으로 보인다.) 굳이 알아둬야할 필요는 없을 듯 하다.

 

  • th:replace 를 사용하여 fragment를 사용하던 기존 방식의 경우 위와 같이 div 태그 전체가 fragment로 대체된 것을 볼 수 있다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div th:insert="fragment/simple_fragment::simple-fragment(itemList=${ myItemList })"></div>
</body>
</html>

 

  • th:replace를 th:insert로 바꾸고 다시 서버를 구동하여 접속해보면 th:insert 속성이 적용된 최상위 div 태그가 그대로 남아있고 내부에 fragment가 삽입된 것을 볼 수 있다.

 

 

5. fragment 표현식 ~{ ... }

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="simple-fragment(itemList)"
     th:with="defaultText=${ defaultText } ?: '항목을 선택해주세요.'"
     class="select-box">
  <div class="select-box-selected"
       onclick="this.classList.toggle('select-box-open')"
       th:text="${ defaultText }">
  </div>
  <div class="select-box-items" th:insert="${ itemList }">
  </div>
</div>
</html>

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div th:insert="fragment/simple_fragment::simple-fragment(itemList=~{ ::#simple-my-list })">
        <ul id="simple-my-list">
            <li th:each="item: ${ myItemList }" th:text="${ item }"></li>
        </ul>
    </div>
</body>
</html>
  • fragment 표현식은 fragment를 객체(object)처럼 사용할 수 있도록 하기 위한 표현식이다. 조건에 따라 특정 fragment를 삽입하는 등의 방식으로도 사용 가능하고 fragment의 매개변수로 fragment를 넘겨주는 경우에도 사용할 수 있다.
  • simple-fragment의 select-box-items가 항목 문자열 리스트를 받는 대신 항목 리스트 프래그먼트를 받아 insert 하도록 수정한 후 index.html에서 항목 리스트 프래그먼트를 만들어 fragment 표현식을 사용하여 simple-fragment에 넘겨주자.

 

  • 서버를 재구동하고 접속해보면 이전과 동일하게 동작하는 것을 확인할 수 있다.  위에서 설명했던 프래그먼트의 내용을 커스터마이징하는 경우에 이 방식을 자주 사용하게되니 잘 알아둘 필요가 있다.

 

 

 Thymeleaf에는 이 외에도 다양한 기능이 있지만 지금까지 알아본 Thymeleaf의 문법과 기능만 잘 숙지해둔다면 이후 프로젝트를 진행하는데 전혀 무리가 없을 것이다. Thymeleaf 에 대한 내용은 여기까지로 끝내고 다음 포스팅부터는 다시 본 목적으로 돌아가서 게시판 개발을 진행하기로 하자.