전에 Thymeleaf에 대한 얘기를 올렸었지만 어쨌든 아직 쓰고 있기는 하다. 뷰페이지를 작업하면 어떤 어플리케이션을 만들던지 간에 당연히 헤더나 메뉴같은 공통적인 부분이 생기기 마련이고 이런 공통 부분을 뷰 템플릿엔진에서 공통적으로 처리하기 마련이다. Thymeleaf는 이런 공통 레이아웃 처리를 위해서 Fragment라는 기능을 제공하고 있다.
Fragment
Thymeleaf 문서의 예제를 보면 Fragment를 다음과 같이 사용한다.
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
</html>
위와 같은 별도의 파일을 만들고 copy부분을 fragement로 지정을 한다. 이를 다른 파일에서 다음과 같이 th:include
를 사용해서 불러온다. 물론 여기서 꼭 th:include
만 사용하는 것은 아니고 엘리먼트를 교체한다거나 하는 다양한 방법들이 있지만 사용방법 자체가 크게 달라지지 않는다.
<body>
...
<div th:include="footer :: copy"></div>
</body>
이 기능은 실제로 사용하면 형편없다고 느껴지는데 공통화 부분을 단순히 각 조각별로 분리해 놓은 것에 불과하기 때문이다. 공통화를 처리할 수는 있지만 관리하고 다루기가 어렵다. 위의 예제에서 th:frament
를 정의한 파일의 사실 html태그나 body태그는 아무런 의미도 없는 부분이다. 실제 사용하는 것은 fragment뿐이지만 Thymeleaf의 사상에 따라 각 뷰페이지를 웹브라우저에서 직접 띄워볼 수 있어야 하기 때문에 실제 템플릿 기능에선 필요없는 HTML을 완성시켜놓았을 뿐이다.(하지만 실제로 사용하면 CSS, JavaScript나 다른 요소들에 영향을 받기 때문에 별도의 공통부분만 잘라놓는다고 웹브라우저에서 보는게 의미 있는지 잘 모르겠다.) 신경써서 잘 관리하면 어떻게 될지 모르지만 어쨌든 결과적으로는 한 파일에 fragment만 모아놓은 HTML 파일이 따로 생기게 되고 HTML과 자연스럽게(?) 섞여있어서 주석을 잘 넣어놓지 않으면 fragment가 잘 구분도 안된다.
페이지에서 일부 공통화가 필요한 부분(페이지의 몇몇 파일에서만 같은 부분을 넣어야 한다든지)같은 경우는 이러한 방식이 어울릴 수 있겠지만 전체 레이아웃을 관리하는 것은 무리이다. 전체 레이아웃을 변경해야 한다던지 할 때 fragement만 바꾸어서 영향을 주고 하는 것은 어렵다. fragment가 전체 레아아웃의 모양을 다루는 것이 아니라 파편화된 각 조각을 다루기 때문이다.
Thymeleaf Layout Dialect
그래서 Emanuel Rabina가 만든 Thymeleaf Layout Dialect이라는 레이아웃 관리자를 쓰고 있는데 상당히 흡족해 하고 있다. Thymeleaf 업그래이드에 맞춰서 업데이트도 잘 이뤄지고 있고 현재는 Thymeleaf 2.1에 맞춰진 1.2가 최신 버전이다.
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
<version>1.2</version>
</dependency>
Thymeleaf Layout Dialect를 사용하려면 pom.xml
에서 의존성을 추가해 주어야 한다. 일반적으로 XML을 사용해서 Thymeleaf를 설정하면 다음과 비슷하게 설정해서 사용할 것이다.
<bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
<!-- property here -->
</bean>
<bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
</bean>
여기에 아래와 같이 LayoutDialect를 추가해 주어야 한다.
<bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
<property name="additionalDialects">
<set>
<bean class="nz.net.ultraq.thymeleaf.LayoutDialect"/>
</set>
</property>
</bean>
LayoutDialect 사용방법
LayoutDialect는 상속방식(데코레이터방식이라고 해야하나?)의 레이아웃을 사용하고 있다. 레이아웃용 파일을 만들어 놓고 각 페이지에서 원하는 레이아웃 파일을 상속받아서 필요한 부분만 교체해서 사용한다. Jade가 사용하는 방식과도 유사한데 나한테 익숙해서 인지 몰라도 꽤 유용하다.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<title layout:title-pattern="$CONTENT_TITLE :: $DECORATOR_TITLE">Site Tilte</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link href="style.css" rel="stylesheet" />
</head>
<body>
<header layout:fragment="header">
<h1>Layout Tilte</h1>
</header>
<section layout:fragment="content">
<p>Layout Content</p>
</section>
<footer layout:fragment="footer">
<p>Layout Footer</p>
</footer>
</body>
</html>
위와 같은 layout/commonLayout.html
파일을 만들었다고 해보자. 일반적인 HTML파일이지만 layout속성 사용을 위해서 상단에 xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
를 정의했다.(없어도 상관은 없지만 없으면 보통 IDE에서 오류 표시가 난다.) 이 레이아웃을 상속받은 페이지에서 공통적으로 사용하므로 하위 페이지에서 교체가 필요한 부분은 layout:fragment
로 이름을 지정한다. 하위 페이지에서는 이 fragment 이름을 사용해서 필요한 부분만 교체해서 사용한다.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layout/commonLayout">
<head>
<title>Page Title</title>
<link href="page-style.css" rel="stylesheet" />
</head>
<body>
<section layout:fragment="content">
<p>Page Content</p>
</section>
<footer>
This is notiong...
</footer>
<script layout:fragment="custom-script">
console.log('script for page');
</script>
</body>
</html>
레이아웃을 사용하는 하위페이지이다. 상단에서 layout:decorator="layout/commonLayout"
로 사용할 레이아웃을 지정했다. 이는 컨트롤러에서 템플릿 파일을 지정할 때와 같은 방식으로 지정하면 된다. 하위페이지에서 자신이 사용할 레이아웃파일을 지정하므로 다양한 레이아웃을 만들어 놓고 상황에 따라 불러와서 사용할 수 있다. 이 페이지는 레이아웃과 결합해서 다음과 같이 만들어진다.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Page Title :: Site Tilte</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link href="style.css" rel="stylesheet" />
<link href="page-style.css" rel="stylesheet" />
</head>
<body>
<header>
<h1>Layout Tilte</h1>
</header>
<section>
<p>Page Content</p>
</section>
<footer>
<p>Layout Footer</p>
</footer>
<script src="common-script.js"></script>
<script>
console.log('script for page');
</script>
</body>
</html>
이 만들어진 페이지에서 LayoutDialect의 몇가지 특징을 볼 수 있다.
- 전체 HTML 페이지는 레이아웃 파일을 중심으로 만들어진다. 그래서 하위페이지에서 body태그 안에 fragment를 덮어쓰지 않은 부분은 모두 버리고 레이아웃 파일의 내용만 사용한다.(하위 페이지의 footer는 출력되지 않은 걸 볼 수 있다.)
- 하위 페이지에서
layout:fragment
로 지정한 영역은 레이아웃 파일에서 같은 이름의 fragment 부분 대신 들어간다.(대신 들어갈 때 내용만 바꿀지 해당 태그까지 바꿀지는 사용방법에 따라 다르다.) 위에서 보면 header와 footer는 레이아웃파일에서 출력된 내용이고 content 부분은 하위 페이지의 내용으로 바뀐 것을 볼 수 있다. <head>
태그는 별도의 fragement 지정을 하지 않아도 LayoutDialect이 자동으로 처리한다. 즉, 레이아웃의 헤더하위에 title을 제외한 하위 페이지의 헤더부분을 추가한다. 위에서 보면 레이아웃에서 지정한style.css
아래page-style.css
가 추가된 것을 볼 수 있다.- 레이아웃 파일에서 타이틀에
layout:title-pattern="$CONTENT_TITLE :: $DECORATOR_TITLE"
와 같이 사용해서 페이지의 title을 원하는 모양으로 만들어 낼 수 있다.layout:title-pattern
으로 지정하는데$CONTENT_TITLE
은 하위 페이지의 타이틀 명을 의미하고$DECORATOR_TITLE
은 레이아웃의 타이틀 명을 의미한다. 그래서 만들어진 페이지 타이틀이<title>Page Title :: Site Tilte</title>
가 된다.
맺음말
LayoutDialect은 간단하면서도 레이아웃을 다루기가 많이 편하다. 좀 써보고 나니 Thymeleaf 자체의 fragement 기능이 더 형편없게 느껴져서 LayoutDialect기능을 별도의 Dialect로 관리할게 아니라 Thymeleaf의 포함된 기능으로 다루어야 한다고 생각될 정도이다. 이것만으로도 Thymeleaf를 사용하는 편의성이 엄청나게 커졌다. LayoutDialect를 쓸 경우 Thymeleaf의 사상인 뷰파일만 브라우저에서 띄웠을때 볼 수 있어야 한다는 장점(?)이 동작안하는 문제가 있었는데 이 부분도 얼마전에 해결되었다. Thymeleaf가 fragment에서 사용하는 방식과 동일하게 Thymeleaf Layout Dialect JS가 만들어져서(아직 사용은 안해봤다.) 오프라인에서도 LayoutDialect 기능을 그대로 사용할 수 있게 되었다.
너무 상세하기 설명해주셔서 감사합니다! 많은 도움이 되었습니다