Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Thymeleaf에서 레이아웃 기능을 지원하는 Thymeleaf Layout Dialect

전에 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">
      &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 기능을 그대로 사용할 수 있게 되었다.

2013/12/01 23:33 2013/12/01 23:33