Spring

스프링부트 - 타임리프(Thymeleaf)

코린이 파닥거리기 2025. 5. 13. 00:23
728x90
반응형
SMALL

오늘은 스프링부트의 뷰 템플릿 타임리프에 대해서 알아보겠다.

 

뷰 템플릿?

만약 우리가 로그인 시 페이지에 

000님 반갑습니다! 라는 문구가 뜨게 하는데 이것은 html파일을 하나의 사용자마다 따로따로 다 만들까?

그렇지 않다. 그럼 왜 웹 서비스를 여나

그래서 우리는 view템플릿을 사용해서 하나의 페이지에 변수를 동적으로 할당하여 페이지에 나타낸다.

타임리프는 view템플릿엔진 중 하나라고 보면 된다.


타임리프는 서버 사이드 렌더링이라고 한다. 

 

웹 페이지를 사용자에게 보여주는 방식은 크게 서버 사이드 랜더링(SSR) 클라이언트 사이드 렌더링(CSR)

두개가 있다. 


서버 사이드 렌더링?

사용자가 웹 페이지를 요청했을 때, 서버에서 해당 페이지의 모든 내용을 렌더링(HTML 문서 형태로 완전히 만들어서)

클라이언트(웹 브라우저)로 전송하는 방식이다.

  1. 사용자가 브라우저에 URL 요청
  2. 요청을 받은 서버는 DB 등에서 필요한 데이터를 가져와서 가져온 데이터를 기반으로 HTML템플릿 파일을 생성
  3. HTML파일을 → 브라우저로 전송
  4. 브라우저는 HTML파일을 즉시 파싱하여 사용자에게 보여줌

클라이언트 사이드 렌더링?

클라이언트 사이드 렌더링은 웹 페이지의 초기 로드 시 최소한의 HTML(빈 컨테이너)와 JS파일을 클라이언트로 전송하고, 

실제 웹 페이지의 내용은 클라이언트의 웹 브라우저에서 JS코드를 실행하여 동적으로 렌더링 하는 방식이다.

  1. 사용자가 브라우저에 URL 요청
  2. 서버는 최산의 HTML파일과 해당 페이지를 렌더링하는데 필요한 JS파일들을 브라우저로 전송
  3. 브라우저는 HTML을 표시하지만, 동시에 JS파일을 다운로드하고 실행하기 시작
  4. 실행된 JS코드는 서버에 API요청을 보내 필요한 데이터를 비동기(Async)로 가져온다.
  5. 데이터가 로드되면 JS는 서버에서 가져온 데이터를 기반으로 브라우저의 DOM을 조작해 웹 페이지의 내용을 동적으로 생성하고 렌더링하여 사용자에게 보여준다.

이렇게 Thymeleaf는 스프링 부트에서 권장하는 View Template이다.

타임리프는 컨트롤러에서 View로 넘겨준 Model을 이용하여 내용 출력

 

예시를 들어서 이해해보자

 

ThymeleafController.java

    @GetMapping("user")
    // model이 thymeleaf로 전달됨
    public String user(Model model) {
        Map<String, Object> user = new HashMap<>();
        user.put("userId", "z");
        user.put("userName", "zoo");
        user.put("userAge", 25);
        model.addAttribute("user", user);
        return "user";
    }

 

templates/html/user.html

아이디:<span>[[${user.userId}]]</span><br>
이름:<span>[[${user.userName}]]</span><br>
나이:<span>[[${user.userAge}]]</span><br>
<hr>
아이디:<span th:text="${user.userId}"></span><br>
이름:<span th:text="${user.userName}"></span><br>
나이:<span th:text="${user.userAge}"></span><br>
<hr>
<!-- 콜론 앞 단어 - name space  -->
아이디:<span data-th-text="${user.userId}"></span><br>
이름:<span data-th-text="${user.userName}"></span><br>
<!-- %{|으로 감싸면 원하는 원본을 출력가능 -->
나이:<span data-th-text="${user.userAge}"></span><br>

서버에서 받은 데이터를 타임리프로 나타내는 법은 

크게는 3가지가 있다

  • [[  ${나타내고 싶은 데이터 값} ]]
  • th:text="${나타내고 싶은 데이터값}"
  • data-th-text = "${나타내고 싶은 데이터값}"

만약 정적인 값과 같이 표현하고싶으면

  • [[  ${나타내고 싶은 데이터 값} ]]
  • th:text="${나타내고 싶은 데이터값} + 님"
  • data-th-text = "@{|${나타내고 싶은 데이터값}님|}"

이렇게 첫번째 두번째는 거의 다를게 없는데

마지막 표현은 

@{|     |}이라는 파이프라인같이 생긴 놈을 추가해줘야된다.

 

 

그럼 하나의 데이터말고 나타내고 싶은 데이터가 여러개 있으면?

Iteration과 비슷한 th:each를 사용하면된다.

<body>
    <table border="1">
        <tr>
            <td>아이디</td>
            <td>이름</td>
            <td>나이</td>
        </tr>
        <tr th:each="user : ${userList}">
            <td th:text="${user.userId}"></td>
            <td th:text="${user.userName}"></td>
            <td th:text="${user.userAge}"></td>
        </tr>
    </table>
    <hr>
    <th:block th:each="pageNumber : ${#numbers.sequence(1, 10)}">
        <span th:text="${pageNumber}"></span>
    </th:block>
</body>

이렇게 하면 Map에 저장된 값들이 주르륵 나오게 된다.

 

이런 함수 말고도

th:if     →      " "안에서 if문 처럼 사용가능하다.

th:unless     →    if와 정반대를 수행한다.

th:switch |  th:case  →  switch | case문과 동일하다고 보면 된다.


템플릿 레이아웃(Template Layout)

  • 공통으로 사용될 전체 모습을 미리 작성한다.

레이아웃을 사용하려면 일단 의존성 부터 추가를 해줘야한다.

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

요렇게 추가만 해주면 이제 템플릿 레이아웃을 사용가능하다.

<!DOCTYPE html>
<html lang="en">

<head>
    <title>Bootstrap Example</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
    <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
        <div class="container-fluid">
            <a class="navbar-brand" href="javascript:void(0)">Logo</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mynavbar">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="mynavbar">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link" th:href="@{/layout1}">1번 화면</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" th:href="@{/layout2}">2번 화면</a>
                    </li>
                </ul>
                <form class="d-flex">
                    <input class="form-control me-2" type="text" placeholder="Search">
                    <button class="btn btn-primary" type="button">Search</button>
                </form>
            </div>
        </div>
    </nav>
    <th:block layout:fragment="content"></th:block>
</body>

</html>

다음과 같이 레이아웃 html파일을 작성해주고

<html layout:decorate="~{common/layout}">
<div class="container-fluid mt-3" layout:fragment="content">
    <h3>1번 화면</h3>
    <img src="yourimageURL" style="width: 100%">
</div>

</html>

html태그 초기값에 그냥 경로로 불러와주기만 하면 된다.

<html layout:decorate="~{common/layout}">

개발자도구 → Elements → layout칸에 요소 클릭을 하면 볼 수 있다.

이렇게 잘 나오는 것을 알 수 있다


Escape(th:text)

  • 특정 문자(<, > , &, ", ')를 HTML엔티티로 변환해서 출력하는 방식
  • JS 주입 공격(XSS)를 방지하기 위해 사용된다.
  • but, 의도한대로 HTML을 표현하지 못할 수도 있다.

Unescape(th:utext)

  • 특별한 변환없이 원래의 문자를 그대로 출력하는 방식
  • JS 주입 공격(XSS)에 노출된다.
    @GetMapping("/escape")
    public String escape(Model model) {
        String data = "<div><h1>제목</h1><h3>내용</h3></div>";
        model.addAttribute("data", data);
        return "escape";
    }

이렇게 컨트롤러에 data를 설정하고 HTML로 데이터값을 보내주면 어떻게 될까

그대로 나온다.

Unescape는 반대로 HTML엔티티로 변환없이 문자를 그대로 출력한다.

728x90
반응형
LIST