스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 공부하고 정리하는 포스트입니다.


타임리프

특징

  • 서버 사이드 HTML 렌더링 (SSR)
  • 내츄럴 템플릿
  • 스프링 통합 지원

서버 사이드 HTML 렌더링 (SSR)

  • 타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도로 사용됨.

내츄럴 템플릿

  • 순수 HTML을 최대한 유지
  • HTML 유지 -> 웹 브라우저에서 파일을 직접 열어서 확인 가능, 서버를 통해 뷰 템플릿을 거쳐 동적으로 변경된 결과를 확인 가능
    • JSP를 포함한 다른 뷰 템플릿은 해당 파일을 열면 웹 브라우저에서 정상적인 결과 확인 불가능 -> JSP 코드와 HTML이 섞이기 때문
  • 이처럼 순수 HTML을 유지하면서 뷰 템플릿도 사용할 수 있는 특징을 내츄럴 템플릿이라고 함

스프링 통합 지원

  • 스프링과 자연스럽게 통합, 다양한 기능을 편리하게 사용할 수 있게 지원함.

기본 기능

  • 타임리프 사용 선언 : <html xmlns:th="http://www.thymeleaf.org">

기본 표현식

  • 간단한 표현
    • 변수 표현식 : ${...}
    • 선택 변수 표현식 : *{...}
    • 메시지 표현식 : #{...}
    • 링크 URL 표현식 : @{...}
    • 조각 표현식 : ~{...}
  • 리터럴
    • 텍스트 : ‘one text’, ‘Another one!’, …
    • 숫자 : 0, 34, 3.0, 12.3, …
    • 불린 : true, false
    • 널 : null
    • 리터럴 토큰 : one, sometext, main, …
  • 문자 연산
    • 문자 합치기 : +
    • 리터럴 대체 : |The name is ${name}|
  • 산술 연산
    • Binary operators : +, -, *, /, %
    • Minus sign(unary operator) : -
  • 불린 연산
    • Binary operators : and, or
    • Boolean negation(unary operator) : !, not
  • 비교와 동등
    • 비교 : >, <, >=, <= (gt, lt, ge, le)
    • 동등 연산 : ==, != (eq, ne)
  • 조건 연산
    • If-then : (if) ? (then)
    • If-then-else : (if) ? (then) : (else)
    • Default : (value) ? : (default value)
  • 특별한 토큰
    • No-Operation: _

텍스트 - text, utext

  • 타임리프는 기본적으로 HTML 태그의 속성에 기능을 정의해서 동작함
    • HTML의 콘텐츠에 데이터를 출력할 때 : <span th:text="${data}">
    • 콘텐츠 영역 안에 직접 출력할 때 : [[${data}]]

Escape

  • HTML은 <, > 같은 특수 문자를 기반으로 정의됨 -> 뷰 템플릿으로 HTML 화면을 생성할 때 특수 문자에 주의

HTML 엔티티

  • 웹 브라우저는 <를 HTML 태그의 시작으로 인식함 -> <를 문자로 표현하는 방법이 HTML 엔티티
  • 특수문자를 HTML 엔티티로 변경하는 것을 이스케이프(escape)라고 함.

  • 타임리프가 제공하는 th:text, [[...]]는 기본적으로 이스케이프를 제공함.

Unescape

  • 이스케이프를 사용하지 않고 싶을 때 타임리프는 다음 두 기능을 제공함
    • th:text -> th:utext
    • [[...]] -> [(...)]

변수 - SpringEL

  • 변수를 사용할 때는 변수 표현식을 사용 : ${...}
    • SpringEL이라는 스프링이 제공하는 표현식을 사용할 수 있음

SpringEL 다양한 표현식 사용

  • Object
    • user.username : user의 username을 프로퍼티 접근 -> user.getUsername()
    • user['username'] : 위와 같음 -> user.getUsername()
    • 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를 찾고 메서드 직접 호출

지역 변수 선언

  • th:with를 사용하면 지역 변수를 선언해서 사용 가능
    • 지역 변수는 선언한 태그 안에서만 사용할 수 있음

기본 객체들

  • 다음 기본 객체들을 제공함
    • ${#request}
    • ${#response}
    • ${#session}
    • ${#servletContext}
    • ${#locale}
  • ${#request}HttpServletRequest 객체가 그대로 제공되기 때문에 데이터를 조회하려면 request.getParameter("data")와 같이 불편하게 접근해야 함 -> 이를 해결하는 것이 편의 객체
    • HTTP 요청 파라미터 접근 : Param -> ex) ${param.paramData}
    • HTTP 세션 접근 : session -> ex) ${session.sessionData}
    • 스프링 빈 접근 : @ -> ex) ${@helloBean.hello('Spring!')}

유틸리티 객체와 날짜

  • 타임리프 유틸리티 객체
    • #message : 메시지, 국제화 처리
    • #uris : URI 이스케이프 지원
    • #dates : java.util.Date 서식 지원
    • #calendars : java.util.Calendar 서식 지원
    • #temporals : 자바8 날짜 서식 지원
    • #numbers : 숫자 서식 지원
    • #strings : 문자 관련 편의 기능
    • #objects : 객체 관련 기능 제공
    • #bools : boolean 관련 기능 제공
    • #arrays : 배열 관련 기능 제공
    • #lists, #sets, #maps : 컬렉션 관련 기능 제공
    • #ids : 아이디 처리 관련 기능 제공, 뒤에서 설명
  • 타임리프 유틸리티 객체
    • https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#expression-utilityobjects
  • 유틸리티 객체 예시
    • https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#appendix-b-expressionutility-objects

자바8 날짜

  • 타임리프에서 자바8 날짜인 LocalDate, LocalDateTime, Instant를 사용하려면 추가 라이브러리 필요
    • 타임리프 자바8 날짜 지원 라이브러리 : thymeleaf-extras-java8time
  • 자바8 날짜용 유틸리티 객체 : #temporals
  • 사용 예시
<span th:text="${#temporals.format(localDateTime, 'yyyy-MM-dd HH:mm:ss')}"></span>

URL 링크

  • 단순한 URL : @{/hello} /hello
  • 쿼리 파라미터 : @{/hello(param1=${param1}, param2=${param2})} -> /hello?param1=data1&param2=data2
    • ()에 있는 부분은 쿼리 파라미터로 처리.
  • 경로 변수 : @{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})} -> /hello/data1/data2
    • URL 경로상에 변수가 있으면 ()부분은 경로 변수로 처리.
  • 경로 변수 + 쿼리 파라미터 : @{/hello/{param1}(param1=${param1}, param2=${param2})} -> /hello/data1?param2=data2
    • 경로 변수와 쿼리 파라미터를 함께 사용.
  • 상대경로, 절대경로, 프로토콜 기준을 표현.
    • /hello : 절대 경로
    • hello : 상대 경로

리터럴

  • 리터럴 : 소스 코드 상에 고정된 값
    • 문자 : 'hello'
    • 숫자 : 10
    • 불린 : true, false
    • null : null
  • 타임리프에서 문자 리터럴은 항상 '(작은 따옴표)로 감싸야 함 -> <span th:text="'hello'">
    • 문자를 항상 '로 감싸는 것은 귀찮음 -> 공백 없이 쭉 이어진다면 하나의 의미있는 토큰으로 인지해서 작은 따옴표를 생략할 수 있음
    • 룰 : A-Z, a-z, 0-9, [], ., -, _ -> <span th:text="hello">
  • 오류 <span th:text="hello world!"></span>
    • 문자 리터럴은 원칙 상 ' 로 감싸야 함. 중간에 공백이 있어서 하나의 의미있는 토큰으로도 인식되지 않음.
  • 수정 <span th:text="'hello world!'"></span>
    • ' 로 감싸면 정상 동작.

리터럴 대체(Literal substitutions)

  • <span th:text="|hello ${data}|">
    • 리터럴 대체 문법을 사용하면 마치 템플릿을 사용하는 것 처럼 편리.

연산

  • 비교연산 : HTML 엔티티를 사용해야 하는 부분을 주의
    • > (gt), < (lt), >= (ge), <= (le), ! (not), == (eq), != (neq, ne)
  • 조건식 : 자바의 조건식과 유사.
  • Elvis 연산자 : 조건식의 편의 버전
  • No-Operation : _인 경우 마치 타임리프가 실행되지 않는 것 처럼 동작.
    • 이것을 잘 사용하면 HTML의 내용 그대로 활용할 수 있음.

속성 값 설정

속성 설정

  • th:* 속성을 지정하면 타임리프는 기존 속성을 th:*로 지정한 속성으로 대체, 기존 속성이 없으면 새로 생성
    • <input type="text" name="mock" th:name="userA" /> -> 타임리프 렌더링 후 <input type="text" name="userA" />

속성 추가

  • th:attrappend : 속성 값의 뒤에 값을 추가
  • th:attrprepend : 속성 값의 앞에 값을 추가
  • th:classappend : class 속성에 자연스럽게 추가

checked 처리

  • HTML에서는 <input type="checkbox" name="active" checked="false" /> -> 이 경우에도 checked 속성이 있기 때문에 checked 처리 -> HTML에서 checked속성은 checked속성의 값과 상관없이 checked라는 속성만 있어도 체크. 이런 부분이 true, false값을 주로 사용하는 개발자 입장에서는 불편.

  • 타임리프의 th:checked는 값이 false 인 경우 checked 속성 자체를 제거

    • <input type="checkbox" name="active" th:checked="false" /> -> 타임리프 렌더링 후 : <input type="checkbox" name="active" />

반복

  • 반복 기능 : <tr th:each="user : ${users}">
    • 반복 시 오른쪽 컬렉션(${users})의 값을 하나씩 꺼내서 왼쪽 변수(user)에 담아서 태그를 반복 실행
    • th:each는 List 뿐만 아니라 ‘배열’, ‘java.util.Iterable’, ‘java.util.Enumeration’을 구현한 모든 객체를 반복에 사용할 수 있음.
    • ‘Map’도 사용 할 수 있는데 이 경우 변수에 담기는 값은 Map.Entry
  • 반복 상태 유지 : <tr th:each="user, userStat : ${users}">
    • 반복의 두번째 파라미터를 설정해서 반복의 상태를 확인
    • 두번째 파라미터는 생략 가능, 생략하면 지정한 ‘변수명(user) + Stat’가 됨. 여기서는 ‘user + Stat = userStat’이므로 생략 가능
  • 반복 상태 유지 기능
    • index : 0부터 시작하는 값
    • count : 1부터 시작하는 값
    • size : 전체 사이즈
    • even, odd : 홀수, 짝수 여부( boolean )
    • first, last :처음, 마지막 여부( boolean )
    • current : 현재 객체

조건부 평가

  • if, unless
    • 타임리프는 해당 조건이 맞지 않으면 태그 자체를 렌더링하지 않음
    • 만약 다음 조건이 false인 경우 <span>...<span> 부분 자체가 렌더링 되지 않고 사라짐 -> <span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
  • switch
    • *은 만족하는 조건이 없을 때 사용하는 디폴트

주석

  • 표준 HTML 주석 : <!-- -->
    • 자바스크립트의 표준 HTML 주석은 타임리프가 렌더링 하지 않고, 그대로 남겨둠
  • 타임리프 파서 주석 : <!--/* */-->
    • 타임리프 파서 주석은 타임리프의 진짜 주석. 렌더링에서 주석 부분을 제거.
  • 타임리프 프로토타입 주석 : <!--/*/ /*/-->
    • 타임리프 프로토타입은 약간 특이한데, HTML 주석에 약간의 구문을 더함.
    • HTML 파일을 웹 브라우저에서 그대로 열어보면 HTML 주석이기 때문에 이 부분이 웹 브라우저가 렌더링하지 않음.
    • 타임리프 렌더링을 거치면 이 부분이 정상 렌더링 됨
    • 쉽게 이야기해서 HTML 파일을 그대로 열어보면 주석처리가 되지만, 타임리프를 렌더링 한 경우에는 보이는 기능.

블록

  • <th:block>은 HTML 태그가 아닌 유일한 타임리프 자체 태그

  • 타임리프의 특성상 HTML 태그 안에 속성으로 기능을 정의해서 사용 -> 따라서, 애매한 경우에 사용하면 됨. <th:block>은 렌더링 시 제거


자바스크립트 인라인

  • 자바스크립트에서 타임리프를 편리하게 사용할 수 있는 기능
    • <script th:inline="javascript">

텍스트 렌더링

  • var username = [[${user.username}]];
    • 인라인 사용 전 -> var username = userA;
    • 인라인 사용 후 -> var username = "userA";
  • 사용 전에는 변수 이름이 그대로 출력. 자바스크립트 오류 발생 -> 사용 후 문자 타입은 "로 포함. 문제가 될 수 있는 문자는 이스케이프 처리.

자바스크립트 내추럴 템플릿

  • HTML 파일을 직접 열어도 동작하는 내추럴 템플릿 기능을 제공

  • 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를 지원
[# th:each="user, stat : ${users}"]
var user[[${stat.count}]] = [[${user}]];
[/]

템플릿 조각

  • 웹 페이지에는 공통 영역이 많음. 이러한 부분을 변경 시 다 수정하는 것은 비효율적 -> 템플릿 조각과 레이아웃 기능 지원

  • th:fragment가 있는 태그는 다른 곳에 포함되는 코드 조각

  • template/fragment/footer :: copy

    • ‘template/fragment/footer.html’ 템플릿에 있는 th:fragment="copy"라는 부분을 템플릿 조각으로 가져와서 사용한다는 의미

부분 포함 insert

  • <div th:insert="~{template/fragment/footer :: copy}"></div>
    • th:insert를 사용하면 현재 태그(div) 내부에 추가

부분 포함 replace

  • <div th:replace="~{template/fragment/footer :: copy}"></div>
    • th:replace를 사용하면 현재 태그(div)를 대체

부분 포함 단순 표현식

  • <div th:replace="template/fragment/footer :: copy"></div>
    • ~{...}를 사용하는 것이 원칙. 단, 템플릿 조각을 사용하는 코드가 단순하면 생략 가능

파라미터 사용

  • 파라미터를 전달하여 동적으로 조각을 렌더링
    • <div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>

템플릿 레이아웃

  • <head>에 공통으로 사용하는 css, javascript 같은 정보들이 있음. 이러한 공통 정보들을 한 곳에 모아두고, 공통으로 사용.

  • common_header(~{::title},~{::link})

    • ::title은 현재 페이지의 title 태그들을 전달
    • ::link는 현재 페이지의 link 태그들을 전달

템플릿 레이아웃 확장

  • <head>에만 적용하는게 아니라 <html> 전체에 적용 가능