kinggora 2022. 8. 24. 16:29

템플릿 레이아웃1

이전에는 일부 코드 조각을 가지고 와서 사용했다면, 이번에는 개념을 더 확장해서 코드 조각을 레이아웃에 넘겨서 사용하는 방법에 대해서 알아보자.

예를 들어서 <head>에 공통으로 사용하는 css, javascript 같은 정보들은 한 곳에 모아두고 공통으로 사용하지만, 각 페이지마다 필요한 정보를 더 추가해서 사용하고 싶을 때, 레이아웃 틀로 대체하는 대신 페이지마다 다른 정보에 대한 코드 조각(단순 값이 아닌 태그 자체)을 넘겨서 사용하는 방식이다.

 

> localhost:8080/template/layout 을 접속하면 매핑되는 HTML

th:replace 태그가 달린 head가 통째로 template/layout/base 의 common_header(fragment)로 대체된다. 

이 때, 파라미터로 넘긴 title, link 태그만 레이아웃에 반영!

=> common_header(~{::title},~{::link})

  ::title 은 현재 페이지의 title 태그들을 전달한다.

  ::link 는 현재 페이지의 link 태그들을 전달한다.

 

<head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
    <title>메인 타이틀</title>
    <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
    <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>

 

> template/layout/base.html

 

<head th:fragment="common_header(title,links)">
    <title th:replace="${title}">레이아웃 타이틀</title>
    <!-- 공통 -->
    <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
    <link rel="shortcut icon" th:href="@{/images/favicon.ico}">
    <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>
 <!-- 추가 -->
 <th:block th:replace="${links}" />
</head>

 

파라미터로 받은 태그를 변수에 담아 (title, links) 레이아웃에 반영시킨다.

 

작동 방식

공통 레이아웃 (공통 부분 + 개별 부분을 추가할 replace 태그)

URL 매핑 -> 공통 레이아웃을 호출하고, 개별 정보를 포함하는 HTML

-> 공통 레이아웃 호출 (개별 부분 파라미터로 넘김) -> 공통 레이아웃 기반 + 개별 부분으로 갈아끼워진 형태로 렌더링 

템플릿 레이아웃2

앞서 이야기한 개념을 <head> 정도에만 적용하는게 아니라 <html> 전체에 적용할 수도 있다.

태그를 전달하고 갈아 끼우는 것은 템플릿 레이아웃1 과 동일하게 적용된다.

2. 타임리프 - 스프링 통합과 폼

타임리프 스프링 통합

기본 메뉴얼: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

스프링 통합 메뉴얼: https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

 

타임리프는 스프링 없이도 동작하지만, 스프링과 통합을 위한 다양한 기능을 편리하게 제공한다. 그리고 이런 부분은 스프링으로 백엔드를 개발하는 개발자 입장에서 타임리프를 선택하는 하나의 이유가 된다.

 

스프링 통합으로 추가되는 기능들

  • 스프링의 SpringEL 문법 통합
  • ${@myBean.doSomething()} 처럼 스프링 빈 호출 지원
  • 편리한 폼 관리를 위한 추가 속성: th:object (기능 강화, 폼 커맨드 객체 선택) , th:field , th:errors , th:errorclass
  • 폼 컴포넌트 기능: checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원
  • 스프링의 메시지, 국제화 기능의 편리한 통합
  • 스프링의 검증, 오류 처리 통합 스프링의 변환 서비스 통합(ConversionService)

설정 방법

타임리프 템플릿 엔진을 스프링 빈에 등록하고, 타임리프용 뷰 리졸버를 스프링 빈으로 등록하는 방법 https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#the-springstandard-dialect https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#views-and-view-resolvers

 

스프링 부트는 이런 부분을 모두 자동화 해준다.

build.gradle 에 다음 한줄을 넣어주면 Gradle은 타임리프와 관련된 라이브러리를 다운로드 받고, 스프링 부트는 앞서 설명한 타임리프와 관련된 설정용 스프링 빈을 자동으로 등록해준다.

 

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

 

타임리프 관련 설정을 변경하고 싶으면 다음을 참고해서 application.properties 에 추가하면 된다.

스프링 부트가 제공하는 타임리프 설정, thymeleaf 검색 필요

https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties-templating

입력 폼 처리

  • th:object : 커맨드 객체를 지정. 해당 오브젝트 정보를 modelAttribute로 넘겨주어야 함
  • *{...} : 선택 변수 식. th:object 에서 선택한 객체에 접근. ${item.itemName} -> *{itemName}
  • th:field : HTML 태그의 id , name , value 속성을 자동으로 처리. 즉, 각 속성을 지워도 자동 생성. 속성으로 지정한 변수의 이름과 값을 사용함

 

렌더링 전

<input type="text" th:field="*{itemName}" />

렌더링 후

<input type="text" id="itemName" name="itemName" th:value="*{itemName}" />

 

이것의 진짜 위력은 뒤에 설명할 검증(Validation)에서 나타난다.

요구사항 추가

타임리프를 사용해서 폼에서 체크박스, 라디오 버튼, 셀렉트 박스를 편리하게 사용하는 방법을 학습해보자.

기존 상품 서비스에 다음 요구사항이 추가되었다.

 

판매 여부

판매 오픈 여부 (체크 박스)

 

등록 지역

서울, 부산, 제주 (체크 박스/다중 선택)

 

상품 종류

도서, 식품, 기타 (라디오 버튼/하나만 선택)

 

배송 방식

빠른 배송, 일반 배송, 느린 배송 (셀렉트 박스/하나만 선택)

 

예시 이미지

체크 박스 - 단일1 (open)

단순 HTML 체크 박스

<!-- single checkbox -->
<div>판매 여부</div>
<div>
  <div class="form-check">
    <input type="checkbox" id="open" name="open" class="form-check-input">
    <label for="open" class="form-check-label">판매 오픈</label>
  </div>
</div>

 

체크 박스를 체크하면 HTML Form에서 open=on 이라는 값이 넘어간다. 스프링은 on 이라는 문자를 true 타입으로 변환해준다. (스프링 타입 컨버터가 이 기능을 수행하는데, 뒤에서 설명)

주의 - 체크 박스를 선택하지 않을 때

HTML에서 체크 박스를 선택하지 않고 폼을 전송하면 open 이라는 필드 자체가 서버로 전송되지 않는다.

수정의 경우, 상황에 따라서 이 방식이 문제가 될 수 있다. 사용자가 의도적으로 체크되어 있던 값을 체크를 해제해도 저장시 아무 값도 넘어가지 않기 때문에, 서버 구현에 따라서 값이 오지 않은 것으로 판단해서 값을 변경하지 않을 수도 있다.

 

이런 문제를 해결하기 위해서 스프링 MVC는 약간의 트릭을 사용하는데, 히든 필드를 하나 만들어서, _open 처럼 기존 체크 박스 이름 앞에 언더스코어( _ )를 붙여서 전송하면 체크를 해제했다고 인식할 수 있다. 히든 필드는 항상 전송된다. 따라서 체크를 해제한 경우 여기에서 open 은 전송되지 않고, _open 만 전송되는데, 이 경우 스프링 MVC는 체크를 해제했다고 판단한다.

체크 해제를 인식하기 위한 히든 필드

<input type="hidden" name="_open" value="on"/>

체크 해제 시, 체크 박스 필드(open)는 서버로 넘어오지 않지만, 히든 필드(_open)는 넘어온다.

 

*체크 박스 체크*

open=on&_open=on

스프링 MVC가 open 에 값이 있는 것을 확인하고 사용한다. 이때 _open 은 무시한다.

 

*체크 박스 미체크*

_open=on

스프링 MVC가 _open 만 있는 것을 확인하고, open 의 값이 체크되지 않았다고 인식한다. 이 경우 서버에서 Boolean 타입을 찍어보면 결과가 null 이 아니라 false 인 것을 확인할 수 있다. 

체크 박스 - 단일2

타임리프 체크박스

개발할 때 마다 이렇게 히든 필드를 추가하는 것은 상당히 번거롭다. 타임리프가 제공하는 폼 기능을 사용하면 이런 부분을 자동으로 처리할 수 있다.

 

th:field 사용

 

<form action="item.html" th:action th:object="${item}" method="post">
...
    <!-- single checkbox -->
    <div>판매 여부</div>
    <div>
      <div class="form-check">
      <input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
      <label for="open" class="form-check-label">판매 오픈</label>
      </div>
    </div>

 

>HTML 생성 결과

히든 필드 자동 생성 <input type="hidden" name="_open" value="on"/>

 

<input type="chechbox" id="open" th:field="${item.open}" class="form-check-input" disabled>

disabled 추가시 체크 박스 선택하지 못함 (ex. 상품 상세)

 

타임리프의 체크 확인

checked="checked"

체크 박스에서 판매 여부를 선택(on)해서 저장하면, 조회시에 checked 속성이 추가된 것을 확인할 수 있다. 이런 부분을 개발자가 직접 처리하려면 상당히 번거롭다. 타임리프의 th:field 를 사용하면, 값이 true 인 경우 체크를 자동으로 처리해준다.

선택을 하지 않으면 check 속성 사라짐

 

*새로운 속성을 추가,변경하는 서비스 로직 -> 리포지토리에서 도메인을 변경하는 로직도 추가해야 함