오늘은 스프링부트의 뷰 템플릿 타임리프에 대해서 알아보겠다.
뷰 템플릿?
만약 우리가 로그인 시 페이지에
000님 반갑습니다! 라는 문구가 뜨게 하는데 이것은 html파일을 하나의 사용자마다 따로따로 다 만들까?
그렇지 않다. 그럼 왜 웹 서비스를 여나
그래서 우리는 view템플릿을 사용해서 하나의 페이지에 변수를 동적으로 할당하여 페이지에 나타낸다.
타임리프는 view템플릿엔진 중 하나라고 보면 된다.
타임리프는 서버 사이드 렌더링이라고 한다.
웹 페이지를 사용자에게 보여주는 방식은 크게 서버 사이드 랜더링(SSR) 클라이언트 사이드 렌더링(CSR)
두개가 있다.
서버 사이드 렌더링?
사용자가 웹 페이지를 요청했을 때, 서버에서 해당 페이지의 모든 내용을 렌더링(HTML 문서 형태로 완전히 만들어서)
클라이언트(웹 브라우저)로 전송하는 방식이다.
- 사용자가 브라우저에 URL 요청
- 요청을 받은 서버는 DB 등에서 필요한 데이터를 가져와서 가져온 데이터를 기반으로 HTML템플릿 파일을 생성
- HTML파일을 → 브라우저로 전송
- 브라우저는 HTML파일을 즉시 파싱하여 사용자에게 보여줌
클라이언트 사이드 렌더링?
클라이언트 사이드 렌더링은 웹 페이지의 초기 로드 시 최소한의 HTML(빈 컨테이너)와 JS파일을 클라이언트로 전송하고,
실제 웹 페이지의 내용은 클라이언트의 웹 브라우저에서 JS코드를 실행하여 동적으로 렌더링 하는 방식이다.
- 사용자가 브라우저에 URL 요청
- 서버는 최산의 HTML파일과 해당 페이지를 렌더링하는데 필요한 JS파일들을 브라우저로 전송
- 브라우저는 HTML을 표시하지만, 동시에 JS파일을 다운로드하고 실행하기 시작
- 실행된 JS코드는 서버에 API요청을 보내 필요한 데이터를 비동기(Async)로 가져온다.
- 데이터가 로드되면 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엔티티로 변환없이 문자를 그대로 출력한다.