입력 폼 처리

th:object : 커맨드 객체를 지정

*{...} : 선택 변수 식으로 th:object에서 선택한 객체에 접근

th:field : HTML 태그의 id, name, value 속성을 자동으로 처리

<form action="item.html" th:action th:object="${item}" method="post">
 <div>
 <label for="itemName">상품명</label>
 <input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
 </div>
 </form>

모델로 넘겨받은 item 객체를 th:object로 지정하고 *{...} 선택변수식으로 간단히 적용할 수 있다.

*{itemName}은 모델로 넘겨받은 item.itemName 혹은 item.getItemName과 같다.

 

th:field는 위의 설명처럼 id, name value 속성을 자동으로 처리해주므로 렌더링 시 id="itemName", name="itemName", value="" 로 속성이 생성된다.

 

렌더링 전

<input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">

렌더링 후

<input type="text" id="itemName" class="form-control" placeholder="이름을 입력하세요" name="itemName" value="">

 

체크박스

체크박스는 선택이 안되면 클라이언트에서 서버로 값을 보내지 않는다. 이로인해 다양한 문제가 발생할 수 있는데, 스프링 MVC에서는 해당 문제를 해결하기 위해 히든 필드를 만들어서 체크와 체크해제를 인식한다.

<input type="checkbox" id="open" name="open" class="form-check-input">
<input type="hidden" name="_open" value="on"/>

체크가 되면 open=on&_open=on 가 되어 open에 값이 있음을 확인하여 체크되어있음을 알 수 있다.

체크를 하지 않으면 _open=on 만 남게 되어 open에 값이 없으므로 체크되지 않음을 알 수 있다.

 

타임리프를 적용한 체크박스

<input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>

렌더링 후

<input type="checkbox" id="open" class="form-check-input" name="open" value="true">
<input type="hidden" name="_open" value="on"/>
<label for="open" class="form-check-label">판매 오픈</label>

타임리프를 이용하면 체크 박스의 히든 필드를 자동으로 생성해주고 체크시 true 미체크시 false를 반환한다.

체크 박스에서 체크시 checked 속성이 추가되는데 th:field를 사용하면 true인 경우 자동으로 처리해준다.

 

멀티 체크박스

<div th:each="region : ${regions}" class="form-check form-check-inline">
<input type="checkbox" th:field="*{regions}" th:value="${region.key}" class="form-check-input">
<label th:for="${#ids.prev('regions')}" th:text="${region.value}" class="form-check-label">서울</label>
</div>

멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있는데, name은 같아도 되지만 문제는 id 값은 중복이 되면 안된다.

each 루프를 돌릴 때 th:field가 중복이 발생하지 않게 1, 2, 3 과같은 숫자를 붙여준다.

그리고 label for은 동적으로 생성된 아이디를 참조해야 하므로 th:for="${#ids.prev('regions')}"로 매핑한다.

 

라디오 버튼

자바 ENUM을 활용하여 배열형태로 개발

<div th:each="type : ${itemTypes}" class="form-check form-check-inline">
 <input type="radio" th:field="*{itemType}" th:value="${type.name()}"
class="form-check-input">
 <label th:for="${#ids.prev('itemType')}" th:text="${type.description}"
class="form-check-label">
 BOOK
 </label>
 </div>

폼 전송시 값이 있다면 item.itemType=FOOD를 없다면 item.itemType=null을 출력한다.

여기서 type.description은 enum의 메서드로 키워드의 값을 가져온다.

 

셀렉트 박스

셀렉트 박스는 여러 선택지 중 하나를 선택할 수 있다.

<select th:field="*{deliveryCode}" class="form-select">
 <option value="">==배송 방식 선택==</option>
 <option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}"
 th:text="${deliveryCode.displayName}">FAST</option>
 </select>

<select> 태그를 사용하고 option태그로 선택지들을 만든다.

'Spring > SpringMVC' 카테고리의 다른 글

스프링 로그인 - 쿠키, 세션  (0) 2023.02.12
스프링 검증 - Bean Validation  (0) 2023.02.11
스프링 검증  (0) 2023.02.10
메시지, 국제화  (0) 2023.02.05
타임리프 기본 기능 모음  (0) 2023.02.04

       타임리프 특징

  • 서버 사이드 HTML 렌더링(SSR) -> 백엔드 서버에서 HTML을 동적으로 렌더링
  • 네츄럴 템플릿 -> 순수 HTML을 최대한 유지
  • 스프링 통합 지원 -> 스프링의 다양한 기능을 편리하게 사용할 수 있도록 지원

타임리프 사용시 선언

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

 

기본 텍스트 출력 시 - text (escape)

<span th:text="${data}"></span>
<span>컨텐츠 안에서 직접 출력 = [[${data}]]</span>

 

HTML 문서는 <, > 같은 특수문자를 기반으로 정의되는데 이것을 본문에 그대로 넣게되면 < 같은 경우는 &lt 로 치환된다. 이렇게 HTML에서 문자로 표현하는 것을 HTML 엔티티라고 한다. 그리고 HTML 특수문자 -> HTML 엔티티로 변경하는 것을 이스케이프(escape)라고 한다.

 

타임리프에서 제공하는 th:text, [[  ]]는 기본적으로 이스케이프(escape)를 제공한다.

 

기본 텍스트 출력 시 - utext (Unescape)

<span th:utext="${data}"></span>

꼭 필요할 때를 제외하고 escape인 text를 이용하자

 

변수 - SpringEL

<h1>Object</h1>
<span th:text="${user.username}"></span>
<span th:text="${user['username']}"></span>
<span th:text="${user.getUsername}"></span>
<h1>List</h1>
<span th:text="${users[0].username}"></span>
<span th:text="${users[0]['username']}"></span>
<span th:text="${users[0].getUsername()}"></span>
<h1>Map</h1>
<span th:text="${userMap['userA'].username}"></span>
<span th:text="${userMap['userA']['username']}"></span>
<span th:text="${userMap['userA'].getUsername()}"></span>

모델로 값을 받은 변수를 프로퍼티 접근법 혹은 getter로 간단하게 출력할 수 있다.

 

유틸리티 객체와 날짜

<span th:text="${#temporals.format(localDatetime,'yyyy-MM-dd HH:mm:ss')}">2023-02-04 11:02:33</span>

<span th:text="${#temporals.day(localDateTime)}"></span>
<span th:text="${#temporals.month(localDateTime)}"></span>
<span th:text="${#temporals.monthName(localDateTime)}"></span>
<span th:text="${#temporals.monthNameShort(localDateTime)}"></span>
<span th:text="${#temporals.year(localDateTime)}"></span>
<span th:text="${#temporals.dayOfWeek(localDateTime)}"></span>
<span th:text="${#temporals.dayOfWeekName(localDateTime)}"></span>
<span th:text="${#temporals.dayOfWeekNameShort(localDateTime)}"></span>
<span th:text="${#temporals.hour(localDateTime)}"></span>
<span th:text="${#temporals.minute(localDateTime)}"></span>
<span th:text="${#temporals.second(localDateTime)}"></span>
<span th:text="${#temporals.nanosecond(localDateTime)}"></span>

모델에서 LocalDateTime.now()을 "localDateTime"으로 시간 값을 받아와서 출력한다.

 

URL 링크

<a th:href="@{/hello}">url</a>
<a th:href="@{/hello(param1=${param1}, param2=${param2})}">query param</a>
<a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a>
<a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query param</a>

단순히 URL을 입력하려면 @를 이용하여 하면 되고, 쿼리 파라미터 값을 입력하려면 2번째 줄과 같이 입력해주면 된다. path variable은 url 형태를 입력해주고 () 안에 파라미터 값을 매핑해주면 된다. 만약 매핑 값이 추가로 있다면 쿼리 파라미터로 적용된다.

 

리터럴

리터럴이란 소스 코드상에서 고정된 값을 의미한다. ex) "A"(문자 리터럴) int a = 10 * 20(10과 20은 숫자 리터럴)

타임리프에는 문자, 숫자, 불린, null 총 4개의 리터럴이 존재하고 문자 리터럴은 항상 '(작은 따옴표)로 감싸줘야 한다.

<span th:text="'hello'">

하지만 공백 없이 이어진다면 하나의 의미있는 토큰으로 인지해 작은따옴표를 생략할 수 있다.

 

<span th:text="hello world">오류 발생</span>
<span th:text="'hello world'">수정 완료</span>

대표적인 오류 예시이다. hello 뒤에 공백이 있으므로 이것은 '(작은 따옴표)로 묶어주어야 한다.

 

<span th:text="'hello ' + ${data}"></span>
<span th:text="|hello ${data}|">리터럴 대체</span>

'(작은 따옴표)로 감싸는게 귀찮다면 | ... | 리터럴 대체를 이용하여 사용하면 된다.

 

연산

<h1>비교 연산</h1>
<span th:text="10 + 2"></span>
<span th:text="10 % 2 == 0"></span>
<span th:text="1 &gt; 2"></span>
<span th:text="1 gt 2"></span>
<span th:text="1 >= 2"></span>
<span th:text="1 ge 2"></span>
<span th:text="1 == 2"></span>
<span th:text="1 != 2"></span>

<h1>조건식</h1>
<span th:text="(10 % 2 == 0)? '짝수':'홀수'"></span>

<h1>Elvis 연산자</h1>
<span th:text="${data}?: '데이터가 없습니다.'">데이터가 있으면 출력 없으면 데이터가 없습니다.</span>

<h1>No-Operation</h1>
<span th:text="${data}?: _">데이터가 있으면 출력 없으면 현재 HTML 내용을 그대로 출력</span>

 

반복

<h1>반복</h1>
<table border="1">
    <tr>
        <th>username</th>
        <th>age</th>
    </tr>
    <tr th:each="user : ${users}">
        <td th:text="${user.username}">username</td>
        <td th:text="${user.age}">0</td>
    </tr>
</table>

반복은 th:each="내가 사용할 변수명" : ${users}" 형식으로 사용한다.

users는 보통 컬렉션 값으로 하나하나씩 지정한 변수명을 담아서 사용하는 형식으로 사용한다.

 

<h1>반복 상태 유지</h1>
<table border="1">
    <tr>
        <th>count</th>
        <th>username</th>
        <th>age</th>
        <th>etc</th>
    </tr>
    <tr th:each="user, userStat : ${users}">
        <td th:text="${userStat.count}">username</td>
        <td th:text="${user.username}">username</td>
        <td th:text="${user.age}">0</td>
        <td>
            index = <span th:text="${userStat.index}"></span>
            count = <span th:text="${userStat.count}"></span>
            size = <span th:text="${userStat.size}"></span>
            even? = <span th:text="${userStat.even}"></span>
            odd? = <span th:text="${userStat.odd}"></span>
            first? = <span th:text="${userStat.first}"></span>
            last? = <span th:text="${userStat.last}"></span>
            current = <span th:text="${userStat.current}"></span>
        </td>
    </tr>
</table>

반복의 두번째 파라미터를 설정하여 반복의 상태를 확인할 수 있다.

두번째 파라미터는 생략 가능한데, 생략하게되면 지정한 변수명 + Stat이 된다. ex) 지정한 변수명이 user인 경우 userStat

 

기능들은 다음과 같다.

  • index : 0부터 시작
  • count : 1부터 시작
  • size : 전체 사이즈
  • even, odd : 홀수 짝수 여부(boolean)
  • first, last : 처음인지 마지막인지 확인 (boolean)
  • current : 현재 객체

조건부 평가

if, unless 사용

<h1>조건부 평가 if, unless, switch</h1>
<span th:text="'화석'" th:if="${user.age gt 25}"></span>
<span th:text="'화석'" th:unless="${user.age le 25}"></span>
<div th:each="user : ${users}">
    <div th:switch="${user.age}">
        <span th:case="10">10살</span>
        <span th:case="20">20살</span>
        <span th:case="*">기타</span>
    </div>
</div>

조건이 false인 경우 span 태그 자체를 렌더링하지 않고 사라진다.

 

switch에서 *은 만족하는 조건이 없을 때 사용하는 디폴트이다.

 

주석

<h1>1. 표준 HTML 주석</h1>
<!--
<span th:text="${data}">html data</span>
-->
<h1>2. 타임리프 파서 주석</h1>
<!--/* [[${data}]] */-->
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/-->
<h1>3. 타임리프 프로토타입 주석</h1>
<!--/*/
<span th:text="${data}">html data</span>
/*/-->

표준 HTML 주석은 타임리프가 렌더링 하지 않고, 그대로 남겨둔다.

타임리프 파서 주석은 타임리프의 주석으로 렌더링에서 주석 부분을 제거한다.

타임리프 프로토타입 주석은 HTML 파일로 직접 열면 렌더링이 되지 않으나 타임리프 렌더링을 거치면 정상 렌더링이 된다. 즉, 타임리프 렌더링 한 경우에만 보인다.

 

블록

th:block은 HTML 태그가 아닌 타임리프의 유일한 태그다.

<th:block th:each="user : ${users}">
 <div>
 사용자 이름1 <span th:text="${user.username}"></span>
 사용자 나이1 <span th:text="${user.age}"></span>
 </div>
 <div>
 요약 <span th:text="${user.username} + ' / ' + ${user.age}"></span>
 </div>
</th:block>

보통 HTML 태그안에 속성으로 기능을 정의하여 사용하지만 애매한 경우 사용하면 된다. 렌더링 시 th:block은 제거된다.

 

자바스크립트 인라인

<!-- 자바스크립트 인라인 사용 전 -->
<script>
 var username = [[${user.username}]];
 var age = [[${user.age}]];
 //자바스크립트 내추럴 템플릿
 var username2 = /*[[${user.username}]]*/ "test username";
 //객체
 var user = [[${user}]];
</script>
<!-- 자바스크립트 인라인 사용 후 -->
<script th:inline="javascript">
 var username = [[${user.username}]];
 var age = [[${user.age}]];
 //자바스크립트 내추럴 템플릿
 var username2 = /*[[${user.username}]]*/ "test username";
 //객체
 var user = [[${user}]];
</script>

var username = [[${user.username}]]; 부분을 보면

  • 인라인 사용 전 -> var username = userA;
  • 인라인 사용 후 -> var username = "userA";

로 바뀐 것을 확인할 수 있는데, 렌더링시 개발자가 원하는 것은 변수명이 아닌 문자열 값이므로 타임리프가 자동으로 바꿔준다. 물론 숫자는 "가 필요 없기에 정상 렌더링 된다.

 

타임리프의 내추럴템플릿의 기능을 주석을 활용하여 확인할 수 있다.

var username2 = /*[[${user.username}]]*/ "test username";

  • 인라인 사용 전 -> var username2 = /*userA*/ "test username";
  • 인라인 사용 후 -> var username2 = "userA";

타임리프 자바스크립트 인라인 기능을 사용 시 객체를 JSON으로 자동 변환해준다.

var user = [[${user}]];

  • 인라인 사용 전 -> var user = BasicController.User(username=userA, age=10);
  • 인라인 사용 후 -> var user = {"username":"userA", "age":10};

인라인 사용 전의 경우는 toString()의 호출 값이고, 사용 후는 JSON으로 변환된 모습이다.

 

자바스크립트 인라인은 each도 지원한다.

<!-- 자바스크립트 인라인 each -->
<script th:inline="javascript">
 [# th:each="user, stat : ${users}"]
 var user[[${stat.count}]] = [[${user}]];
 [/]
</script>

출력 시 var user1 = {"username":"userA", "age":10}; 과 같은 JSON형태로 출력된다.

 

템플릿 조각

웹 개발 시 상단, 하단, 카테고리 등 공통영역이 많이 있다. 즉 중복되는 부분이 많기에 이것을 해결하기 위한 템플릿 조각과 레이아웃 기능을 소개한다.

 

footer.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<footer th:fragment="copy">
 푸터 자리 입니다.
</footer>
<footer th:fragment="copyParam (param1, param2)">
 <p>파라미터 자리 입니다.</p>
 <p th:text="${param1}"></p>
 <p th:text="${param2}"></p>
</footer>
</body>
</html>

th:fragment가 있는 태그는 다른 곳에 포함되는 코드 조각으로 이해하자

 

footerMain.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <meta charset="UTF-8">
 <title>Title</title>
</head>
<body>
<h1>부분 포함</h1>
<h2>부분 포함 insert</h2>
<div th:insert="~{template/fragment/footer :: copy}"></div>
<h2>부분 포함 replace</h2>
<div th:replace="~{template/fragment/footer :: copy}"></div>
<h2>부분 포함 단순 표현식</h2>
<div th:replace="template/fragment/footer :: copy"></div>
<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
</body>
</html>

여기서 중요한 것은 :: 이부분을 주의깊게 보자

template/fragment/footer :: copy 는 템플릿에 있는 th:fragment="copy"라는 부분을 템플릿 조각으로 가져와 사용하겠다는 뜻이다.

 

부분 포함 insert 렌더링 결과 -> th:insert 사용시 현재 태그(div) 내부에 추가

<h2>부분 포함 insert</h2>
<div>
<footer>
푸터 자리 입니다.
</footer>
</div>

 

부분포함 replace, 부분 포함 단순표현식 결과 -> th:replace 사용시 현재 태그(div)에 대체하므로 div가 사라지고 footer가 자리잡음

<h2>부분 포함 replace</h2>
<footer>
푸터 자리 입니다.
</footer>

 

파라미터 사용

<h1>파라미터 사용</h1>
<footer>
<p>파라미터 자리 입니다.</p>
<p>데이터1</p>
<p>데이터2</p>
</footer>

 

템플릿 레이아웃

 

css나 javascript를 공통 적용하고 싶다면 어떻게 해야 할까?

위의 개념을 확장해 코드 조각을 레이아웃에 넘기는 방법을 알아본다.

 

base.html

<html xmlns:th="http://www.thymeleaf.org">
<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>

 

layoutMain.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<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>
<body>
메인 컨텐츠
</body>
</html>

common_header(~{::title},~{::link})" 이 부분을 주의깊게 보자

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

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

 

출력 결과

<!DOCTYPE html>
<html>
<head>
<title>메인 타이틀</title>
<!-- 공통 -->
<link rel="stylesheet" type="text/css" media="all" href="/css/awesomeapp.css">
<link rel="shortcut icon" href="/images/favicon.ico">
<script type="text/javascript" src="/sh/scripts/codebase.js"></script>
<!-- 추가 -->
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/themes/smoothness/jquery-ui.css">
</head>
<body>
메인 컨텐츠
</body>
</html>

레이아웃 개념을 두고, 그 레이아웃에 코드 조각을 전달하여 완성시킨다.

 

<head> 태그에만 적용시키는 것이 아닌 <html> 태그 전체에 적용할 수 있다.

 

layoutFile.html

<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
 <title th:replace="${title}">레이아웃 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<div th:replace="${content}">
 <p>레이아웃 컨텐츠</p>
</div>
<footer>
 레이아웃 푸터
</footer>
</body>
</html>

 

layoutExtendMain.html

<!DOCTYPE html>
<html th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title}, ~{::section})}"
 xmlns:th="http://www.thymeleaf.org">
<head>
 <title>메인 페이지 타이틀</title>
</head>
<body>
<section>
 <p>메인 페이지 컨텐츠</p>
 <div>메인 페이지 포함 내용</div>
</section>
</body>
</html>

 

결과

<!DOCTYPE html>
<html>
<head>
<title>메인 페이지 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
<footer>
레이아웃 푸터
</footer>
</body>
</html>

 

'Spring > SpringMVC' 카테고리의 다른 글

스프링 로그인 - 쿠키, 세션  (0) 2023.02.12
스프링 검증 - Bean Validation  (0) 2023.02.11
스프링 검증  (0) 2023.02.10
메시지, 국제화  (0) 2023.02.05
타임리프 - 스프링 통합과 폼  (0) 2023.02.05

+ Recent posts