스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - sec01
출처 : 스프링 MVC 2편
타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용됨
순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있음
타임리프는 스프링과 자연스럽게 통합되고, 스프링의 다양한 기능을 편리하게 사용할 수 있게 지원함
HTML의 콘텐츠(content)에 데이터를 출력할 때 사용 <span th:text="${data}">
HTML 테그의 속성이 아니라 HTML 콘텐츠 영역안에서 직접 데이터를 출력할때 사용
[[${data}]]
"Hello <b>Spring!</b>"
이렇게 쓰였을 때, 우리는 Spring!이 굵게 나오기를 기대한다, 하지만 결과는 Hello Spring! 그리고 소스를 보면 Hello <b>Spring!</b>
이렇게 나옴 => 우리가 원하던 모양 ❌
웹 브라우저는 <
를 HTML 테그의 시작으로 인식 => 따라서 < 를 테그의 시작이 아니라 문자로 표현할 수 있는 방법이 필요한데, 이것을 HTML 엔티티라 한다. 이렇게 HTML에서 사용하는 특수 문자를 HTML 엔티티로 변경하는 것을 이스케이프(escape)라 한다. 그리고 타임리프가 제공하는 th:text
, [[...]]
는 기본적으로 이스케이스(escape)를 제공한다
th:text
를 th:utext
로 변경[[...]]
를 [(...)]
로 변경<span th:text="${user.username}">
Object
user.username
: user의 username을 프로퍼티 접근 => user.getUsername()
user['username']
: 위와 같음
user.getUsername()
: user의 getUsername()
을 직접 호출
List
users[0].username
: List에서 첫 번째 회원을 찾고 username 프로퍼티 접근 => list.get(0).getUsername()
users[0]['username']
: 위와 같음
users[0].getUsername()
: List에서 첫 번째 회원을 찾고 메서드 직접 호출
Map
userMap['userA'].username
: Map에서 userA를 찾고, username 프로퍼티 접근 => map.get("userA").getUsername()
userMap['userA']['username']
: 위와 같음
userMap['userA'].getUsername()
: Map에서 userA를 찾고 메서드 직접 호출
<h1>지역 변수 - (th:with)</h1>
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
지역 변수는 선언한 테그 내에서만 사용 가능
${#request}
, ${#response}
, ${#session}
, ${#servletContext}
, ${#locale}
🚨 #request
는 HttpServletRequest 객체가 그대로 제공되기 때문에 데이터를 조회하려면 request.getParameter("data")
처럼 불편하게 접근해야 함
이런 점을 해결하기 위한 편의 객체도 제공
HTTP 요청 파라미터 접근: param ${param.paramData}
HTTP 세션 접근: session ${session.sessionData}
+) 세션은 유저가 웹브라우저를 나가기 전까지는 계속 유지가 되는 것
스프링 빈 접근: @ ${@helloBean.hello('Spring!')}
+) @helloBean은 지정한 빈의 이름
#message
: 메시지, 국제화 처리
#uris
: URI 이스케이프 지원
#dates
: java.util.Date 서식 지원
#calendars
: java.util.Calendar 서식 지원
#temporals
: 자바8 날짜 서식 지원
#numbers
: 숫자 서식 지원
#strings
: 문자 관련 편의 기능
#objects
: 객체 관련 기능 제공
#bools
: boolean 관련 기능 제공
#arrays
: 배열 관련 기능 제공
#lists
, #sets , #maps : 컬렉션 관련 기능 제공
#ids
: 아이디 처리 관련 기능 제공, 뒤에서 설명
대략 이 정도 알고, 필요할 때 구글링...ㄱ...
자바8 날짜인 LocalDate
, LocalDateTime
, Instant
를 사용하려면 라이브러리가 추가적으로 필요 -> 스프링 부트 타임리프를 사용하면 해당 라이브러리 자동 추가 + 통합
@{...}
단순한 URL
@{/hello}
=> /hello
쿼리 파라미터
@{/hello(param1=${param1}, param2=${param2})}
=> /hello?param1=data1¶m2=data2
() 에 있는 부분은 쿼리 파라미터로 처리된다.
경로 변수
@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}
=> /hello/data1/data2
() 부분은 경로 변수로 처리된다.
경로 변수 + 쿼리 파라미터
@{/hello/{param1}(param1=${param1}, param2=${param2})}
=> /hello/data1?param2=data2
경로 변수와 쿼리 파라미터를 함께 사용 가능!
소스 코드상에 고정된 값
String a = "Hello" => 문자 리터럴
int a = 10 * 20 => 숫자 리터럴
타임리프에서 문자 리터럴은 항상 ' (작은 따옴표)로 감싸야 한다.
<span th:text="'hello'">
하지만, 너무 귀찮...
공백 없이 쭉 이어진다면 하나의 의미있는 토큰으로 인지해서 작은 따옴표를 생략 가능
=> 이 말은 즉슨 중간 공백이 있으면 하나의 토큰으로 인지 ❌
가능한 아이들 : A-Z , a-z , 0-9 , [] , . , - , _
<span th:text="|hello ${data}|">
=> 공백이 있어도 알아서 다 하나의 의미로 처리를 해주기 때문에 얘가 더 편함
비교연산: HTML 엔티티를 사용해야 하는 부분을 주의하자!
'>' (gt), < (lt), >= (ge), <= (le), ! (not), == (eq), != (neq, ne)
조건식: 자바의 조건식과 유사
<li>조건식
<ul>
<li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)?
'짝수':'홀수'"></span></li>
</ul>
</li>
Elvis 연산자: 조건식의 편의 버전
<li>Elvis 연산자
<ul>
<li>${data}?: '데이터가 없습니다.' = <span th:text="${data}?: '데이터가
없습니다.'"></span></li>
<li>${nullData}?: '데이터가 없습니다.' = <span th:text="${nullData}?:
'데이터가 없습니다.'"></span></li>
</ul>
</li>
No-Operation: '_'인 경우 마치 타임리프가 실행되지 않는 것 처럼 동작
<li>No-Operation
<ul>
<li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</
span></li>
<li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가
없습니다.</span></li>
</ul>
</li>
타임리프는 주로 HTML 태그에 th:*
속성을 지정하는 방식으로 동작 => th:*
로 속성을 적용하면 기존 속성을 대체하고 없으면 새로 만듬
속성 설정
th:attrappend
: 속성 값의 뒤에 값을 추가한다.
th:attrprepend
: 속성 값의 앞에 값을 추가한다.
th:classappend
: class 속성에 자연스럽게 추가한다.
if HTML) <input type="checkbox" name="active" checked="false" />
=>
checked 속성이 있기 때문에 checked 처리가 되어버리고 속성의 값과 상관없이 checked 라는 속성만 있어도 체크가 됨
이런 부분이 true , false 값을 주로 사용하는 개발자 입장에서는 좀 언짢
BUT! 타임리프의 th:checked
는 값이 false 인 경우 checked 속성 자체를 제거
<input type="checkbox" name="active" th:checked="false" /> => 렌더링 후: <input type="checkbox" name="active" />
<tr th:each="user : ${users}">
반복시 오른쪽 컬렉션( ${users} )의 값을 하나씩 꺼내서 왼쪽 변수( user )에 담아서 태그를 반복 실행
+) List 뿐만 아니라 배열, Iterable , Enumeration 을 구현한 모든
객체를 반복에 사용 가능! Map 도 사용할 수 있는데 이 경우 변수에 담기는 값 -> Map.Entry
반복 상태 유지
<tr th:each="user, userStat : ${users}">
두번째 파라미터를 설정해서 반복의 상태를 확인 가능!
두번째 파라미터는 생략 가능한데, 생략하면 지정한 변수명( user ) + Stat 가 됨
반복 상태 유지 기능
index
: 0부터 시작하는 값
count
: 1부터 시작하는 값
size
: 전체 사이즈
even , odd
: 홀수, 짝수 여부( boolean )
first , last
:처음, 마지막 여부( boolean )
current
: 현재 객체
타임리프는 해당 조건이 맞지 않으면 태그 자체를 렌더링 ❌ , 사라짐
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살</span>
<span th:case="*">기타</span>
</td>
* 은 만족하는 조건이 없을 때 사용하는 디폴트이다
<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>
/*/-->
결과
<h1>1. 표준 HTML 주석</h1>
<!--
<span th:text="${data}">html data</span>
-->
<h1>2. 타임리프 파서 주석</h1>
<h1>3. 타임리프 프로토타입 주석</h1>
표준 HTML 주석
자바스크립트의 표준 HTML 주석은 타임리프가 렌더링 하지 않고, 그대로 남겨둔다.
타임리프 파서 주석
타임리프 파서 주석은 타임리프의 진짜 주석 -> 렌더링에서 주석 부분 제거
타임리프 프로토타입 주석
HTML 주석에 약간의 구문을 더함
HTML 파일을 웹 브라우저에서 그대로 열어보면 HTML 주석이기 때문에 이 부분이 웹 브라우저가 렌더링하지 않음
BUT! 타임리프 렌더링을 거치면 이 부분이 정상 렌더링 된다.
즉, HTML 파일을 그대로 열어보면 주석처리가 되지만, 타임리프를 렌더링 한 경우에만 보임
HTML 태그가 아닌 타임리프의 유일한 자체 태그다.
div 파트 2개를 동시에 반복으로 돌려서 출력하고 싶을 때는 블록으로 묶은 다음 그 블록에서 반복처리를 진행해야 함
자바스크립트에서 타임리프를 편리하게 사용할 수 있는 자바스크립트 인라인 기능 제공
<script th:inline="javascript">
<!-- 자바스크립트 인라인 사용 전 -->
<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>
인라인 사용 전
<script>
var username = userA;
var age = 10;
//자바스크립트 내추럴 템플릿
var username2 = /*userA*/ "test username";
//객체
var user = BasicController.User(username=userA, age=10);
</script>
인라인 사용 후
<script>
var username = "userA";
var age = 10;
//자바스크립트 내추럴 템플릿
var username2 = "userA";
//객체
var user = {"username":"userA","age":10};
</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";
var user = [[${user}]];
var user = BasicController.User(username=userA, age=10);
var user = {"username":"userA","age":10};
<script th:inline="javascript">
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]
</script>
웹 페이지의 개발시에 공통된 부분들을 떼다 쓸 수 있도록 해줌
이 태그는 다른 곳에 포함되는 코드 조각
<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>
template/fragment/footer :: copy
: template/fragment/footer.html
템플릿에 있는 th:fragment="copy"
라는 부분을 템플릿 조각으로 가져와서 사용한다는 의미
+) 경로 :: 이름
이전에는 일부 코드 조각을 가지고와서 사용했다면, 이번에는 코드 조각을 레이아웃에
넘겨서 사용하는 방법
뼈대 레이아웃
<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>
추가 레이아웃
<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>
결과
<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>
common_header(~{::title},~{::link})
::title
은 현재 페이지의 title 태그들을 전달한다.
::link
는 현재 페이지의 link 태그들을 전달한다.
뼈대
<head>
<title th:replace="${title}">레이아웃 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<div th:replace="${content}">
<p>레이아웃 컨텐츠</p>
</div>
<footer>
레이아웃 푸터
</footer>
</body>
추가
<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>
생성 결과
<head>
<title>메인 페이지 타이틀</title>
</head>
<body>
<h1>레이아웃 H1</h1>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
<footer>
레이아웃 푸터
</footer>
th:fragmen
t 속성이 정의되어 있음 => 이 레이아웃 파일을 기본으로 하고 여기에 필요한 내용을 전달해서 부분부분 변경하는 것th:replace
를 사용해서 변경함 => 즉, 뼈대에 필요한 내용을 전달하면서 자체를 뼈대로 변경함