티스토리 뷰

728x90
반응형

Spring MVC 패턴에서 View Template Engine(뷰 템플릿 엔진)으로 보통 Thymeleaf(타임리프)가 자주 쓰인다.

그 이유로는 아래 4가지가 있을 수 있다.

  • JSP와 유사하게 ${}을 별도의 처리 없이 이용할 수 있다.
  • Model에 담긴 객체를 화면에서 JavaScript로 처리하기 편리하다.
  • 연산이나 포맷과 관련된 기능을 추가적인 개발 없이 지원한다.
  • 개발 도구를 이용할 때 .html 파일로 생성하는데 문제가 없고 별도의 확장자를 이용하지 않는다.

본 포스팅은 Thymeleaf를 쓸 때마다 여기저기서 문법을 찾기가 매우 번거로워 자주 쓰는 구문들을 정리하기 위한 목적이기 때문에 Thymeleaf를 쓰는 이유나 장단점을 알아보기 보단 바로 본론으로 들어간다.

Goals

  • 5가지 기본 표현식
  • 자주 쓰는 구문 정리

 

5가지 기본 표현식


Thymeleaf에서는 값을 표현하는 방법이 5가지다.

  • ${...} : Variable expressions.
  • *{...} : Selection expressions.
  • #{...} : Message (i18n) expressions.
  • @{...} : Link (URL) expressions.
  • ~{...} : Fragment expressions.

 

${...} : Variable expressions

Variable expressions are OGNL expressions –or Spring EL if you’re integrating Thymeleaf with Spring– executed on the context variables — also called model attributes in Spring jargon.

변수 표현식은 컨텍스트 변수에서 실행되는 OGNL 표현식(또는 Thymeleaf를 Spring과 통합하는 경우 Spring EL)이며 Spring 전문 용어로 모델 속성이라고도 합니다.

 

쉽게 말해 Model 객체의 속성 값에 접근하거나, 삼항 연산자같은 간단한 식들도 표현할 수 있는 표현식이다.

(+Thymeleaf에서 제공하는 기본 객체(#numbers, #dates 등), fragment 등에 접근할 때도 쓰임)

Controller의 GET 매핑 메서드 중, 파라미터로 선언한 Model 객체에 View에서 사용하고 싶은 객체를 속성 값으로 지정해 주면, 이 표현식으로 해당 객체에 접근하는 것이다.

예를 들어 아래와 같이 MemberController에서 list라는 메서드가 있다고 해보자.

@Controller
public class MemberController {
    
    private final MemberService memberService;

    // ...

    @GetMapping(value = "/members")
    public String list(Model model){

				Long memberId = 1;

        Member member = memberService.findById(memberId);
        model.addAttribute("member", member);
        
        return "/members/list";
    }
}

Model 객체에 "member"라는 이름으로 member 객체를 속성 값으로 추가했다. 이를 Thymeleaf 템플릿에서 그대로 사용할 수 있다.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
	<p th:text="${member.id}"></p>
	<p th:text="${member.name}"></p>
</body>
</html>

 

*{...} : Selection expressions

Selection expressions are just like variable expressions, except they will be executed on a previously selected object instead of the whole context variables map.

선택 표현식은 전체 컨텍스트 변수 map 대신 이전에 선택한 객체에서 실행된다는 점을 제외하면, 변수 표현식과 같습니다.

 

사실 ${}와 거의 동일하다고 보면 되는데, 사용하기 위해서는 한 가지 전제 조건이 붙는다. Model 객체에 속성 값으로 다양한 객체가 담겨 있을 때, 하나의 객체를 먼저 지정하면 지정된 객체의 속성 이름만으로 접근할 수 있다.

예시를 보자.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
	<div th:object="${member}">
		<p th:text="*{id}"></p>
		<p th:text="*{name}"></p>
	</div>
</body>
</html>

th:object="${member}"로 먼저 어떤 속성 값을 사용할 지 미리 지정했다.

즉, member 컨텍스트 변수를 “선택”했다는 의미로, member.id로 접근하는 것이 아닌 객체 이름을 생략한 id로만 접근할 수 있다.

 

#{...} : Message (i18n) expressions

Message expressions (often called text externalization, internationalization or i18n) allows us to retrieve locale-specific messages from external sources (.properties files), referencing them by a key and (optionally) applying a set of parameters.

메시지 표현식(종종 텍스트 외부화, 국제화 또는 i18n이라고 함)을 사용하면 외부 소스(.properties 파일)에서 로케일별 메시지를 검색하여 키로 참조하고 (선택적으로) 매개변수 집합을 적용할 수 있습니다.

 

Spring에서 국제화(다국어 처리)를 위해 로케일별 message.properties 파일을 만들기도 한다.

이 파일 안에 담겨 있는 메세지 변수를 참조할 때 사용한다고 생각하면 된다.

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
    <div>
        <span th:text="#{welcome.message}"/>
    </div>
</body>
</html>

 

@{...} : Link (URL) expressions

Link expressions are meant to build URLs and add useful context and session info to them (a process usually called URL rewriting).

링크 식은 URL을 빌드하고 유용한 컨텍스트 및 세션 정보를 URL에 추가하기 위한 것입니다(일반적으로 URL 재작성이라고 하는 프로세스).

 

링크 처리를 할 때 사용되며, 특히 파라미터를 전달해야 하는 상황에서 좀 더 가독성 좋은 코드를 만들 수 있다.

예를 들어 /order/details이란 URL에 id 값과 type 값을 GET 파라미터로 전달해야 할 때,

<a th:href="@{/order/details(id=${orderId},type=${orderType})}">...</a>

위와 같이 () 안에 key=vaule 형태로 파라미터 값을 추가할 수 있다.

<!-- HTML로 랜더링 후 예시 -->
<a href="/myapp/order/details?id=23;type=online">...</a>

또한 위 id 값을 path에 사용하고 싶으면 {}에 key를 적어주면 된다.

<a th:href="@{/order/details/{id}(id=${orderId},type=${orderType})}">...</a>
<!-- HTML로 랜더링 후 예시 -->
<a href="/myapp/order/details/23?type=online">...</a>

 

~{...} : Fragment expressions

Fragment expressions are an easy way to represent fragments of markup and move them around templates. Thanks to these expressions, fragments can be replicated, passed to other templates as arguments, and so on.

Fragment(프래그먼트) 식은 마크업의 fragment를 표시하고 템플릿에서 이동하는 쉬운 방법입니다. 이러한 식 덕분에, fragment를 복제하고 인수로 다른 템플릿에 전달할 수 있습니다.

 

fragment라는 HTML 조각 파일들을 가져올 수 있는 표현 식이다. 보통 특정 부분을 다른 내용으로 변경할 수 있는 th:insertth:replace와 같이 사용한다.

<div th:insert="~{/commons :: main}">...</div>

위의 경우 commons라는 파일의 main이라는 fragment를 가져 오겠다는 의미다. 이에 대한 자세한 설명은 아래 자주 쓰는 구문 정리에서 다루겠다.

 

자주 쓰는 구문 정리


지금부터는 실제로 Thymeleaf를 사용하면서 자주 썼던 구문들을 간단한 예시와 함께 정리해 보겠다.

 

텍스트 출력 : th:text

HTML 파일에 텍스트를 출력하는 방법이다.

<p th:text="#{home.welcome}">Welcome !</p>

재밌는 점은 만약 th:text의 값(위의 경우에는 home.welcome)이 null이면 “Welcome !”을 출력하고, null이 아니면 home.welcome 값을 출력하게 된다.

 

인라인(inline) 표현식

태그 속성을 사용하지 않고 HTML 텍스트에 직접 표현식을 작성할 수 있다.

원래는 아래와 같은 식을

<p>Hello, <span th:text="${session.user.name}">Sebastian</span>!</p>

다음과 같이 바꿀 수 있다.

<p>Hello, [[${session.user.name}]]!</p>

또한 JavaScript나 CSS에서도 인라인 표현식을 사용할 수 있는 방법이 있다.

<script th:inline="javascript">
    ...
    var username = [[${session.user.name}]];
    ...
</script>

위와 같이 script 태그 안에 th:inline="javascript" 속성을 추가해 주면 된다.

CSS도 동일하다.

<style th:inline="css">
    .[[${classname}]] {
      text-align: [[${align}]];
    }
</style>

 

반복문 : th:each

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <table>
        <thead>
            <tr>
                <th>#</th>
                <th>이름</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="member, state : ${members}">
                <td th:text="${state.index}"></td>
                            <td th:text="${member.id}"></td>
                <td th:text="${member.name}"></td>
            </tr>
        </tbody>
    </table>
</body>
</html>

members라는 객체 리스트를 순회하며 각 객체를 member라는 이름으로 참조해 사용할 수 있다.

또한 부가적으로 사용할 수 있는 상태(state) 객체라는 것이 있다. 위 예제에서는 state라는 변수명으로 지정했는데, 변수명은 크게 상관 없다. 이 상태 객체는 인덱스 번호, 홀수, 짝수 등의 정보를 가지고 있다.

 

제어문 : th:if, th:unless, 삼항 연산자

Thymeleaf에서는 if ~ else를 한 묶음으로 처리하지 않고 따로따로 처리한다.

예를 들어 ‘sno 값이 5의 배수일 때만 출력하라’는 구문이다.

<li th:each="dto : ${dtoList}">
    <span th:if="${dto.sno % 5 == 0}" th:text="{dto.sno}"></span>
</li>

‘위 조건이 아닐 때(sno 값이 5의 배수가 아닐 때)는 *을 출력하라’는 구문을 아래와 같이 추가할 수 있다.

<li th:each="dto : ${dtoList}">
    <span th:if="${dto.sno % 5 == 0}" th:text="{dto.sno}"></span>
    <span th:unless="${dto.sno % 5 == 0}" th:text="*"></span>
</li>

위 두 구문을 삼항 연산자를 활용해 하나의 구문으로 만들 수 있다.

<li th:each="dto : ${dtoList}">
    <span th:text="{dto.sno % 5 == 0 ? dto.sno : '*'}"></span>
</li>

위와 같이 삼항 연산자는 표현식 안에 활용할 수 있으므로 훨씬 편리하다.

 

th:block

별도의 태그가 필요없는 구문으로 개발자가 원하는 속성을 지정할 수 있는 단순한 속성 컨테이너다. 실제 화면에서는 html로 처리되지 않기 때문에 반복문 등을 별도로 처리할 때 많이 사용된다.

위 제어문 예시에서 사용하면 아래와 같다.

<th:block="dto : ${dtoList}">
    <li th:text="{dto.sno % 5 == 0 ? dto.sno : '*'}"></li>
</th:block>

 

레이아웃 처리 : th:insert, th:replace, th:fragment

Thymeleaf에서 레이아웃을 처리하기 위한 기본적인 방법은 먼저 포함하고 싶은 부분을 fragment로 정의해야 한다.

예를 들어 /templates/fragments/fragment1.html 파일이 아래와 같다고 해보자.

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

  <body>
  
    <div th:fragment="part1">
      <h2>Part 1</h2>
    </div>
		<div th:fragment="part2">
      <h2>Part 2</h2>
    </div>
  
  </body>
  
</html>

이를 다른 html 파일에서 fragment1.html 조각들을 가져오는 방법은 아래와 같다.

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

    <body>

    <div th:insert="~{/fragments/fragment1 :: part1}"></div>
        <div th:replace="~{/fragments/fragment1 :: part2}"></div>

    </body>
  
</html>

위 코드가 랜더링 된 html 파일을 보자.

...

    <body>

    <div>
        <div>
            <h2>Part 1</h2>
        </div>
        </div>
        <div>
            <h2>Part 2</h2>
        </div>

    </body>

...

th:insert와  th:replace의 차이는 다음과 같다.

  • th:insert : 바깥쪽 태그는 그대로 유지하면서 새롭게 추가되는 방식
  • th:replace : 기존 내용을 완전히 대체하는 방식

또한 fragment를 정의하지 않아도 다른 파일의 조각을 불러올 수 있다.

~{::}에서 ::뒤에 CSS 선택자를 이용할 수 있다. 만약 footer.html 파일의 내용 중 아래와 같은 코드가 작성되어 있다면,

<div id="copy-section">
  &copy; 2011 The Good Thymes Virtual Grocery
</div>

CSS 선택자 중 id 속성 값을 가져오는 #을 사용하여 html 조각을 include 할 수 있다.

<body>

  ...

  <div th:insert="~{footer :: #copy-section}"></div>

</body>

참고로 fragment 표현식을 사용하는 방법은 아래와 같다.

  • "~{templatename::selector}" : templatename 파일 안에 있는 Markup Selector 조각을 가져온다.
  • "~{templatename}" : templatename 파일 전체를 가져온다.
  • "~{::selector}" or "~{this::selector}" : 현재 파일 안에 있는 Markup Selector 조각을 가져온다.

마지막으로 fragment 표현식에는 파일 조각을 파라미터 값으로 전달할 수 있다.

/templates/fragments/fg2.html

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

    <body>

        <div th:fragment="target(first, second)">
            <div th:replace="${first}"></div>
            <div th:replace="${second}"></div>
        </div>

    </body>
  
</html>

fg2.html 파일의 fragment를 불러와 보자.

/templates/layout.html

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

    <body>

        <th:block th:replace="~{/fragments/fg2 :: target(~{this :: hello}, ~{this :: everyone})}">
            <h2 th:fragment="hello">Hello, </h2>
            <h2 th:fragment="everyone">everyone!</h2>
        <th:block>

    </body>
  
</html>

위 파일의 렌더링 결과를 예측해 보자.

먼저 fg2.htmltarget fragment가 layout.html에 include될 것이다.

이때, ${first}~{this :: hello}로, ${first}~{this :: everyone}로 치환된다고 생각하면 좀 편하다.

결과는 아래와 같다.

/templates/layout.html

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

    <body>

        <div>
            <h2">Hello, </h2>
            <h2>everyone!</h2>
        </div>

    </body>
  
</html>

어떤 파라미터에 값을 전달하고 싶은지 명확하게 지정할 수도 있다.

/templates/layout.html

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

    <body>

        <th:block th:replace="~{/fragments/fg2 :: target(second = ~{this :: hello}, first = ~{this :: everyone})}">
            <h2 th:fragment="hello">Hello, </h2>
            <h2 th:fragment="everyone">everyone!</h2>
        <th:block>

    </body>
  
</html>

렌더링 된 /templates/layout.html

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

    <body>

        <div>
            <h2">everyone!</h2>
            <h2>Hello, </h2>
        </div>

    </body>
  
</html>

 

References

728x90
반응형
댓글