타임 리프 사용법 정리

박준우·2025년 9월 7일
0

Spring Boot

목록 보기
13/14

1. 타임리프란?

동적인 html을 만들기 위해 사용하는 탬플릿 엔진이다. 템플릿 엔진은 타임리프, JSP, Freemarker, Groovy 등의 종류가 있지만, Thymleaf의 장점은 따로 확장파일을 만들지 않고, HTML 내의 태그로써 이를 사용할 수 있기 때문이다. 이를 보고 natural templates라고 한다.

사용자에게 화면을 띄우는 과정

사용자의 요청에 의해 유저에게 화면을 띄우는 과정은 아래와 같다.

사용자 요청 (Request) → Dispatcher Servlet → Controller → Service(내부 로직=컨트롤러에서 사용할 함수 구현) → 
Repository(interface) → (DB) → Controller → Model(저장) → View (Thymeleaf로 Model이용) → 사용자 화면 (Response)

1. 사용자의 접속: "이 페이지 보여주세요!" (Request) 🙋‍♀️

모든 것은 사용자가 브라우저 주소창에 URL을 입력하거나(GET), 폼(Form) 데이터를 전송(POST)하면서 시작됩니다.

GET 요청: "/users/1" 과 같은 특정 페이지 정보를 조회하고 싶을 때.

POST 요청: 회원가입 폼에 정보를 입력하고 '가입' 버튼을 눌러 데이터를 저장/처리하고 싶을 때.

이 요청은 스프링의 Dispatcher Servlet이라는 관문으로 가장 먼저 들어오게 됩니다. Dispatcher Servlet은 모든 요청을 받아서 어떤 컨트롤러에게 일을 시킬지 결정하는 똑똑한 안내원 역할을 합니다.

2. 컨트롤러의 역할: 요청 접수 및 처리 지시 👨‍🍳

Dispatcher Servlet은 URL을 보고 이 요청을 처리할 담당자, 즉 컨트롤러(@Controller)의 메서드를 찾아 호출합니다.

컨트롤러는 마치 레스토랑의 메인 셰프와 같습니다. 주문(Request)을 받아서 어떤 요리(Business Logic)를 할지 결정하고, 필요한 재료(Data)를 준비하도록 지시합니다.

@Controller
public class UserController {

    @GetMapping("/user-profile") // 1. GET 요청 접수
    public String getUserProfile(Model model) { // 2. Model 객체를 파라미터로 받음

        // 3. 비즈니스 로직 처리 (예: 서비스 계층 호출)
        String userName = "홍길동";
        int userAge = 30;
        // 실제로는 DB에서 조회한 데이터를 가져옵니다.

        // 4. Model에 데이터 담기
        model.addAttribute("name", userName);
        model.addAttribute("age", userAge);

        // 5. 보여줄 뷰(HTML 파일)의 논리적 이름 반환
        return "profile"; // "profile.html" 파일을 찾아줘!
    }
}

① @GetMapping("/user-profile"): /user-profile 이라는 주소로 GET 요청이 오면 이 메서드를 실행하라고 알려줍니다.

② Model model: 화면에 데이터를 전달할 Model 객체를 준비합니다. 스프링이 알아서 이 객체를 만들어 메서드에 넣어줍니다.

③ 비즈니스 로직 처리: 지금은 간단히 변수를 선언했지만, 보통은 UserService 같은 서비스 계층을 호출해서 DB에서 데이터를 조회하는 등 실제 작업이 이루어집니다.

④ model.addAttribute("key", value): 가장 중요한 부분입니다! Model이라는 배달 상자에 "name"이라는 이름표를 붙여 "홍길동"이라는 데이터를 담습니다. 이 Model은 이제 컨트롤러의 손을 떠나 뷰(Thymeleaf)로 전달될 준비를 합니다.

⑤ return "profile": 모든 처리가 끝났으니, 이제 "profile"이라는 이름의 뷰(HTML)를 사용자에게 보여주라고 반환합니다.

3. 모델의 역할: 데이터 배달부 📦

컨트롤러가 열심히 준비한 데이터를 뷰(Thymeleaf 템플릿)까지 안전하게 운반하는 것이 바로 Model의 역할입니다.

Model은 Key-Value 형태로 데이터를 저장하는 단순한 상자(객체)입니다.

컨트롤러는 addAttribute() 메서드로 데이터를 담고, Thymeleaf는 이 Model에서 데이터를 꺼내 사용합니다.

컨트롤러와 뷰 사이의 데이터 다리 역할을 수행하며, 둘 사이의 의존성을 낮춰주는 중요한 역할을 합니다.

4. 뷰(Thymeleaf)의 역할: 데이터로 화면 그리기 🎨

컨트롤러가 반환한 뷰 이름("profile")과 데이터가 담긴 Model은 이제 Thymeleaf 템플릿 엔진으로 전달됩니다.

Thymeleaf는 src/main/resources/templates 폴더에서 profile.html 파일을 찾아서 읽어들인 후, Model에 담겨온 데이터를 HTML 코드와 조합하여 최종적인 동적 HTML 페이지를 만듭니다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>User Profile</title>
</head>
<body>
    <h1><span th:text="${name}">사용자 이름</span></h1>

    <p>나이: <span th:text="${age}">0</span></p>
</body>
</html>

th:text="${name}": Thymeleaf 문법입니다. "Model에서 name이라는 key를 가진 값을 찾아서 이 <span> 태그의 텍스트로 넣어주세요"라는 뜻입니다.

엔진이 이 파일을 처리하면, ${name}은 "홍길동"으로, ${age}는 30으로 치환됩니다.

5. 최종 응답: 완성된 HTML을 사용자에게! 💻

Thymeleaf 엔진이 모든 데이터를 채워 넣어 완성한 최종 HTML 코드는 다시 Dispatcher Servlet을 거쳐 사용자의 웹 브라우저로 전송(Response)됩니다.

<!DOCTYPE html>
<html>
<head>
    <title>User Profile</title>
</head>
<body>
    <h1><span>홍길동</span></h1>
    <p>나이: <span>30</span></p>
</body>
</html>

2. 타입리프 사용법

타임리프를 사용하기 위해서는 컨트롤러 파일과, html 파일이 필요하다.

1. ThymeleafExController 컨트롤러

@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {

    @GetMapping(value="/ex01")
    public String thymeleafExample01(Model model){
        model.addAttribute("data", "타입리프 예제입니다.");
        return "thymeleafEx/thymeleafEx01";
    }
}

2. thymeleafEx/thymeleafEx01.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>타임리프 작동연습</title>
</head>
<body>
    <p th:text="${data}"> Hello Thymeleaf</p>
</body>
</html>

사용자가 /thymeleaf/ex01로 요청하면, 모델을 만들어 html로 리턴하고, html에 동적인 값을 적용한 뒤 이를 반환한다.

3. DB를 이용한 타임리프 사용 예제

(0) DTO(Data Transfer Object)

데이터를 주고 받을 떄는 Entity 클래스 자체를 반환하는게 아닌 데이터 전달용 객체(DTO)를 사용해야 한다. DB의 설계를 외부에 노출하지 않기 위해서이며, 요청과 응답 객체가 항상 엔티티와 같지는 않기 때문이다.

(1) th:text 사용 예제(String 형)

th:text는 모델에 저장된 문자열을 출력할 수 있는 타임리프 문법이다.

DTO 생성

@Getter
@Setter
public class itemDto {
    private Long id;
    private String itemNm;
    private Integer price;
    private String itemDetail;
    private String sellStatCd;
    private LocalDateTime regTime;
    private LocalDateTime updateTime;
}

컨트롤러 생성

@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {
 
 	@GetMapping(value = "/ex02")
    public String thymeleafExample02(Model model){
        ItemDto itemDto = new ItemDto();
        itemDto.setItemDetail("상품 상세 설명");
        itemDto.setItemNm("테스트 상품1");
        itemDto.setPrice(10000);
        itemDto.setRegTime(LocalDateTime.now());

        model.addAttribute("itemDto", itemDto); //itemDto 객체를 return한다. 
        return "thymeleafEx/thymeleafEx02";
    }
}    

th:text 사용 html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>상품 데이터 출력 예제</h1>
    <div>
        상품명 : <span th:text="${itemDto.itemNm}"></span> //객체의 itemNm 속성을 출력한다. 
    </div>
    <div>
        상품상세설명 : <span th:text="${itemDto.itemDetail}"></span>
    </div>
    <div>
        상품등록일 : <span th:text="${itemDto.regTime}"></span>
    </div>
    <div>
        상품가격 : <span th:text="${itemDto.price}"></span>
    </div>
</body>
</html>

(2) th:each(객체 배열, 컬렉션)

여러개의 데이터를 가진 컬렉션 데이터를 화면에 출력한다.

컨트롤러 생성

@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {
    List<ItemDto> itemDtoList = new ArrayList<>(); // List 객체 배열 
    for(int i=1;i<=10;i++){
        ItemDto itemDto = new ItemDto();
        itemDto.setItemDetail("상품 상세 설명"+i);
        itemDto.setItemNm("테스트 상품" + i);
        itemDto.setPrice(1000*i);
        itemDto.setRegTime(LocalDateTime.now());

        itemDtoList.add(itemDto);
    }

    model.addAttribute("itemDtoList", itemDtoList);
    return "thymeleafEx/thymeleafEx03";    
}

th:each 사용 html

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

<h1>상품 리스트 출력 예제</h1>

<table border="1">
    <thead>
    <tr>
        <td>순번</td>
        <td>상품명</td>
        <td>상품설명</td>
        <td>가격</td>
        <td>상품등록일</td>
    </tr>
    </thead>
    <tbody>
    <tr th:each="itemDto, status: ${itemDtoList}">
        <td th:text="${status.index}"></td>
        <td th:text="${itemDto.itemNm}"></td>
        <td th:text="${itemDto.itemDetail}"></td>
        <td th:text="${itemDto.price}"></td>
        <td th:text="${itemDto.regTime}"></td>
    </tr>
    </tbody>
</table>

</body>
</html>

(3)th:if, th:unless(조건문 1)

th:if는 조건문으로, 반환값이 true면 실행되고, th:unless는 반환값이 false일 경우 실행되는 코드이다.

컨트롤러 생성

@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {
    @GetMapping(value = "/ex04")
    public String thymeleafExample04(Model model){

        List<ItemDto> itemDtoList = new ArrayList<>();

        for(int i=1;i<=10;i++){

            ItemDto itemDto = new ItemDto();
            itemDto.setItemDetail("상품 상세 설명"+i);
            itemDto.setItemNm("테스트 상품" + i);
            itemDto.setPrice(1000*i);
            itemDto.setRegTime(LocalDateTime.now());

            itemDtoList.add(itemDto);
        }

        model.addAttribute("itemDtoList", itemDtoList);
        return "thymeleafEx/thymeleafEx04";
    } 
}

th:if, th:unless 사용 html

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

<h1>상품 리스트 출력 예제</h1>

<table border="1">
    <thead>
    <tr>
        <td>순번</td>
        <td>상품명</td>
        <td>상품설명</td>
        <td>가격</td>
        <td>상품등록일</td>
    </tr>
    </thead>
    <tbody> // status.even = 짝수 순번이면 true반환
    <tr th:each="itemDto, status: ${itemDtoList}">
        <td th:if="${status.even}" th:text="짝수"></td>  //짝수임으로 짝수를 저장
        <td th:unless="${status.even}" th:text="홀수"></td> //짝수가 아님으로 홀수를 저장
        <td th:text="${itemDto.itemNm}"></td>
        <td th:text="${itemDto.itemDetail}"></td>
        <td th:text="${itemDto.price}"></td>
        <td th:text="${itemDto.regTime}"></td>
    </tr>
    </tbody>
</table>

</body>
</html>

(4) th:switch, th:case(조건문 2)

th:switch, th:case는 True, false 뿐만 아닌 여러 조건을 동시에 처리할 때 사용한다.

th:switch, th:case 사용 html

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

<h1>상품 리스트 출력 예제</h1>

<table border="1">
    <thead>
    <tr>
        <td>순번</td>
        <td>상품명</td>
        <td>상품설명</td>
        <td>가격</td>
        <td>상품등록일</td>
    </tr>
    </thead>
    <tbody>
    <tr th:each="itemDto, status: ${itemDtoList}">
        <td th:switch="${status.even}"> // switch를 통해 조건을 설정한다.
            <span th:case=true>짝수</span> // case를 통해 실행조건을 선택한다.
            <span th:case=false>홀수</span>
        </td>
        <td th:text="${itemDto.itemNm}"></td>
        <td th:text="${itemDto.itemDetail}"></td>
        <td th:text="${itemDto.price}"></td>
        <td th:text="${itemDto.regTime}"></td>
    </tr>
    </tbody>
</table>

</body>
</html>

(5) th:href(링크)

th:href는 문자에 URL링크를 넣고 싶을 때 사용한다. 링크의 종류로는 Absolute URL과 Context-relative URL이 존재한다. absolute URL은 외부의 서버로 이동할 때 사용하며, 'https://' 또는 'http://'로 시작한다. Context-relative URL은 서버 내부에서 이동함으로 /로 시작하는 경로를 통해 이동할 수 있다.

컨트롤러 생성

@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {

    @GetMapping(value = "/ex05")
    public String thymeleafExample05(){
        return "thymeleafEx/thymeleafEx05";
    }    
    
    //파라미터 전달 용도.
    @GetMapping(value = "/ex06")
    public String thymeleafExample06(String param1, String param2, Model model){
        model.addAttribute("param1", param1); // param1이라는 문자가 저장된 인자를 "param1"로 모델네이밍
        model.addAttribute("param2", param2);
        return "thymeleafEx/thymeleafEx06";
    }    
    
}

th:href 사용 html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>Thymeleaf 링크처리 예제 페이지</h1>
    <div>
        <a th:href="@{/thymeleaf/ex01}">예제1 페이지 이동</a> //서버 내 이동 링크 생성
    </div>
    <div>
        <a th:href="@{https://www.thymeleaf.org/}">thymeleaf 공식 페이지(외부) 이동</a>
    </div>
    <div>
    	//파라미터 데이터 전달.
        <a th:href="@{/thymeleaf/ex06(param1 = '파라미터 데이터1', param2 = '파라미터 데이터2')}"> 
        thymeleaf 파라미터 전달</a>
        
    </div>
</body>
</html>

th:href로 파라미터 전달하기.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>파라미터 전달 예제</h1>
    <div th:text="${param1}"></div> // 이 창에 전달된 '파라미터 데이터1' 출력
    <div th:text="${param2}"></div>
</body>
</html>

4.타임리프 페이지 레이아웃(공유 헤더)

html에는 3가지 영역이 있다. 헤더, 바디, 푸터, (+메뉴) 이다. 이 중 헤더와 푸터 메뉴 영역은 페이지를 이동 하더라도 똑같은 내용을 출력한다. 따라서, 페이지마다 이를 추가하지 않고 공유하며 사용해야 하며 그 기능을 지원하는 것이 Thymeleaf-layout-dialect 의존성이다.

(1) 의존성 추가(gradle)

먼저 아래처럼 gradle에 의존성(dependencies)을 직접 추가해준다.

implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' 

(2) fragments 패키지 추가

fragments 패키지는 공통된 요소(헤더 등)를 보관하는 패키지다.

layout 패키지는 공통된 요소를 적용할 html 뼈대 파일이다. 예를 들어 사이트에 공통으로 적용될 배경같은 것을 보관한다.

두 패키지 모두 사용자가 직접 만들어야 하며, html파일이기에 template폴더에 이를 만들면 된다.

1. 푸터 만들기

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <div class="footer" th:fragment="footer">
    	푸터 영역입니다.
    </div>
</html>

2. 헤더 만들기

<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org">
	<div th:fragment="header"></div>
	헤더 영역입니다.
	</div>
</html>

3. 레이아웃 만들기

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> //
<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <th:block layout:fragment="script"></th:block>
    <th:block layout:fragment="css"></th:block>

</head>
<body>

    <div th:replace="~{fragments/header::header}"></div> // 헤더

    <div layout:fragment="content" class="content"> //이 부분 내용 치환

    </div>

    <div th:replace="~{fragments/footer::footer}"></div>// 푸터

</body>
</html>

xmlns: "XML Namespace"의 약자이다. "지금부터 이름 공간(소속)을 선언할게"라는 의미의 명령어다. 즉, xlmlns:th 는 th:를 사용하면, html페이지에서 th: 로 시작하는 속성들은 타임리프 라이브러리를 사용한다는 의미이다. 따라서 두번째 xmlns:layout 의 의미는 앞으로 th:layout 을 지정한 URL의 라이브러리를 사용함을 의미한다.

th:replace는 해당속성이 선언된 html 태그를 다른 html파일로 치환하는 것이다. 즉, fragment/header 파일의 header 태그를 레이아웃 html로 불러온다고 생각하자.

layout:fragment="content"는 이 칸의 이름을 content로 지정한다. 다른 본문 html파일에서 content라는 이름을 통해 이 공간에 데이터를 넣는것이 가능하다. class ="content" 는 추후 css나 자바스크립트를 사용하기 위해 붙여진 이름이다.

본문 만들기(content영역)

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

<div layout:fragment="content">
    본문 영역 입니다.
</div>

</html>

layout:decorate= 이 페이지를 다른 html파일로 옮김을 의미한다.
~{layouts/layout1}페이지를 보낼 주소이다.

layout:fragment=content 는 이 부분의 이름을 content로 설정함을 의미한다.

컨트롤러

@Controller
@RequestMapping(value="/thymeleaf")
public class ThymeleafExController {
	@GetMapping(value = "/ex07")
    public String thymeleafExample07(){
        return "thymeleafEx/thymeleafEx07";
    }
}

총 정리

  1. 사용자가 html을 요청한다.(컨트롤러)
  2. 레이아웃을 불러온다.
  3. 레이아웃이 헤더, 푸터, 본문을 가져와 넣는다.
  4. 반환한다.

5. 부트 스트랩 사용하기

부트 스트랩은 웹사이트를 쉽게 만들수 있게 도와주는 프레임워크다. 부트 스트랩을 이용하면 쉽게 웹페이지를 꾸밀 수 있다. CDN(Content Delivery Network)는 물리적으로 멀리 떨어진 css, 자바 스크립트 이미지를 빨리 받을 수 있게 가까운서버에서 캐싱해둔 파일을 의미한다. 본서버가 미국에 있기에, 직접 추가하는것이 더 현명하나, CDN을 이용해 외부 URL에서 html, css, javascript 부트 스트랩 파일을 받을 수 있다.

이는 프론트 영역임으로 깊게 들어가지 않고 예제만 올리겠다.

(1) 예제

1. 레이아웃
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <!-- CSS only -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link th:href="@{/css/layout1.css}" rel="stylesheet">

    <!-- JS, Popper.js, and jQuery -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

    <th:block layout:fragment="script"></th:block>
    <th:block layout:fragment="css"></th:block>

</head>
<body>

    <div th:replace="~{fragments/header::header}"></div>

    <div layout:fragment="content" class="content">

    </div>

    <div th:replace="~{fragments/footer::footer}"></div>

</body>
</html>

2. 푸터
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <div class="footer" th:fragment="footer">
        <footer class="page-footer font-small cyan darken-3">
            <div class="footer-copyright text-center py-3">
                2020 Shopping Mall Example WebSite
            </div>
        </footer>
    </div>
</html>

3. 헤더
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<div th:fragment="header">
    <nav class="navbar navbar-expand-sm bg-primary navbar-dark">
        <button class="navbar-toggler" type="button" data-toggle="collapse"
                data-target="#navbarTogglerDemo03" aria-controls="navbarTogglerDemo03"
                aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <a class="navbar-brand" href="/">Shop</a>

        <div class="collapse navbar-collapse" id="navbarTogglerDemo03">
            <ul class="navbar-nav mr-auto mt-2 mt-lg-0">
                <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
                    <a class="nav-link" href="/admin/item/new">상품 등록</a>
                </li>
                <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
                    <a class="nav-link" href="/admin/items">상품 관리</a>
                </li>
                <li class="nav-item" sec:authorize="isAuthenticated()">
                    <a class="nav-link" href="/cart">장바구니</a>
                </li>
                <li class="nav-item" sec:authorize="isAuthenticated()">
                    <a class="nav-link" href="/orders">구매이력</a>
                </li>
                <li class="nav-item" sec:authorize="isAnonymous()">
                    <a class="nav-link" href="/members/login">로그인</a>
                </li>
                <li class="nav-item" sec:authorize="isAuthenticated()">
                    <a class="nav-link" href="/members/logout">로그아웃</a>
                </li>
            </ul>
            <form class="form-inline my-2 my-lg-0" th:action="@{/}" method="get">
                <input name="searchQuery" class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
                <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
            </form>
        </div>
    </nav>
</div>

</html>
profile
DB가 좋아요

0개의 댓글