0%

Springboot에 thymeleaf-layout-dialect적용하기

들어가기

springboot 프로젝트에 thymeleaf-layout-dialectt이라는 라이브러리를 사용해서 thymeleaf 템플릿 엔진에 레이아웃을 적용해 보겠다.

보통 웹 페이지를 만들면 화면을 header, contents, footer 등으로 나누고 header와 footer는 고정한 뒤 contents 영역만 바꿔가면서 렌더링을 해야 한다.

thymeleaf-layout-dialect 라이브러리가 thymeleaf에서 이런 레이아웃을 가능하게 도와준다.

주의

실습 마지막 부분에, 오래된 문법 교체작업이 있다.

실습환경

  • windows10
  • intellij comunity
  • jdk1.8 (2020-05-21 기준으로 jdk 1.8을 추천한다.)
  • thymeleaf-layout-dialect 2.4.1
  • springboot 2.3.0
  • gradle

목표

아래처럼 header, footer는 고정하고, page url에 따라서 contents 영역만 바뀌게 하는 것이 목표이다.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled.png

실습

springboot 프로젝트 만들기

아래 spring initializr 페이지로 이동해서 springboot 프로젝트를 만들어보자.

https://start.spring.io/index.html

디펜던시에 Spring Web, Thymeleaf 두개를 추가해주자.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%201.png

일단 thymeleaf 가 잘 동작하는지 확인해 보기 위해서, HomeContoller를 추가하자.

HomeController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package hanumoka.example.springbootthymeleaflayout;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HomeController {

@RequestMapping("/")
public String home(){
return "index";
}

}

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%202.png

앞서 프로젝트 생성시 thymeleaf를 추가했기때문에, 컨트롤러가 리턴하는 String은 프로젝트의 resources/templates 폴더 내부의 html파일과 바인딩이 된다.

resources/templates 폴더에 index.html 파일을 만들오 내용을 아래처럼 채우자.

index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>springboot thymeleaf layout example</title>
</head>
<body>
index 페이지
</body>
</html>

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%203.png

이제 프로젝트를 기동시켜서 thymeleaf가 잘 동작하는지 확인해보자.

프로젝트를 기동하고, 브라우저에서 루트 경로로 접속하면 앞서 생성한 index.html 페이지가 렌더링 되는 것을 확인 할 수 있다.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%204.png

Springboot에 thymeleaf dialect layout 라이브러리를 적용해서 thymeleaf 레이아웃을 구성해보자.

메이븐 리파지토리에서 thymeleaf 를 검색해보자.

https://mvnrepository.com/

다섯번째 정도에 Thymeleaf Layout dialect가 보인다.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%205.png

들어가서 2020-05-21 기준 최신버전인 2.4.1의 gradle 방식의 의존성 스크립트를 복사한다.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%206.png

프로젝트의 build.gradle에 해당 내용을 추가해준다.

build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
plugins {
id 'org.springframework.boot' version '2.3.0.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}

group = 'hanumoka.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect', version: '2.4.1'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}

test {
useJUnitPlatform()
}

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%207.png

gradle에 관해서 잘 모르지만, compile방식은 구식이라 implementation구문으로 변경 했다.

주의, jdk가 1.8보다 높은 경우 thymeleaf-layout-dialect에서 groovy관련 문제가 발생할 수 있다. jdk1.8을 추천한다.

jdk11 + thymeleaf-layout-dialect 2.4.1 적용시 오류 내용

1
2
3
4
5
6
WARNING: An inllegal reflective access operation has occurred
WARNING: Illegal reflective access by org.codehaus.groovy.reflection.CachedClass ~
WARNING: Please consider reporting this to the maintainers of org.codehaus.grovy.refleaction.CachedClass
WARNING: Use --illegal-access=warn to enable warnings of fucther inllegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
WARNING: An inllegal reflective access operation has occurred

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%208.png

jdk11에서 위처럼 경고가 나면서, 서버가 동작 될때도 있었고 아닐때도 있었다.

jdk1.8로 변경 후 문제가 해결되었다.

thymeleaf-layout-dialect로 thymeleaf 레이아웃 셋팅하기

아래처럼 templates폴더 내부에 fragments, layouts폴더와 html파일들을 만들어주자.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%209.png

layouts폴더 내부의 html파일은 말그대로 레이아웃으로 사용할 html 파일이다.

fragments폴더 내부의 파일들은 layout파일에서 공통적이며 반복적으로 사용 할 레이아웃 구성요소 html파일들이다.

templates/layouts/layout.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html
lagn="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
>
<head th:replace="fragments/config :: configFragment"></head>

<header th:replace="fragments/header :: headerFragment"></header>

<div layout:fragment="content"></div>

<footer th:replace="fragments/footer :: footerFragment"></footer>
</html>

레이아웃 html 페이지에서는 fragments폴더 내부의 html 들을 부속으로 호출한다.

config, header, footer 들은 레이아웃으 공통 구성 요소이고,

아래 부분이 실제 page url에 따라 바뀔 부분이다.

1
<div layout:fragment="content"></div>

templates/fragments/config.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
>
<head th:fragment="configFragment">
<meta charset="UTF-8" />
<!-- 공통으로 쓰이는 css파일을넣는다.-->
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
/>
<!-- 컨텐츠페이지의 CSS 영역이 들어감 -->
<th:block layout:fragment="css"></th:block>

<!-- 공통으로 쓰이는 css파일을넣는다.-->
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<!-- 컨텐츠페이지의 스크립트 영역이 들어감 -->
<th:block layout:fragment="script"></th:block>
</head>
</html>

config.html은 렌더링 되는 페이지에서 공통으로 사용할 css, script등을 설정한다.

아래부분에서 라우팅 될 contents 페이지에 설정된 css, script등을 역으로 받아온다.

1
2
3
4
<!-- 컨텐츠페이지의 CSS 영역이 들어감 -->
<th:block layout:fragment="css"></th:block>
<!-- 컨텐츠페이지의 스크립트 영역이 들어감 -->
<th:block layout:fragment="script"></th:block>

위 부분은 나중에 페이지가 동작하는 것을 보고 더 설명 하겠다.

templates/fragments/header.html

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<header th:fragment="headerFragment">
<div style="border: 1px solid green">
header.html 입니다.
</div>
</header>
</html>

templates/fragments/footer.html

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<footer th:fragment="footerFragment">
<div style="border: 1px solid gold">
Footer영역입니다.
</div>
</footer>
</html>

templates/index.html(컨텐츠 페이지)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layouts/layout"
>
<!-- index.html 고유 CSS 추가 -->
<th:block layout:fragment="css"> </th:block>

<!-- index.html 고유 스크립트 추가 -->
<th:block layout:fragment="script"> </th:block>

<div layout:fragment="content">
<div style="border: 1px solid red">
index.html 입니다. 즉 컨텐츠 영역
</div>
</div>
</html>

templates/hello.html(컨텐츠 페이지)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layouts/layout"
>
<!-- hello.html 고유 CSS 추가 -->
<th:block layout:fragment="css"> </th:block>

<!-- hello.html 고유 스크립트 추가 -->
<th:block layout:fragment="script">
<script>
alert("hello.html");
</script>
</th:block>

<div layout:fragment="content">
<div style="border: 1px solid red">
hello.html 입니다. 즉 컨텐츠 영역
</div>
</div>
</html>

index.html, hello.html 은 컨텐츠 페이지다. 대충 코드를 보면 감이 올것이다.

아래처럼 index.html의 layout:fragment=”content” 영역이 layout.html의 layout:fragment=”content”에 치환되는 방식이다.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%2010.png

위에서 fragments/config 부분을 설명하다 말았는데, 여기서 보자

아래 이미지를 보면 이해가 될 것이다. hello.html 이 렌더링 되면 아래처럼 조립이 된다.

즉 config.html 파일을 통해서, contents 페이지 고유의 css, script등을 고립 시킬 수 가 있다.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%2011.png

hello.html을 리다이렉트 할 메소드도 추가해주자.

HomeController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package hanumoka.example.springbootthymeleaflayout;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HomeController {

@RequestMapping("/")
public String home(){
return "index";
}

@RequestMapping("/hello")
public String hello(){
return "hello";
}

}

java코드는 레이아웃을 적용한다고 해도 딱히 문법적으로 바뀌는 부분이 없다.

이제 프로젝트를 기동시키고 http://localhost:8080/, http://localhost:8080/hello 페이지 url 접근해서 제대로 동작하는 지 확인 해보자.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%2012.png

hello에서는 javascript 코드인 alert 창이 동작한다.(window.onload 코드를 생략해서 alert이 먼저 동작 했다.)

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%2013.png

추가

위 예제로 정상 동작 하지만, 일부 문법이 deprecated 될거라는 WARN이 발생한다.

1
2
3
2020-05-21 22:28:23.122  WARN 15980 --- [nio-8080-exec-1] n.n.u.t.decorators.DecoratorProcessor    : The layout:decorator/data-layout-decorator processor has been deprecated and will be removed in the next major version of the layout dialect.  Please use layout:decorate/data-layout-decorate instead to future-proof your code.  See https://github.com/ultraq/thymeleaf-layout-dialect/issues/95 for more information.
2020-05-21 22:28:23.207 WARN 15980 --- [nio-8080-exec-1] n.n.u.t.expressions.ExpressionProcessor : Fragment expression "layouts/layout" is being wrapped as a Thymeleaf 3 fragment expression (~{...}) for backwards compatibility purposes. This wrapping will be dropped in the next major version of the expression processor, so please rewrite as a Thymeleaf 3 fragment expression to future-proof your code. See https://github.com/thymeleaf/thymeleaf/issues/451 for more information.
2020-05-21 22:28:23.373 WARN 15980 --- [nio-8080-exec-1] n.n.u.t.fragments.FragmentProcessor : You don't need to put the layout:fragment/data-layout-fragment attribute into the <head> section - the decoration process will automatically copy the <head> section of your content templates into your layout page.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%2014.png

음… 위 예제는 몇 년전에 내가 만든 기준으로 해서 문법 변경이 조금 있었나 보다.

빠르게 수정해보자.

일단 기존에 사용하던 config.html 파일을 이제 사용할 필요가 없다.

layout.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html
lagn="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
>
<head>
<meta charset="UTF-8" />
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
/>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>

<header th:replace="fragments/header :: headerFragment"></header>

<div layout:fragment="content"></div>

<footer th:replace="fragments/footer :: footerFragment"></footer>
</html>

fragments/config 부분을 지워주고, config.html에 있던 공통 css, script 등을 layout.html head태그로 옮긴다.

아래 이미지 처럼 contents.html의 head태그가 렌더링 될 때, 자동으로 layout.html head 태그에 추가 된다.(치환이 아니라 추가 된다.)

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%2015.png

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout}"
>
<head>
<!-- thymeleaf layout dialect가 contents페이지의 head태그를 layout의 head태그에 자동으로 추가해준다. -->
<!-- 필요힌 css, script 추가영역 -->
</head>

<div layout:fragment="content">
<div style="border: 1px solid red">
index.html 입니다. 즉 컨텐츠 영역
</div>
</div>
</html>

contents html 페이지에서는 layout:decorator=”layouts/layout” 설정도 layout:decorate=”~{layouts/layout}” 이렇게 변경 되었다.

수정하고 확인해보면, 정상동작 아래처럼 잘 동작 한다.

Springboot%20thymeleaf%20layout%20dialect%20f1913625c1b946eca3591005c7e07316/Untitled%2016.png

github 소스

https://github.com/blog-examples/20200520-springboot-tymeleaf-dialect

참고자료

https://ultraq.github.io/thymeleaf-layout-dialect/Examples.html#passing-data-to-layouts