[아이티센 부트캠프] Spring Boot 2 (thymeleaf)

이언덕·2026년 4월 22일

아이티센 부트캠프

목록 보기
64/115
post-thumbnail

타임리프란 무엇인가

Thymeleaf는 서버에서 준비한 데이터를 HTML 화면에 자연스럽게 넣어 주는 템플릿 엔진이다.
여기서 템플릿 엔진은 미리 만들어 둔 화면 틀에 실행 중 필요한 값을 끼워 넣어 최종 화면을 만드는 도구를 의미한다.
즉, HTML 구조는 유지하면서 회원 이름, 게시글 제목, 상품 가격처럼 상황에 따라 달라지는 값을 화면에 반영한다.


중요한 점은 Thymeleaf가 데이터를 직접 만드는 도구는 아니라는 것이다.
데이터는 컨트롤러가 준비하고, Model에 담겨 템플릿으로 전달된다.
Thymeleaf는 그 값을 꺼내서 정해진 위치에 넣고, 브라우저가 볼 수 있는 최종 HTML을 만들어 준다.


그래서 같은 템플릿 파일이라도 어떤 데이터를 받았는지에 따라 결과 화면이 달라진다.
예를 들어 같은 회원 상세 화면이라도 Model에 담긴 회원 이름이 다르면 화면에 보이는 이름도 달라진다.


핵심은 Thymeleaf가 HTML을 버리고 새 문서를 만드는 도구가 아니라, 이미 만들어 둔 HTML 틀에 서버 데이터를 반영해 최종 화면을 완성하는 도구라는 점이다.


템플릿 엔진이라는 말부터 이해하기

화면 틀과 데이터를 합쳐서 최종 화면을 만든다

템플릿 엔진은 미리 만들어 둔 화면 틀과 실행 중에 준비된 데이터를 합쳐서 최종 화면을 만든다.
여기서 화면 틀은 개발자가 작성한 HTML 파일이고, 데이터는 컨트롤러가 Model에 담아 넘긴 값이다.
즉, 템플릿 엔진은 화면 모양을 새로 만드는 것이 아니라, 이미 정해진 화면 자리마다 필요한 값을 채워 넣는다.


이 구조가 필요한 이유는 역할을 나누기 위해서다.
컨트롤러가 화면 문자열까지 직접 만들면 데이터 처리 코드와 화면 코드가 한곳에 섞이기 쉽다.
반대로 템플릿 엔진을 사용하면 컨트롤러는 데이터를 준비하는 데 집중하고, 템플릿은 그 데이터를 어떻게 보여 줄지에 집중한다.


그래서 Thymeleaf를 볼 때는 먼저 두 가지를 나눠서 봐야 한다.

  • 컨트롤러는 화면에 필요한 값을 준비한다.
  • 템플릿은 그 값을 화면의 알맞은 위치에 넣는다.

이렇게 역할을 나눠서 보면 th:text, th:value, th:each 같은 문법도 단순 암기 대상이 아니라, 데이터를 화면에 반영하기 위한 도구로 이해할 수 있다.


서버에서 화면을 완성해서 보낸다

Thymeleaf는 보통 서버에서 화면을 완성한 뒤 브라우저로 보낸다.
이 방식을 Server Side Rendering이라고 한다.
쉽게 말하면 브라우저가 요청을 보냈을 때, 서버가 먼저 데이터를 준비하고 그 데이터를 HTML에 넣어서 완성된 화면을 내려주는 방식이다.


브라우저 입장에서는 이미 완성된 HTML을 받는다.
그래서 브라우저가 받은 화면에는 컨트롤러가 준비한 값이 이미 반영되어 있다.
즉, 값을 끼워 넣는 일은 브라우저가 아니라 서버에서 먼저 처리된다.


흐름은 아래처럼 이해하면 쉽다.

  • 브라우저가 서버에 요청을 보낸다.
  • 컨트롤러가 요청을 받고 필요한 데이터를 준비한다.
  • 컨트롤러가 데이터를 Model에 담는다.
  • Thymeleaf가 템플릿 파일을 읽고 Model 값을 반영한다.
  • 완성된 HTML이 브라우저에 전달된다.

즉, Thymeleaf에서 중요한 흐름은 컨트롤러가 값을 만들고, Model이 값을 전달하고, 템플릿이 그 값을 화면에 넣어 최종 HTML을 완성한다는 점이다.
이 흐름을 먼저 잡아 두면 뒤에서 나오는 문법을 훨씬 쉽게 연결해서 볼 수 있다.


일반 HTML과 무엇이 다른가

문서 구조는 그대로 두고, 필요한 기능만 속성으로 붙인다

Thymeleaf 템플릿 파일은 겉으로 보면 거의 일반 HTML 파일과 같다.
문서의 기본 구조도 HTML, head, body, 태그 배치 방식 그대로 사용한다.
그래서 처음 보면 “그냥 HTML 파일 아닌가?”라고 느끼기 쉽다.


차이는 필요한 태그에 th:로 시작하는 속성을 붙인다는 점이다.
th: 속성은 서버에서 Thymeleaf가 해석하는 특별한 지시문이다.
예를 들어 th:text는 “이 태그 안의 글자를 서버에서 전달받은 값으로 바꿔라”라는 의미를 가진다.


즉, Thymeleaf는 HTML 구조를 지우고 새로 만드는 방식이 아니다.
기존 HTML 태그는 그대로 두고, 서버에서 처리해야 할 부분에만 th: 속성을 붙여 동적인 화면으로 바꾼다.
이 점 때문에 서버 없이 열어도 기본 HTML 화면을 확인할 수 있고, 서버를 거치면 데이터가 반영된 화면을 볼 수 있다.


이때 최상위 <html> 태그에 xmlns:th를 선언하는 이유는 문서 안에서 th: 접두어를 사용할 수 있게 하기 위해서다.
쉽게 말해 “이 문서에서는 Thymeleaf 전용 속성인 th:를 사용하겠다”라고 알려 주는 설정이다.

// exam01.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <!-- th: 속성을 사용하겠다고 선언 -->
<head>
    <meta charset="UTF-8">
    <title>첫 템플릿</title>
</head>
<body>
    <p>
        <span th:text="${say}">Hi</span> <!-- 서버 실행 시 ${say} 값으로 바뀜 -->
        Thymeleaf
    </p>
</body>
</html>

이 예제에서 핵심은 <span> 태그 안에 있는 Hi와 th:text="${say}"의 관계다.
서버가 실행되지 않으면 Thymeleaf가 동작하지 않으므로 기본 글자인 Hi가 그대로 보인다.
반대로 서버가 실행되면 th:text가 동작하고, Model에 담긴 say 값을 꺼내 <span> 태그 안에 넣는다.


즉, 같은 파일이라도 서버를 거치지 않으면 기본 HTML처럼 보이고, 서버를 거치면 Model 값이 반영된 동적 화면이 된다.
이 차이가 Thymeleaf를 일반 HTML과 구분하는 가장 중요한 지점이다.

서버를 실행하지 않고 템플릿 파일을 직접 연 화면

서버가 템플릿을 처리하지 않은 상태에서는 th:text가 실행되지 않기 때문에 기본 글자인 Hi가 그대로 보인다.


기본예제로 이해 확인하기

서버를 끈 상태와 서버를 켠 상태가 왜 달라지는가

아래 예제는 Thymeleaf의 가장 기본적인 출력 흐름을 보여 준다.
이 예제에서 봐야 할 핵심은 서버를 끈 상태에서는 태그 안의 기본값이 보이고, 서버를 실행한 상태에서는 Model 값이 반영된다는 점이다.
즉, Thymeleaf의 Natural Template 특징을 가장 쉽게 확인할 수 있는 예제다.


먼저 서버를 끈 상태에서 hithymeleaf.html 파일을 브라우저로 직접 열면 아래처럼 보인다.
이때는 서버가 템플릿을 처리하지 않는다.
그래서 th:text="${say}"는 실행되지 않고, <span> 태그 안에 적어 둔 기본값 Hi가 그대로 출력된다.

서버가 템플릿을 처리하지 않으면 th:text는 실행되지 않고 기본 글자인 Hi가 그대로 보인다.


이제 서버를 실행한 뒤 컨트롤러를 거쳐 같은 템플릿을 요청하면 결과가 달라진다.
브라우저에서 직접 파일을 여는 것이 아니라, 컨트롤러가 매핑한 요청 주소로 들어간다.
그러면 컨트롤러가 "안녕?"라는 값을 Model에 담고, 템플릿은 그 값을 th:text로 꺼내 화면에 출력한다.

// ThymeleafController1.java
@Controller
@RequestMapping("/step1")
public class ThymeleafController1 {
    @GetMapping("/hithymeleaf")
    public String hiThymeleaf(Model model) {
        model.addAttribute("say", "안녕?"); // say라는 이름으로 화면에 보낼 값 저장
        return "basic/hithymeleaf"; // 사용할 템플릿 이름 반환
    }
}
// hithymeleaf.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <p>
        <span th:text="${say}">Hi</span> <!-- 서버 실행 시 say 값으로 변경 -->
        Thymeleaf
    </p>
</body>
</html>

여기서 model.addAttribute("say", "안녕?")의 "say"와 템플릿의 ${say}는 서로 연결된다.
컨트롤러가 say라는 이름으로 값을 담았기 때문에, 템플릿에서도 ${say}로 그 값을 꺼낼 수 있다.


즉, th:text는 태그 안의 기본값 Hi를 계속 유지하는 것이 아니다.
서버에서 템플릿이 처리되는 순간, Model에 담긴 실제 값인 "안녕?"로 태그 안의 내용을 바꿔 넣는다.
그래서 서버를 끈 상태에서는 Hi Thymeleaf, 서버를 실행한 상태에서는 안녕? Thymeleaf가 보인다.

`hithymeleaf` 실습 예제 실행 결과

컨트롤러가 Model에 담은 "안녕?" 값이 ${say} 위치에 들어가 최종 화면에 출력된다.


이 흐름을 이해하면 Thymeleaf를 단순 문법으로만 보지 않게 된다.
값은 컨트롤러가 준비하고, 템플릿은 그 값을 화면에 반영한다는 역할 분리가 자연스럽게 잡힌다.


내추럴 템플릿이란 무엇인가

Thymeleaf를 설명할 때 자주 나오는 말이 Natural Template이다.
이 말은 템플릿 파일이 서버에서 처리되기 전에도 일반 HTML처럼 열리고, 서버에서 처리된 뒤에는 데이터가 반영된 동적 화면으로 동작한다는 뜻이다.


앞에서 본 hithymeleaf.html 예제도 이 특징을 보여 준다.
서버 없이 파일을 직접 열면 <span> 태그 안의 기본값 Hi가 보인다.
반대로 서버를 실행하고 컨트롤러 요청 주소로 들어가면 Model에 담긴 "안녕?" 값이 th:text에 의해 화면에 반영된다.


즉, 같은 템플릿 파일이 상황에 따라 두 가지 방식으로 보인다.

  • 서버 없이 열면 기본 HTML 화면을 확인할 수 있다.
  • 서버에서 처리되면 데이터가 반영된 동적 화면이 된다.

이 구조가 좋은 이유는 화면 틀을 서버 실행 없이도 먼저 확인할 수 있기 때문이다.
또 HTML 구조가 크게 깨지지 않기 때문에 화면을 만드는 사람과 서버 데이터를 연결하는 사람이 같은 파일을 보고 작업하기 쉽다.


즉, Natural Template의 핵심은 순수 HTML 구조를 유지하면서도 서버가 실행되면 동적인 화면으로 바뀔 수 있다는 점이다.




값이 화면에 들어가는 전체 흐름

앞에서는 Thymeleaf가 서버에서 준비한 데이터를 HTML에 반영하는 템플릿 엔진이라는 점을 봤다.
이제는 그 데이터가 실제로 어디서 만들어지고, 어떤 이름으로 전달되고, 템플릿 안에서 어떻게 화면에 들어가는지 흐름을 봐야 한다.


Thymeleaf 문법을 바로 외우려고 하면 각각의 문법이 따로 떨어져 보인다.
하지만 먼저 값의 이동 경로를 이해하면 Model, ${...}, th:text 같은 문법이 서로 연결되어 보인다.


즉, 이 큰 주제의 핵심은 컨트롤러가 값을 준비하고, Model이 값을 전달하고, Thymeleaf가 그 값을 화면에 반영하는 전체 흐름을 잡는 것이다.


컨트롤러와 Model의 역할

누가 값을 준비하고 누가 전달하는가

브라우저가 요청을 보내면 먼저 컨트롤러가 그 요청을 받는다.
컨트롤러는 요청을 처리한 뒤, 화면에 보여 줄 값을 준비한다.
예를 들어 인사 문구, 회원 이름, 게시글 목록처럼 화면에 필요한 데이터가 여기에 해당한다.


준비한 값은 바로 템플릿으로 넘어가는 것이 아니라 Model에 담긴다.
여기서 Model은 화면에서 사용할 데이터를 잠시 담아 두는 전달용 바구니라고 이해하면 된다.
컨트롤러가 Model에 값을 담아 두면, Thymeleaf는 템플릿 안에서 그 값을 꺼내 사용할 수 있다.


이 흐름을 역할로 나누면 아래처럼 볼 수 있다.

  • 컨트롤러는 화면에 필요한 값을 준비한다.
  • Model은 컨트롤러가 준비한 값을 템플릿까지 전달한다.
  • Thymeleaf는 Model에 담긴 값을 꺼내 화면에 반영한다.

이 세 역할이 연결되어야 최종 화면에 데이터가 보인다.
즉, 화면에 값이 출력된다고 해서 템플릿이 혼자 값을 만든 것이 아니다.
먼저 컨트롤러가 값을 준비하고, 그 값을 Model에 담고, 마지막으로 Thymeleaf가 그 값을 화면에 넣는 구조다.


핵심은 값의 출발점은 컨트롤러이고, Model은 그 값을 템플릿으로 전달하는 통로라는 점이다.
이 흐름을 잡아 두면 뒤에서 ${...}를 봤을 때 “어디에 담긴 값을 꺼내는지”가 더 쉽게 이해된다.


템플릿 파일은 어떻게 연결되는가

컨트롤러는 화면 파일의 전체 경로를 직접 반환하지 않는다.
대신 사용할 템플릿 이름을 문자열로 반환한다.
그러면 Spring Boot와 Thymeleaf는 정해진 기본 규칙에 따라 실제 HTML 파일을 찾아간다.


이때 자주 나오는 말이 prefix와 suffix다.
prefix는 템플릿 이름 앞에 자동으로 붙는 기본 경로이고, suffix는 템플릿 이름 뒤에 자동으로 붙는 확장자다.


Thymeleaf의 기본 연결 규칙은 아래처럼 이해하면 된다.

  • prefix : src/main/resources/templates/
  • 컨트롤러 반환값 : basic/hithymeleaf
  • suffix : .html

이 세 가지가 합쳐지면 실제로 찾는 파일은 src/main/resources/templates/basic/hithymeleaf.html이 된다.


예를 들어 컨트롤러가 아래처럼 반환한다고 보자.

// ThymeleafController1.java
return "basic/hithymeleaf"; // templates/basic/hithymeleaf.html 파일을 찾음

이 반환값은 단순 문자열처럼 보이지만, 실제로는 템플릿 파일을 찾기 위한 이름이다.
그래서 컨트롤러에서 basic/hithymeleaf.html처럼 확장자까지 직접 쓰지 않는다.
Thymeleaf가 뒤에 .html을 자동으로 붙여 찾기 때문이다.


즉, 컨트롤러 반환값은 “브라우저에 보여 줄 문장”이 아니라 “어떤 템플릿 파일을 사용할지 알려 주는 이름”이다.
이 점을 이해하면 왜 컨트롤러에서 전체 경로를 쓰지 않고 basic/hithymeleaf처럼 반환하는지 자연스럽게 이해할 수 있다.


흐름을 한 줄로 다시 묶어 보기

지금까지의 흐름을 하나로 묶으면 아래 순서로 정리할 수 있다.
브라우저가 요청을 보내면 컨트롤러가 요청을 받고, 화면에 필요한 값을 준비한다.
그 값은 Model에 담기고, 컨트롤러가 반환한 템플릿 이름을 기준으로 실제 HTML 파일이 선택된다.


그다음 Thymeleaf는 선택된 템플릿 파일을 읽고, Model에 담긴 값을 템플릿 안의 표현식 위치에 반영한다.
마지막으로 데이터가 들어간 최종 HTML이 브라우저에 전달된다.


흐름을 짧게 쓰면 아래와 같다.

  • 요청
  • 컨트롤러에서 데이터 준비
  • Model에 데이터 저장
  • 템플릿 파일 선택
  • Thymeleaf가 값 반영
  • 최종 HTML 출력

즉, 요청 → 데이터 준비 → Model 전달 → 템플릿 선택 → 값 반영 → 최종 화면 출력 순서로 이해하면 된다.
이 순서를 먼저 잡아 두면 뒤에서 나오는 ${...}, th:text, th:value 같은 문법도 “값이 화면에 들어가는 과정”으로 연결해서 볼 수 있다.


JSP와 Thymeleaf의 차이

JSP와 Thymeleaf는 둘 다 서버에서 데이터를 반영해 화면을 만든다는 공통점이 있다.
하지만 값을 HTML에 넣는 방식은 다르다.
JSP는 서버 표현식을 값이 들어갈 자리에 직접 쓰는 방식이고, Thymeleaf는 기존 HTML 속성은 유지한 채 th: 속성으로 서버 값을 반영하는 방식이다.


아래 예제를 보면 차이가 더 잘 보인다.

// compare01.html
<!-- JSP 방식 -->
<input type="text" name="userName" value="${user.name}">
<!-- Thymeleaf 방식 -->
<input type="text" name="userName" value="unico" th:value="${user.name}">
// 결과설명
// JSP 방식 : 서버에서 처리되면 value 자리에 user.name 값이 들어간다.
// Thymeleaf 방식 : 서버 없이 열면 unico가 보이고, 서버에서 처리되면 user.name 값으로 바뀐다.

JSP 방식은 value 속성 안에 ${user.name}이 바로 들어가 있다.
즉, 서버가 처리해야 실제 값으로 바뀌는 구조다.
반대로 Thymeleaf 방식은 일반 HTML 속성인 value="unico"를 그대로 남겨 두고, 서버가 실행되면 th:value="${user.name}"가 그 값을 덮어쓴다.


이 차이가 중요한 이유는 Thymeleaf가 HTML의 기본 형태를 유지할 수 있기 때문이다.
서버 없이 파일을 열면 기본값인 unico를 볼 수 있고, 서버를 거치면 Model에서 전달된 user.name 값이 입력칸에 들어간다.


즉, Thymeleaf는 실행 전 화면 구조와 실행 후 서버 데이터 반영 결과를 하나의 파일 안에서 함께 다룰 수 있다.
그래서 앞에서 본 Natural Template 특징과도 자연스럽게 연결된다.


여기서 꼭 기억해야 하는 흐름

이 큰 주제에서 가장 중요한 것은 문법을 외우는 것이 아니다.
먼저 값이 어떤 순서로 이동하는지 이해하는 것이다.


브라우저 요청이 들어오면 컨트롤러가 화면에 필요한 값을 준비하고, Model이 그 값을 전달하고, Thymeleaf가 템플릿에 반영해서 최종 HTML을 만든다.
이 흐름이 잡혀야 뒤에서 나오는 ${...}, th:text, th:value, [[...]] 같은 문법도 서로 연결해서 이해할 수 있다.


즉, 앞으로 볼 문법들은 전부 따로 떨어진 기능이 아니다.
컨트롤러가 준비한 값을 템플릿에서 꺼내고, 필요한 위치에 넣고, 화면에 맞게 출력하기 위한 도구들이다.
이제부터는 이 흐름 위에서 실제 값을 어떻게 꺼내고 출력하는지 하나씩 보면 된다.




값 출력과 텍스트 출력 방식

앞에서는 컨트롤러가 값을 준비하고, Model이 그 값을 템플릿으로 전달하고, Thymeleaf가 최종 화면에 반영하는 흐름을 봤다.
이제는 그 값들을 템플릿 안에서 실제로 어떻게 꺼내고, 화면의 어느 위치에 넣는지 봐야 한다.


Thymeleaf 출력 문법은 크게 두 가지로 나눠서 보면 쉽다.
하나는 Model에 담긴 값을 꺼내는 표현식이고, 다른 하나는 꺼낸 값을 태그 안이나 문장 안에 넣는 출력 방식이다.


대표적으로 ${...}(변수 표현식)는 Model에 담긴 값을 꺼낼 때 사용한다.
th:text(텍스트 출력 속성)는 꺼낸 값을 태그 안의 글자로 넣을 때 사용한다.
[[...]](텍스트 인라인 표현식)는 문장 안에 값을 직접 끼워 넣을 때 사용한다.
th:value(값 속성 설정)는 입력 태그의 value 값을 채울 때 사용한다.


즉, 이 큰 주제의 핵심은 값을 읽는 문법과 값을 출력하는 위치를 구분해서 이해하는 것이다.
이 기준을 먼저 잡으면 ${...}(변수 표현식), th:text(텍스트 출력 속성), [[...]](텍스트 인라인 표현식), th:value(값 속성 설정)가 각각 왜 필요한지 훨씬 쉽게 이해할 수 있다.


${...}는 무엇인가

${...}는 Thymeleaf에서 가장 기본적으로 사용하는 변수 표현식이다.
변수 표현식은 템플릿 안에서 이미 준비된 값을 꺼내 읽는 표현식을 의미한다.
여기서 말하는 값은 컨트롤러가 Model에 담아 보낸 값일 수도 있고, 요청 파라미터나 세션에 들어 있는 값일 수도 있다.


초보자가 자주 헷갈리는 부분은 “이 값이 어디서 생겼는가”이다.
${...}는 값을 새로 만드는 문법이 아니다.
이미 컨트롤러나 요청, 세션 같은 곳에 준비되어 있는 값을 템플릿에서 꺼내 읽는 문법이다.


예를 들어 컨트롤러에서 model.addAttribute("data", "Hello")처럼 값을 담았다면, 템플릿에서는 ${data}로 그 값을 꺼낼 수 있다.
여기서 "data"라는 이름과 ${data} 안의 data가 서로 연결된다.

// ThymeleafController1.java
model.addAttribute("data", "Hello"); // data라는 이름으로 값 저장
// text-basic.html
<span th:text="${data}">기본값</span> <!-- data 이름으로 저장된 값 출력 -->

이 흐름에서 중요한 점은 이름이 맞아야 한다는 것이다.
컨트롤러가 data라는 이름으로 값을 담았기 때문에 템플릿도 ${data}라고 써야 값을 찾을 수 있다.
이름이 다르면 템플릿은 어떤 값을 꺼내야 하는지 알 수 없다.


중요한 점은 ${...}가 값을 만드는 문법이 아니라, 이미 준비된 값을 이름으로 찾아 꺼내는 변수 표현식이라는 점이다.
이 기준을 잡아 두면 뒤에서 객체 안의 값이나 리스트 안의 값을 읽을 때도 훨씬 덜 헷갈린다.


th:text와 [[...]]는 무엇이 다른가

값을 꺼냈다면 이제 그 값을 화면 어디에 넣을지 정해야 한다.
이때 자주 만나는 것이 th:text(텍스트 출력 속성)와 [[...]](텍스트 인라인 표현식)이다.
둘 다 값을 화면에 출력한다는 점은 같지만, 값이 들어가는 위치가 다르다.


th:text는 태그 안의 내용을 바꾸는 속성이다.
즉, 태그 사이에 보이는 글자를 서버에서 전달받은 값으로 바꾼다.
그래서 제목, 설명, 이름, 가격처럼 화면에 독립적으로 보여 줄 텍스트를 출력할 때 많이 사용한다.


반대로 [[...]]는 문장 안에 값을 직접 끼워 넣는 텍스트 인라인 표현식이다.
인라인은 문장이나 태그 내용 안에 자연스럽게 섞여 들어간다는 뜻으로 이해하면 된다.
즉, 이미 적혀 있는 문장 흐름 안에서 일부 값만 바꿔 보여 줄 때 쓰기 좋다.


예를 들어 th:text는 <span> 태그 안의 내용을 통째로 바꾸는 데 적합하다.
반면 [[...]]는 “안녕하세요, 홍길동님”처럼 문장 중간에 이름만 끼워 넣는 데 적합하다.


정리하면 th:text(텍스트 출력 속성)는 태그 안의 내용을 바꾸는 방식이고, [[...]](텍스트 인라인 표현식)는 문장 안에 값을 직접 섞는 방식이다.
둘 다 값을 출력하지만, 값이 들어가는 위치와 문장 구성 방식이 다르다는 점이 핵심이다.


기본예제로 이해 확인하기

같은 값을 두 가지 방식으로 출력해 보기

아래 예제는 같은 data 값을 th:text(텍스트 출력 속성)와 [[...]](텍스트 인라인 표현식)로 각각 출력하는 코드다.
즉, 같은 값을 가져오더라도 태그 안을 바꾸는 방식과 문장 안에 끼워 넣는 방식이 다르다는 점을 확인하는 예제다.


먼저 컨트롤러는 data라는 이름으로 "Hello spring boot!" 값을 Model에 담는다.
그러면 템플릿에서는 ${data}로 그 값을 꺼낼 수 있다.

// ThymeleafController1.java
@GetMapping("/text-basic") // /text-basic 요청 처리
public String textBasic(Model model) {
    model.addAttribute("data", "Hello spring boot!"); // data라는 이름으로 출력할 문자열 저장
    return "basic/text-basic"; // 사용할 템플릿 이름 반환
}
// text-basic.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>컨텐츠에 데이터 출력하기</h1>
    <ul>
        <li>th:text 사용 <span th:text="${data}">ㅋㅋㅋ</span></li> <!-- span 안의 기본 글자가 data 값으로 바뀜 -->
        <li>컨텐츠 안에서 직접 출력하기 = [[${data}]]</li> <!-- 문장 안에 data 값이 직접 들어감 -->
    </ul>
</body>
</html>
// 출력결과
// th:text 사용 Hello spring boot!
// 컨텐츠 안에서 직접 출력하기 = Hello spring boot!

첫 번째 줄에서는 th:text="${data}"가 <span> 태그 안의 기본 글자인 ㅋㅋㅋ를 "Hello spring boot!"로 바꾼다.
두 번째 줄에서는 [[${data}]]가 문장 흐름 안에 "Hello spring boot!"를 직접 끼워 넣는다.


즉, 두 방식 모두 ${data} 값을 출력하지만 적용되는 위치가 다르다.
th:text는 태그 안의 내용을 바꾸고, [[...]]는 문장 안에 값을 섞어 넣는다.

`th:text`와 `[[...]]` 출력 예제 결과

th:text는 태그 안의 기본 글자를 바꾸고, [[...]]는 문장 안에 값을 직접 끼워 넣는다.


이 예제에서 초보자 기준으로 먼저 기억하면 되는 것은 하나다.
같은 값을 출력하더라도 th:text는 태그 안을 바꾸는 방식이고, [[...]]는 문장 흐름 안에 값을 넣는 방식이라는 점이다.


값 출력과 입력값 채우기는 다르다

th:text와 th:value

값을 화면에 넣는다고 해서 항상 같은 방식으로 처리되는 것은 아니다.
초보자가 특히 헷갈리기 쉬운 부분은 화면에 보이는 글자를 바꾸는 것과 입력칸 안의 기본값을 채우는 것이 서로 다르다는 점이다.


th:text는 태그 사이의 내용을 바꾼다.
즉, <div>, <span>, <p>처럼 화면에 글자를 보여 주는 태그 안쪽 내용을 바꿀 때 사용한다.
반대로 th:value는 태그 안의 글자를 바꾸는 것이 아니라, input 태그의 value 속성값을 바꾼다.


여기서 value는 입력칸 안에 처음부터 들어가 있는 값을 의미한다.
사용자가 수정 화면에 들어갔을 때 기존 이름이나 제목이 입력칸에 미리 채워져 있는 경우가 여기에 해당한다.
그래서 th:value는 주로 입력 폼에서 기존 값을 보여 주거나, 기본 입력값을 넣을 때 사용한다.


아래 예제는 info.name 값이 lee라고 가정한 코드다.

// exam04.html
<div th:text="${info.name}">유니코</div> <!-- 태그 안의 글자를 info.name 값으로 변경 -->
<input type="text" th:value="${info.name}" value="둘리"> <!-- input의 value 값을 info.name 값으로 변경 -->
// 출력결과
// 화면 글자 : lee
// 입력칸 값 : lee

첫 번째 줄은 <div> 태그 사이의 글자인 유니코가 lee로 바뀐다.
두 번째 줄은 input 태그 안의 기본 value 값인 둘리가 lee로 바뀐다.


즉, 둘 다 info.name 값을 사용하지만 값이 들어가는 위치가 다르다.
th:text는 화면에 보이는 텍스트를 바꾸고, th:value는 사용자가 입력하는 칸의 기본값을 바꾼다.


핵심은 th:text는 태그 안의 글자 출력용이고, th:value는 입력칸의 value 값 설정용이라는 점이다.
이 차이를 알아 두면 조회 화면과 입력 폼 화면을 만들 때 훨씬 덜 헷갈린다.


여기서 꼭 기억해야 하는 핵심

이 큰 주제에서 먼저 잡아야 할 핵심은 네 가지다.

  • ${...}(변수 표현식)는 이미 준비된 값을 이름으로 꺼내는 문법이다.
  • th:text(텍스트 출력 속성)는 태그 안의 글자를 바꾸는 문법이다.
  • [[...]](텍스트 인라인 표현식)는 문장 안에 값을 직접 끼워 넣는 문법이다.
  • th:value(값 속성 설정)는 입력 태그의 value 값을 바꾸는 문법이다.

이 네 가지는 모두 값을 화면에 반영하는 문법이지만, 역할이 완전히 같지는 않다.
${...}는 값을 읽는 쪽이고, th:text, [[...]], th:value는 읽어 온 값을 어디에 넣을지 정하는 쪽이다.


즉, 값을 읽는 문법과 값을 넣는 위치를 구분해서 이해해야 Thymeleaf 출력 문법이 한꺼번에 정리된다.
이 기준을 잡아 두면 다음에 나오는 이스케이프와 언이스케이프 차이도 더 쉽게 이해할 수 있다.





이스케이프와 언이스케이프

값을 출력할 수 있게 되면 다음으로는 escape와 unescape를 구분해야 한다.
escape는 태그처럼 생긴 문자열을 실제 태그로 실행하지 않고, 그냥 글자처럼 보여 주는 처리다.
반대로 unescape는 문자열 안에 들어 있는 HTML 태그를 실제 태그처럼 해석해서 화면에 반영하는 처리다.


예를 들어 값이 Hello <b>spring!</b>라고 해 보자.
이 값을 글자처럼 보여 주면 <b>도 그대로 화면에 보인다.
하지만 태그로 해석하면 spring! 부분이 굵게 보인다.
즉, 같은 값이라도 문자로 처리하느냐, 태그로 처리하느냐에 따라 화면 결과가 달라진다.


이 큰 주제의 핵심은 값을 출력할 때 안전하게 문자로 보여 줄지, 태그까지 적용해서 보여 줄지 구분하는 것이다.
이 기준을 잡아 두면 th:text, th:utext, [[...]], [(...)]의 차이도 쉽게 이해할 수 있다.


왜 같은 문자열인데 화면 결과가 다를까

같은 문자열이라도 화면에 글자처럼 보여 줄지, HTML 태그처럼 해석할지에 따라 결과가 달라진다.
예를 들어 값 안에 <b> 태그가 들어 있으면, 이 값을 그대로 글자로 보여 줄 수도 있고 실제 굵은 글씨로 적용해서 보여 줄 수도 있다.
이 차이가 바로 escape와 unescape의 차이다.


여기서 먼저 기억해야 할 기준은 Thymeleaf의 기본 출력 방식은 안전하게 글자처럼 보여 주는 쪽이라는 점이다.
값 안에 태그처럼 생긴 내용이 있어도 기본적으로는 실제 태그로 실행하지 않고 화면에 문자로 보여 준다.
그래야 사용자가 입력한 값이나 외부에서 들어온 값이 예상하지 못한 화면 구조를 만들지 않게 된다.


반대로 unescape는 태그를 실제로 적용해야 할 때 사용한다.
예를 들어 관리자가 직접 작성한 안내 문구 안에 줄바꿈이나 굵은 글씨 같은 HTML 표현을 일부러 넣어야 하는 상황이라면 사용할 수 있다.
하지만 아무 값에나 사용하면 원하지 않는 태그까지 실행될 수 있으므로 조심해야 한다.


즉, 초보자 기준에서는 먼저 이렇게 기억하면 된다.

  • 기본은 안전하게 글자처럼 보여 주는 escape다.
  • 태그 적용이 꼭 필요할 때만 unescape를 선택한다.

핵심은 같은 문자열이라도 출력 방식에 따라 글자처럼 보일 수도 있고, 실제 HTML 태그처럼 적용될 수도 있다는 점이다.


th:text와 th:utext

th:text는 값을 안전하게 글자처럼 출력하는 속성이다.
즉, 값 안에 HTML 태그처럼 생긴 문자열이 있어도 실제 태그로 실행하지 않고 화면에 문자로 보여 준다.
그래서 th:text는 이스케이프 텍스트 출력 속성이라고 이해하면 된다.


반대로 th:utext는 값을 출력할 때 문자열 안의 HTML 태그까지 해석해서 반영할 수 있는 속성이다.
즉, 값 안에 <b> 태그가 들어 있으면 <b>를 글자로 보여 주는 것이 아니라, 실제 굵은 글씨 효과로 적용할 수 있다.
그래서 th:utext는 언이스케이프 텍스트 출력 속성이라고 이해하면 된다.


두 속성은 모두 태그 안에 값을 출력한다는 점은 같다.
하지만 출력 방식은 다르다.
th:text는 값을 문자로 안전하게 보여 주고, th:utext는 값 안의 HTML 태그를 실제 태그처럼 반영할 수 있다.


이 차이를 모르면 같은 값인데도 어떤 화면에서는 <b>spring!</b>처럼 글자로 보이고, 어떤 화면에서는 spring!이 굵게 보이는 이유를 이해하기 어렵다.
즉, 차이는 값 자체가 아니라 그 값을 어떤 방식으로 출력했는가에 있다.


핵심은 기본 출력은 th:text처럼 안전하게 문자로 보여 주는 방식이고, th:utext는 태그 반영이 꼭 필요할 때만 조심해서 쓰는 방식이라는 점이다.


[[...]]와 [(...)]

앞에서는 태그 속성 방식으로 escape와 unescape 차이를 봤다.
이번에는 문장 안에 값을 직접 넣는 인라인 방식에서 같은 차이를 확인하면 된다.
즉, 표현 위치는 달라졌지만 핵심 기준은 같다.


[[...]]는 값을 문장 안에 넣되, HTML 태그처럼 생긴 문자열을 실제 태그로 실행하지 않고 글자처럼 보여 준다.
그래서 [[...]]는 이스케이프 텍스트 인라인 표현식이라고 이해하면 된다.
예를 들어 값이 Hello <b>spring!</b>라면 <b> 태그도 화면에 문자처럼 보인다.


반대로 [(...)]는 값을 문장 안에 넣으면서, 값 안에 들어 있는 HTML 태그를 실제 태그처럼 반영할 수 있다.
그래서 [(...)]는 언이스케이프 텍스트 인라인 표현식이라고 이해하면 된다.
예를 들어 값이 Hello <b>spring!</b>라면 spring! 부분이 굵게 보일 수 있다.


정리하면 [[...]]와 [(...)]는 둘 다 문장 안에 값을 넣는 인라인 표현식이다.
하지만 [[...]]는 안전하게 글자처럼 출력하고, [(...)]는 태그까지 반영해서 출력할 수 있다.


핵심은 th:text와 th:utext의 차이가 태그 속성 방식에서 나타난다면, [[...]]와 [(...)]의 차이는 문장 안에 값을 넣는 인라인 방식에서 나타난다는 점이다.


기본예제로 이해 확인하기

네 가지를 한 화면에서 비교하기

아래 예제는 태그가 들어 있는 문자열을 네 가지 방식으로 출력해 보는 코드다.
같은 data 값이라도 escape 방식으로 출력하는지, unescape 방식으로 출력하는지에 따라 화면 결과가 달라진다.


먼저 컨트롤러는 data라는 이름으로 Hello <b>spring!</b> 값을 Model에 담는다.
이 값에는 <b> 태그가 들어 있으므로, 출력 방식에 따라 <b>가 글자로 보일 수도 있고 실제 굵은 글씨로 적용될 수도 있다.

// ThymeleafController1.java
@GetMapping("/text-unescaped") // /text-unescaped 요청 처리
public String textUnescaped(Model model) {
    model.addAttribute("data", "Hello <b>spring!</b>"); // HTML 태그가 포함된 문자열 저장
    return "basic/text-unescaped"; // 사용할 템플릿 이름 반환
}
// text-unescaped.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>text vs utext</h1>
    <ul>
        <li>th:text = <span th:text="${data}"></span></li> <!-- 태그를 글자처럼 출력 -->
        <li>th:utext = <span th:utext="${data}"></span></li> <!-- 태그를 실제 HTML처럼 반영 -->
    </ul>
    <h1><span th:inline="none">[[...]] vs [(...)]</span></h1> <!-- 인라인 문법 설명 문구를 그대로 출력 -->
    <ul>
        <li><span th:inline="none">[[...]] = </span>[[${data}]]</li> <!-- 인라인 방식으로 글자처럼 출력 -->
        <li><span th:inline="none">[(...)] = </span>[(${data})]</li> <!-- 인라인 방식으로 HTML 태그 반영 -->
    </ul>
</body>
</html>
// 출력결과
// th:text = Hello <b>spring!</b>
// th:utext = Hello spring!
// [[...]] = Hello <b>spring!</b>
// [(...)] = Hello spring!

이 예제에서 th:inline="none"은 [[...]]와 [(...)]라는 문법 모양을 화면에 설명용 글자처럼 보여 주기 위해 사용한다.
그렇지 않으면 Thymeleaf가 [[...]]나 [(...)]를 실제 인라인 표현식으로 처리하려고 할 수 있다.


결과를 보면 th:text(이스케이프 텍스트 출력 속성)와 [[...]](이스케이프 텍스트 인라인 표현식)는 <b> 태그를 글자처럼 보여 준다.
반대로 th:utext(언이스케이프 텍스트 출력 속성)와 [(...)](언이스케이프 텍스트 인라인 표현식)는 <b> 태그를 실제 HTML 태그처럼 반영한다.


즉, 네 가지 문법은 모두 값을 출력하지만 결과가 같지는 않다.
출력 위치가 태그 속성인지 인라인 표현식인지도 중요하고, 출력 방식이 escape인지 unescape인지도 중요하다.

`th:text`와 `th:utext`, `[[...]]`와 `[(...)]` 비교 결과

th:text와 [[...]]는 태그를 글자처럼 보여 주고, th:utext와 [(...)]는 태그를 실제 HTML처럼 반영한다.


여기서 꼭 기억해야 하는 핵심

이 큰 주제에서 먼저 잡아야 할 핵심은 네 가지다.

  • th:text(이스케이프 텍스트 출력 속성)는 값을 안전하게 글자처럼 보여 준다.
  • th:utext(언이스케이프 텍스트 출력 속성)는 값 안의 HTML 태그를 실제 태그처럼 반영할 수 있다.
  • [[...]](이스케이프 텍스트 인라인 표현식)는 문장 안에서 값을 글자처럼 출력한다.
  • [(...)](언이스케이프 텍스트 인라인 표현식)는 문장 안에서 값 안의 HTML 태그를 반영할 수 있다.

즉, 네 가지 문법은 모두 값을 출력하지만 기준이 다르다.
th:text와 th:utext는 태그 속성으로 출력하는 방식이고, [[...]]와 [(...)]는 문장 안에 값을 직접 넣는 인라인 방식이다.


또 th:text와 [[...]]는 escape 방식이고, th:utext와 [(...)]는 unescape 방식이다.
핵심은 기본은 안전하게 글자처럼 보여 주는 escape 방식이고, 태그 반영이 꼭 필요할 때만 unescape 방식을 조심해서 선택해야 한다는 점이다.





변수 표현식과 객체 접근

앞에서는 ${...}(변수 표현식)로 Model에 담긴 값을 꺼내는 기본 흐름을 봤다.
이번에는 값 하나를 단순히 꺼내는 것을 넘어서, 객체 안의 값, 리스트 안의 값, 맵 안의 값을 어떻게 찾아 들어가는지 봐야 한다.


처음에는 ${user.userName}, ${users[1].userName}, ${userMap['userA'].userName}처럼 점과 대괄호가 섞여 있어서 복잡해 보일 수 있다.
하지만 기준은 단순하다.
먼저 어떤 대상을 찾고, 그 대상 안에서 필요한 값을 다시 찾는 흐름이다.


즉, 이 큰 주제의 핵심은 변수 표현식을 외우는 것이 아니라, 값이 들어 있는 위치를 따라가며 필요한 값을 꺼내는 흐름을 이해하는 것이다.
이 기준을 잡아 두면 객체, 리스트, 맵 접근뿐 아니라 th:with, th:object, *{...}도 훨씬 쉽게 이해할 수 있다.


객체 안의 값을 읽는다는 것은 무엇인가

객체는 서로 관련 있는 값을 하나로 묶어 둔 데이터 덩어리라고 생각하면 된다.
예를 들어 user 객체 안에는 사용자 이름, 나이처럼 서로 관련 있는 값이 함께 들어 있을 수 있다.
즉, 객체는 값 하나만 담는 것이 아니라 여러 값을 이름과 함께 묶어 둘 수 있다.


템플릿에서 객체 안의 값을 읽을 때는 점을 사용한다.
예를 들어 ${user.userName}은 user 객체 안에 들어 있는 userName 값을 꺼내겠다는 뜻이다.
여기서 점 앞의 user는 값을 담고 있는 객체이고, 점 뒤의 userName은 그 객체 안에서 꺼낼 값의 이름이다.


초보자가 주의해야 할 점은 user 자체와 user.userName은 같지 않다는 것이다.
${user}는 user 객체 전체를 가리키고, ${user.userName}은 그 객체 안의 이름 값만 가리킨다.
즉, 객체 전체를 보는 것과 객체 안의 특정 값을 꺼내는 것은 다르다.


처음에는 점 표기가 낯설 수 있다.
하지만 결국 흐름은 단순하다.
먼저 큰 상자인 user를 찾고, 그 안에서 필요한 값인 userName을 다시 찾는 구조다.


핵심은 점 표기를 볼 때 점 앞은 대상 객체, 점 뒤는 그 객체 안에서 꺼낼 값이라고 이해하는 것이다.
이 기준을 잡아 두면 리스트나 맵 안에 객체가 들어 있는 경우도 같은 방식으로 읽을 수 있다.


리스트와 맵은 어떻게 읽는가

객체 하나만 다룰 때는 ${user.userName}처럼 객체 이름에서 바로 값을 꺼내면 된다.
하지만 여러 객체가 묶여 있으면 먼저 그중에서 어떤 객체를 꺼낼지 정해야 한다.
이때 자주 만나는 구조가 List와 Map이다.


List는 순서가 있는 묶음이다.
그래서 몇 번째 값을 꺼낼지 인덱스로 지정한다.
인덱스는 0부터 시작하므로 ${users[0]}은 첫 번째 사용자이고, ${users[1]}은 두 번째 사용자다.


예를 들어 ${users[1].userName}은 users라는 List에서 두 번째 사용자를 먼저 꺼내고, 그 사용자 객체 안의 userName 값을 다시 꺼내는 표현식이다.
즉, 흐름은 users → [1] → userName 순서로 읽으면 된다.


반대로 Map은 순서보다 키가 중요한 저장소다.
키는 값을 찾기 위한 이름표라고 생각하면 된다.
그래서 ${userMap['userA']}처럼 키를 사용해 원하는 객체를 먼저 찾는다.


예를 들어 ${userMap['userA'].userName}은 userMap에서 userA라는 키에 해당하는 사용자 객체를 먼저 찾고, 그 객체 안의 userName 값을 꺼내는 표현식이다.
즉, 흐름은 userMap → ['userA'] → userName 순서로 읽으면 된다.


정리하면 List는 순서로 찾고, Map은 키로 찾는다.
하지만 둘 다 마지막에는 객체 안의 값을 다시 꺼낼 수 있다는 점은 같다.


핵심은 List는 인덱스로 대상을 찾고, Map은 키로 대상을 찾은 뒤, 점 표기로 객체 안의 값을 읽는다는 점이다.
이 기준을 잡으면 복잡해 보이는 표현식도 왼쪽부터 차례대로 따라가며 해석할 수 있다.


th:with는 왜 필요한가

표현식이 길어지거나 같은 값을 여러 번 써야 할 때, 매번 긴 경로를 반복하면 템플릿이 금방 읽기 어려워진다.
예를 들어 ${users[0].userName}이나 ${users[0].age}처럼 같은 대상에서 여러 값을 꺼내야 한다면, users[0]을 계속 반복해서 써야 한다.
이럴 때 th:with를 사용하면 특정 값을 짧은 이름으로 다시 정리해서 사용할 수 있다.


th:with는 템플릿 안에서 잠깐 사용할 변수를 만드는 속성이다.
쉽게 말해 “이 범위 안에서는 이 값을 이 이름으로 부르자”라고 정하는 기능이다.
그래서 긴 표현식을 짧게 줄이고, 같은 값을 여러 번 사용할 때 코드가 훨씬 읽기 쉬워진다.


다만 th:with가 컨트롤러의 Model에 값을 새로 저장하는 것은 아니다.
th:with로 만든 이름은 해당 태그와 그 안쪽 범위에서 사용하는 임시 이름에 가깝다.
즉, 화면을 만들 때 템플릿 안에서 잠깐 편하게 쓰기 위한 도구라고 보면 된다.


예를 들어 아래처럼 users[0]을 first라는 이름으로 바꿔 두면, 그 안에서는 ${first.userName}처럼 더 짧게 쓸 수 있다.

// exam18.html
<ul th:with="first=${users[0]}"> <!-- users[0]을 first라는 이름으로 임시 저장 -->
    <li th:text="${first.userName}">이름</li> <!-- first에서 이름 출력 -->
    <li th:text="${first.age}">나이</li> <!-- first에서 나이 출력 -->
</ul>

핵심은 th:with가 긴 표현식을 짧은 이름으로 정리해, 템플릿을 더 읽기 쉽게 만드는 지역 변수 기능이라는 점이다.
이 기준을 잡으면 뒤에서 th:with="first=${users[0]}" 같은 코드를 봐도 어렵지 않게 읽을 수 있다.


th:object와 *{...}는 왜 필요한가

객체 안의 값을 여러 번 출력해야 할 때 매번 ${user.userName}, ${user.age}처럼 전체 경로를 쓰면 코드가 길어진다.
이럴 때 th:object와 *{...}를 함께 사용하면 더 짧고 읽기 쉽게 작성할 수 있다.


th:object는 기준 객체를 정하는 속성이다.
쉽게 말해 “이 태그 안에서는 이 객체를 기준으로 값을 읽겠다”라고 정하는 기능이다.
th:object가 객체를 새로 만드는 것은 아니다.
이미 Model에 담겨 있는 객체 중에서, 현재 범위에서 기준으로 삼을 객체를 선택하는 것이다.


*{...}는 선택 변수 표현식이다.
선택 변수 표현식은 th:object로 정한 기준 객체 안에서 필요한 값만 짧게 꺼내는 표현식이다.
예를 들어 th:object="${user}"로 기준 객체를 user로 정했다면, 그 안에서는 *{userName}만 써도 user.userName을 읽는 것처럼 동작한다.


즉, ${...}와 *{...}는 비슷해 보이지만 기준이 다르다.
${...}는 전체 경로를 직접 쓰는 방식이고, *{...}는 th:object로 정한 기준 객체 안에서 상대적으로 값을 읽는 방식이다.


아래 예제를 보면 차이가 더 분명하다.

// exam19.html
<div th:object="${user}"> <!-- user 객체를 기준 객체로 지정 -->
    <p>이름 : <span th:text="*{userName}">둘리</span></p> <!-- user.userName 출력 -->
    <p>나이 : <span th:text="*{age}">100</span></p> <!-- user.age 출력 -->
</div>

<div>
    <p>이름 : <span th:text="${user.userName}">둘리</span></p> <!-- 전체 경로로 이름 출력 -->
    <p>나이 : <span th:text="${user.age}">100</span></p> <!-- 전체 경로로 나이 출력 -->
</div>

위쪽 코드는 th:object="${user}"로 기준 객체를 먼저 정했기 때문에 *{userName}, *{age}처럼 짧게 쓸 수 있다.
아래쪽 코드는 기준 객체를 따로 정하지 않았기 때문에 ${user.userName}, ${user.age}처럼 전체 경로를 직접 쓴다.


둘 다 결과는 같을 수 있다.
하지만 같은 객체 안의 값을 여러 번 다룰 때는 th:object와 *{...}를 사용하면 코드가 더 간결해진다.


핵심은 th:object는 기준 객체를 정하는 속성이고, *{...}는 그 기준 객체 안의 값을 짧게 읽는 선택 변수 표현식이라는 점이다.
이 차이를 이해하면 ${...}와 *{...}를 같은 문법으로 착각하지 않게 된다.


기본예제로 이해 확인하기

가장 짧은 객체 접근 예제

아래 예제는 user 객체 안에 들어 있는 userName 값 하나를 꺼내는 가장 짧은 형태다.
객체 접근을 처음 볼 때는 복잡한 예제보다, 먼저 “객체를 찾고 그 안의 값을 찾는다”는 흐름부터 잡는 것이 좋다.


먼저 컨트롤러는 user라는 이름으로 사용자 객체를 Model에 담는다.
그러면 템플릿에서는 ${user.userName}처럼 user 객체 안의 userName 값을 꺼낼 수 있다.

// ThymeleafController2.java
@GetMapping("/object-basic") // /object-basic 요청 처리
public String objectBasic(Model model) {
    model.addAttribute("user", new User("사용자 A", 10)); // user 이름으로 객체 저장
    return "basic/object-basic"; // 사용할 템플릿 이름 반환
}
// object-basic.html
<div th:text="${user.userName}">이름</div> <!-- user 객체 안의 userName 값 출력 -->
// 출력결과
// 사용자 A

이 예제에서 ${user.userName}은 왼쪽부터 차례대로 읽으면 된다.
먼저 Model에 담긴 user 객체를 찾고, 그 객체 안의 userName 값을 다시 찾는다.


즉, 템플릿은 값을 새로 만드는 것이 아니다.
이미 전달된 객체 안에서 필요한 값을 찾아 꺼내 화면에 출력한다.


핵심은 ${user.userName}을 user 객체 안의 userName 값을 꺼내는 흐름으로 읽는 것이다.
이 짧은 흐름이 잡히면 뒤에서 나오는 List, Map, th:with, th:object 예제도 훨씬 쉽게 읽을 수 있다.


응용예제로 이해 완성하기

step2/variable로 객체, 리스트, 맵, 지역 변수까지 한 번에 보기

이제 실제 실습 예제로 올라가면 같은 데이터를 여러 방식으로 읽는 구조를 한 화면에서 볼 수 있다.
이 예제는 Object, List, Map, th:with를 한 번에 보여 준다.
그래서 변수 표현식이 단순히 값 하나만 꺼내는 문법이 아니라, 여러 구조 안의 값을 찾아 들어가는 문법이라는 점을 확인하기 좋다.


먼저 컨트롤러에서는 사용자 객체 두 개를 만들고, 이 객체들을 각각 다른 형태로 Model에 담는다.
하나는 단일 객체로 담고, 하나는 List로 묶어 담고, 하나는 Map으로 묶어 담는다.

// ThymeleafController2.java
@GetMapping("/variable") // /variable 요청 처리
public String variable(Model model) {
    User userA = new User("사용자 A", 10); // 첫 번째 사용자 객체 생성
    User userB = new User("사용자 B", 20); // 두 번째 사용자 객체 생성

    List<User> list = new ArrayList<>(); // 순서가 있는 사용자 목록 생성
    list.add(userA); // 0번 위치에 userA 저장
    list.add(userB); // 1번 위치에 userB 저장

    final HashMap<String, User> map = new HashMap<>(); // 키로 찾는 사용자 저장소 생성
    map.put("userA", userA); // userA 키로 userA 저장
    map.put("userB", userB); // userB 키로 userB 저장

    model.addAttribute("user", userA); // 단일 객체 저장
    model.addAttribute("users", list); // List 저장
    model.addAttribute("userMap", map); // Map 저장

    return "basic/variable"; // 사용할 템플릿 이름 반환
}

이 코드에서 중요한 점은 같은 사용자 객체가 여러 형태로 담긴다는 것이다.
user는 단일 객체이고, users는 순서로 접근하는 List이고, userMap은 키로 접근하는 Map이다.
즉, 템플릿에서는 어떤 이름으로 담겼는지에 따라 값을 읽는 방식이 달라진다.


이제 템플릿에서 각각의 값을 어떻게 읽는지 확인하면 된다.

// variable.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>SpringEL 표현식</h1>

<h3>Object</h3>
<ul>
    <li>${user.userName} = <span th:text="${user.userName}"></span></li> <!-- 점 표기로 객체 값 출력 -->
    <li>${user['userName']} = <span th:text="${user['userName']}"></span></li> <!-- 대괄호 표기로 객체 값 출력 -->
    <li>${user.getUserName()} = <span th:text="${user.getUserName()}"></span></li> <!-- 메서드 호출로 객체 값 출력 -->
</ul>

<h3>List</h3>
<ul>
    <li>${users[1].userName} = <span th:text="${users[1].userName}"></span></li> <!-- List의 1번 객체 이름 출력 -->
    <li>${users[1]['userName']} = <span th:text="${users[1]['userName']}"></span></li> <!-- List의 1번 객체 값을 대괄호로 출력 -->
    <li>${users[1].getUserName()} = <span th:text="${users[1].getUserName()}"></span></li> <!-- List의 1번 객체 값을 메서드로 출력 -->
</ul>

<h3>Map</h3>
<ul>
    <li>${userMap['userA'].userName} = <span th:text="${userMap['userA'].userName}"></span></li> <!-- userA 키의 객체 이름 출력 -->
    <li>${userMap['userA']['userName']} = <span th:text="${userMap['userA']['userName']}"></span></li> <!-- userA 키의 객체 값을 대괄호로 출력 -->
    <li>${userMap['userA'].getUserName()} = <span th:text="${userMap['userA'].getUserName()}"></span></li> <!-- userA 키의 객체 값을 메서드로 출력 -->
</ul>

<h1>지역 변수 - (th:with)</h1>
<ul th:with="first=${users[0]}"> <!-- users[0]을 first라는 이름으로 임시 저장 -->
    <li>첫 번째 사람의 이름은 '<span th:text="${first.userName}"></span>'</li> <!-- first에서 이름 출력 -->
    <li>첫 번째 사람의 나이는 '<span th:text="${first.age}"></span>'</li> <!-- first에서 나이 출력 -->
</ul>
</body>
</html>

이 예제에서 Object 부분은 단일 객체를 읽는 방식이다.
${user.userName}, ${user['userName']}, ${user.getUserName()}은 표현 방식은 다르지만 모두 user 객체 안의 이름 값을 읽는다.
즉, 점 표기, 대괄호 표기, 메서드 호출 방식으로 같은 값을 꺼낼 수 있다는 점을 보여 준다.


List 부분은 순서로 객체를 먼저 찾은 뒤, 그 객체 안의 값을 읽는 방식이다.
${users[1].userName}은 users 목록에서 1번 위치의 객체를 먼저 꺼내고, 그 객체 안의 userName 값을 읽는다.
여기서 1번은 두 번째 요소를 뜻한다.
List 인덱스는 0부터 시작하기 때문이다.


Map 부분은 키로 객체를 먼저 찾은 뒤, 그 객체 안의 값을 읽는 방식이다.
${userMap['userA'].userName}은 userMap에서 userA라는 키에 해당하는 객체를 먼저 찾고, 그 객체 안의 userName 값을 읽는다.
즉, List는 순서로 찾고, Map은 키로 찾는다는 차이가 다시 확인된다.


마지막 th:with 부분은 긴 표현식을 짧게 줄이는 예제다.
th:with="first=${users[0]}"는 users[0]을 이 범위 안에서 first라는 이름으로 부르겠다는 뜻이다.
그래서 안쪽에서는 ${first.userName}, ${first.age}처럼 더 짧게 쓸 수 있다.


Object, List, Map, th:with를 쓰면 같은 데이터도 단일 객체, 순서, 키, 임시 이름 기준으로 다르게 읽을 수 있다.


핵심은 같은 사용자 객체라도 Model에 어떤 형태로 담겼는지에 따라 템플릿에서 값을 찾아가는 방식이 달라진다는 점이다.
Object는 객체 이름으로 바로 접근하고, List는 인덱스로 먼저 찾고, Map은 키로 먼저 찾고, th:with는 긴 표현식을 임시 이름으로 줄여서 사용한다.


step3/star로 ${...}와 *{...} 차이 다시 확인하기

위 예제에서 Object, List, Map, th:with를 봤다면, 이제 th:object와 *{...}를 따로 확인하면 된다.
이 예제는 같은 user 객체의 이름과 나이를 두 가지 방식으로 출력한다.
하나는 th:object로 기준 객체를 잡고 *{...}(선택 변수 표현식)를 쓰는 방식이고, 다른 하나는 ${...}(변수 표현식)로 전체 경로를 직접 쓰는 방식이다.


먼저 컨트롤러는 user라는 이름으로 User 객체를 Model에 담는다.
그러면 템플릿에서는 이 user 객체를 기준으로 값을 읽을 수 있다.

// ThymeleafController3.java
@GetMapping("/star") // /star 요청 처리
public String star(Model model) {
    model.addAttribute("user", new User("유니코", 20)); // user 이름으로 객체 저장
    addUsers(model); // 다른 예제에서 사용할 사용자 목록도 함께 저장
    return "basic/starexpression"; // 사용할 템플릿 이름 반환
}
// starexpression.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
    <div th:object="${user}"> <!-- user 객체를 기준 객체로 지정 -->
        <p>이름 : <span th:text="*{userName}">둘리</span></p> <!-- 기준 객체 안의 userName 출력 -->
        <p>나이 : <span th:text="*{age}">100</span></p> <!-- 기준 객체 안의 age 출력 -->
    </div>
    <hr>
    <div>
        <p>이름 : <span th:text="${user.userName}">둘리</span></p> <!-- 전체 경로로 userName 출력 -->
        <p>나이 : <span th:text="${user.age}">100</span></p> <!-- 전체 경로로 age 출력 -->
    </div>
</body>
</html>

위쪽 코드는 th:object="${user}"로 기준 객체를 먼저 정한다.
그래서 그 안에서는 *{userName}, *{age}처럼 객체 이름을 다시 쓰지 않고 필요한 값만 짧게 꺼낼 수 있다.
즉, *{userName}은 기준 객체인 user 안의 userName을 읽는다는 뜻이다.


아래쪽 코드는 기준 객체를 따로 정하지 않는다.
그래서 ${user.userName}, ${user.age}처럼 user 객체 이름부터 전체 경로를 직접 써야 한다.
즉, ${...}는 전체 경로를 쓰는 방식이고, *{...}는 이미 정해진 기준 객체 안에서 값을 짧게 읽는 방식이다.


둘 다 결과는 같을 수 있다.
하지만 같은 객체 안의 여러 값을 반복해서 출력할 때는 th:object와 *{...}를 쓰면 코드가 더 짧고 읽기 쉬워진다.


th:object를 쓰면 기준 객체를 정하고, *{...}로 그 객체 안의 값을 짧게 읽을 수 있다.


핵심은 ${...}는 전체 경로로 값을 읽는 변수 표현식이고, *{...}는 th:object로 정한 기준 객체 안에서 값을 읽는 선택 변수 표현식이라는 점이다.
이 차이를 잡아 두면 두 문법이 결과가 비슷해 보여도 기준이 다르다는 것을 이해할 수 있다.


여기서 꼭 기억해야 하는 핵심

이 큰 주제에서 먼저 잡아야 할 핵심은 네 가지다.

  • 객체는 점 표기로 안쪽 값을 읽는다.
  • List는 인덱스로 대상을 먼저 찾고, 그 안의 값을 읽는다.
  • Map은 키로 대상을 먼저 찾고, 그 안의 값을 읽는다.
  • th:with는 긴 표현식을 짧은 이름으로 정리해 주는 지역 변수 기능이다.

여기서 ${...}와 *{...}의 차이도 꼭 구분해야 한다.
${...}는 전체 경로로 값을 읽는 변수 표현식이다.
반대로 *{...}는 th:object로 정한 기준 객체 안에서 값을 짧게 읽는 선택 변수 표현식이다.


즉, 변수 표현식과 객체 접근을 볼 때는 문법 모양만 외우면 안 된다.
먼저 값이 어디에 들어 있는지 보고, 그 위치까지 어떤 순서로 찾아 들어가는지 따라가야 한다.


핵심은 객체, List, Map 모두 값을 찾는 방식은 다르지만, 결국 왼쪽부터 차례대로 대상을 찾고 그 안의 값을 꺼내는 흐름이라는 점이다.
이 기준을 잡으면 복잡해 보이는 표현식도 하나씩 끊어서 읽을 수 있다.





기본 객체와 유틸리티 객체

앞에서는 객체, List, Map 안의 값을 변수 표현식으로 찾아 들어가는 방법을 봤다.
이번에는 컨트롤러가 직접 Model에 담지 않아도 템플릿 안에서 바로 꺼내 쓸 수 있는 값과, 이미 있는 값을 화면에 맞게 다듬는 도구를 알아야 한다.


이때 나눠서 봐야 할 것이 기본 객체와 유틸리티 객체다.
기본 객체는 요청 파라미터, 세션, 지역 정보처럼 이미 존재하는 값을 템플릿에서 바로 읽게 해 주는 통로다.
유틸리티 객체는 문자열, 숫자, 날짜 같은 값을 화면에 보기 좋게 가공해 주는 도구다.


즉, 이 큰 주제의 핵심은 기본 객체는 값을 꺼내는 도구이고, 유틸리티 객체는 꺼낸 값을 화면에 맞게 다듬는 도구라는 점을 구분하는 것이다.
이 기준을 잡아 두면 param, session, #locale, #strings, #numbers, #temporals 같은 문법이 한꺼번에 덜 복잡하게 보인다.


기본 객체란 무엇인가

기본 객체는 Thymeleaf가 템플릿 안에서 바로 사용할 수 있게 준비해 둔 객체다.
여기서 객체는 값을 꺼내기 위한 통로라고 이해하면 된다.
즉, 컨트롤러가 Model에 직접 담아 준 값이 아니더라도, 요청이나 세션처럼 이미 존재하는 값은 템플릿에서 바로 읽을 수 있다.


예를 들어 사용자가 주소 뒤에 ?name=duke처럼 값을 붙여 요청했다면, 그 값은 요청 파라미터에 들어 있다.
이때 템플릿에서는 param을 통해 그 요청 파라미터 값을 읽을 수 있다.
또 로그인 정보처럼 세션에 저장된 값은 session을 통해 읽을 수 있다.
현재 지역 정보는 #locale을 통해 확인할 수 있다.


대표적으로 아래처럼 이해하면 된다.

  • param : 요청 파라미터 값을 읽을 때 사용한다.
  • session : 세션에 저장된 값을 읽을 때 사용한다.
  • #locale : 현재 지역 정보와 관련된 값을 읽을 때 사용한다.

이 객체들은 값을 새로 만드는 기능이 아니다.
이미 요청, 세션, 실행 환경 안에 존재하는 값을 템플릿에서 편하게 꺼내 쓰게 해 주는 기능이다.


다만 기본 객체가 있다고 해서 모든 데이터를 Model 없이 처리하는 것은 아니다.
화면에 필요한 일반 데이터는 보통 컨트롤러가 Model에 담아 전달하고, 요청 파라미터나 세션처럼 이미 존재하는 값은 기본 객체로 바로 읽을 수 있다고 구분하면 된다.


핵심은 기본 객체가 이미 존재하는 요청값, 세션값, 지역 정보 등을 템플릿에서 바로 읽게 해 주는 통로라는 점이다.
이 기준을 잡으면 param, session, #locale을 단순 문법이 아니라 “어디에 있는 값을 읽는가”로 이해할 수 있다.


유틸리티 객체란 무엇인가

유틸리티 객체는 이미 있는 값을 화면에 맞게 다듬을 때 사용하는 도구다.
기본 객체가 값을 꺼내는 통로라면, 유틸리티 객체는 꺼낸 값을 보기 좋은 형태로 가공하는 도구라고 이해하면 된다.


화면에서는 값을 그대로 출력하는 것만으로 부족한 경우가 많다.
문자열 앞뒤 공백을 제거해야 할 수도 있고, 숫자에 쉼표를 넣어야 할 수도 있다.
날짜도 그대로 출력하면 보기 불편하기 때문에 yyyy-MM-dd처럼 사람이 읽기 쉬운 형식으로 바꿔야 할 때가 있다.


이럴 때 Thymeleaf가 제공하는 유틸리티 객체를 사용한다.
대표적으로 아래처럼 이해하면 된다.

  • #strings : 문자열을 다룰 때 사용한다.
  • #numbers : 숫자 형식을 다듬을 때 사용한다.
  • #dates : 기존 날짜 타입을 다룰 때 사용한다.
  • #temporals : Java 8 이후 날짜 타입을 다룰 때 자주 사용한다.

이 객체들은 값을 새로 만드는 기능이라기보다, 이미 있는 값을 화면에 알맞은 모습으로 바꾸는 기능에 가깝다.
예를 들어 #strings.trim()은 문자열의 앞뒤 공백을 제거하고, #numbers.formatInteger()는 숫자를 보기 좋게 포맷할 수 있다.
#temporals.format()은 날짜와 시간을 원하는 형식의 문자열로 바꿔 줄 수 있다.


초보자 기준에서는 유틸리티 객체를 처음부터 전부 외우려고 하면 어렵다.
먼저 “문자열, 숫자, 날짜처럼 화면에 자주 나오는 값을 보기 좋게 바꿔 주는 도구가 있다” 정도로 이해하면 된다.
필요한 상황이 생겼을 때 해당 유틸리티 객체를 찾아 쓰면 된다.


핵심은 유틸리티 객체가 값을 저장하거나 전달하는 도구가 아니라, 이미 있는 값을 화면에 맞게 가공하는 도구라는 점이다.
이 기준을 잡으면 #strings, #numbers, #dates, #temporals 같은 문법을 기능 목록이 아니라 화면 출력 보조 도구로 이해할 수 있다.


왜 이 기능들이 필요한가

초보자 기준에서는 기본 객체와 유틸리티 객체가 기능 목록처럼 보여서 복잡하게 느껴질 수 있다.
하지만 실제로는 화면을 만들 때 자주 생기는 문제를 해결하기 위한 도구들이다.
즉, 외우기 위한 문법이라기보다 화면에 필요한 값을 읽고 보기 좋게 보여 주기 위한 기능이다.


예를 들어 사용자가 URL에 ?paramData=hello처럼 값을 붙여 요청했다면, 그 값을 화면에 다시 보여 줘야 할 수 있다.
이때 요청 파라미터는 param으로 읽을 수 있다.
또 로그인 정보나 방문 횟수처럼 여러 요청에 걸쳐 유지되는 값은 session에서 읽을 수 있다.


값을 읽는 것만으로 충분하지 않은 경우도 많다.
날짜는 그대로 출력하면 보기 불편할 수 있으므로 yyyy-MM-dd HH:mm:ss처럼 정리해서 보여 주는 편이 좋다.
문자열은 앞뒤 공백을 제거해야 할 수 있고, 숫자는 쉼표를 넣어야 읽기 쉬울 수 있다.
이런 작업은 #temporals, #strings, #numbers 같은 유틸리티 객체가 도와준다.


즉, 기본 객체는 이미 들어와 있는 값을 바로 꺼내기 위해 필요하고, 유틸리티 객체는 그 값을 화면에 맞게 다듬기 위해 필요하다.
이렇게 역할을 나눠서 보면 기능이 많아 보여도 기준이 분명해진다.


핵심은 기본 객체와 유틸리티 객체를 문법 목록으로 외우는 것이 아니라, “값을 꺼내는 도구”와 “값을 보기 좋게 가공하는 도구”로 나눠 이해하는 것이다.
이 기준을 잡아 두면 뒤에서 나오는 종합 예제도 훨씬 덜 복잡하게 읽을 수 있다.


기본예제로 이해 확인하기

짧게 맛보기

아래 예제는 기본 객체와 유틸리티 객체를 가장 짧게 확인하는 예제다.
이 예제에서 봐야 할 핵심은 이미 들어와 있는 값은 기본 객체로 꺼내고, 문자열 같은 값은 유틸리티 객체로 가공할 수 있다는 점이다.


다만 param.name은 요청 주소에 name 값이 붙어 있어야 읽을 수 있다.
예를 들어 /basic?name=duke처럼 요청하면 param.name으로 duke 값을 읽을 수 있다.
또 session.sessionData는 세션에 sessionData라는 이름의 값이 저장되어 있어야 읽을 수 있다.

// exam14.html
<p th:text="${param.name}">이름</p> <!-- 요청 파라미터 name 값 출력 -->
<p th:text="${session.sessionData}">세션</p> <!-- 세션에 저장된 sessionData 값 출력 -->
<p th:text="${#strings.length('Spring')}">0</p> <!-- Spring 문자열 길이 출력 -->
// 출력결과
// duke
// 세션에 저장된 값
// 6

첫 번째 줄은 요청 파라미터에서 name 값을 읽는다.
두 번째 줄은 세션에 저장된 sessionData 값을 읽는다.
세 번째 줄은 #strings.length()를 사용해 Spring 문자열의 길이를 계산한다.


즉, param과 session은 이미 존재하는 값을 꺼내는 기본 객체이고, #strings는 문자열을 가공하는 유틸리티 객체다.
이 짧은 예제를 보면 기본 객체와 유틸리티 객체의 차이를 빠르게 잡을 수 있다.


핵심은 기본 객체는 값을 꺼내는 데 쓰이고, 유틸리티 객체는 값을 화면에 맞게 가공하는 데 쓰인다는 점이다.
이 기준을 잡아 두면 뒤의 종합 예제를 훨씬 덜 복잡하게 읽을 수 있다.


응용예제로 이해 완성하기

step2/basicobjects로 기본 객체와 스프링 빈까지 한 번에 보기

이 예제는 기본 객체, 요청 파라미터, 세션 값, spring bean, 유틸리티 객체를 한 화면에서 확인하는 종합 예제다.
코드가 길어 보이지만 기준은 단순하다.
컨트롤러에서 화면에 필요한 값을 준비하고, 템플릿에서 기본 객체와 유틸리티 객체를 이용해 그 값을 읽거나 가공한다.


먼저 HelloBean은 템플릿에서 호출할 수 있는 spring bean 예제다.
@Component("my")라고 이름을 지정했기 때문에, 템플릿에서는 @my라는 이름으로 이 객체를 사용할 수 있다.

// HelloBean.java
@Component("my") // 템플릿에서 @my 이름으로 사용할 bean 등록
class HelloBean {
    public String getHello(String data) {
        return "안녕? " + data; // 전달받은 문자열을 붙여 반환
    }
}

다음 컨트롤러에서는 날짜, 문자열, 숫자 값을 Model에 담고, 세션에는 세션 데이터와 카운트 값을 저장한다.

// ThymeleafController2.java
@GetMapping("/basicobjects") // /basicobjects 요청 처리
public String basicObjects1(HttpSession session, Model model) {
    model.addAttribute("startTime", LocalDateTime.of(2024, 8, 13, 9, 0)); // 날짜 데이터 저장
    model.addAttribute("tech", "     JAVA, SQL, JAVASCRIPT, SPRING"); // 문자열 데이터 저장
    model.addAttribute("num", 123456789); // 숫자 데이터 저장

    session.setAttribute("sessionData", "sessionData라는 이름으로 Session Scope 영역에 보관된 객체"); // 세션 데이터 저장

    if (session.getAttribute("count") == null) {
        session.setAttribute("count", new int[1]); // 최초 요청이면 카운트 배열 생성
    }

    int su[] = (int[]) session.getAttribute("count"); // 세션에서 카운트 배열 꺼내기
    su[0]++; // 요청할 때마다 카운트 증가

    return "basic/basicobjects"; // 사용할 템플릿 이름 반환
}

이 코드에서 startTime, tech, num은 Model에 담긴 값이다.
반면 sessionData와 count는 세션에 저장된 값이다.
즉, 템플릿에서는 Model 값도 읽고, 세션에 이미 저장된 값도 읽게 된다.


이제 템플릿에서는 기본 객체와 유틸리티 객체를 이용해 값을 꺼내거나 가공한다.

// basicobjects.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>표현식 기본 객체</h1>
<ul>
    <li>locale = <span th:text="${#locale}"></span></li> <!-- 현재 지역 정보 출력 -->
</ul>

<h1>편의 객체</h1>
<ul>
    <li>Request Parameter = <span th:text="${param.paramData}"></span></li> <!-- 요청 파라미터 출력 -->
    <li>spring bean = <span th:text="${@my.getHello('Spring Boot!')}"></span></li> <!-- bean 메서드 호출 -->
</ul>

<hr>
[[${#temporals.format(startTime, 'yyyy-MM-dd HH:mm:ss')}]] <!-- 날짜 형식 변경 -->
<hr>
'[[${#strings.trim(tech)}]]' <!-- 문자열 앞뒤 공백 제거 -->
<hr>
<li>
    ${#numbers.formatInteger(num, 3, 'COMMA')} =
    <span th:text="${#numbers.formatInteger(num, 3, 'COMMA')}"></span> <!-- 숫자 쉼표 형식 출력 -->
</li>
<hr>
<h3>[[${session.count[0]}]]</h3> <!-- 세션 카운트 출력 -->
</body>
</html>

여기서 param.paramData는 요청 주소에 붙어 들어온 paramData 값을 읽는다.
예를 들어 주소에 ?paramData=hello가 붙어 있으면 템플릿에서 그 값을 읽을 수 있다.


@my.getHello('Spring Boot!')는 템플릿 안에서 spring bean의 메서드를 호출하는 예제다.
HelloBean에 @Component("my")라는 이름을 붙였기 때문에, 템플릿에서는 @my로 접근한다.
즉, @ 뒤에는 등록된 bean 이름을 쓴다고 이해하면 된다.


또 이 예제는 기본 객체와 유틸리티 객체가 한 화면 안에서 함께 쓰일 수 있다는 점을 보여 준다.
요청값을 읽고, 세션값을 확인하고, 날짜와 문자열과 숫자를 화면에 맞게 가공하는 일이 모두 같은 템플릿 안에서 일어날 수 있다.


#locale, param, @my를 사용하면 지역 정보, 요청 파라미터, spring bean을 템플릿 안에서 읽거나 호출할 수 있다.


날짜·문자열·숫자 유틸리티 객체는 왜 편한가

같은 예제 안에서 #temporals, #strings, #numbers는 값을 화면에 맞게 다듬는 역할을 한다.
#temporals는 날짜와 시간을 원하는 형식으로 바꾸고, #strings는 문자열을 다루며, #numbers는 숫자를 보기 좋은 형식으로 바꾼다.


예를 들어 날짜는 그대로 출력하면 읽기 불편할 수 있다.
그래서 #temporals.format()을 사용해 yyyy-MM-dd HH:mm:ss 형식으로 바꾼다.
문자열은 앞뒤 공백이 있을 수 있으므로 #strings.trim()으로 공백을 제거할 수 있다.
숫자는 #numbers.formatInteger()를 사용해 쉼표가 들어간 형식으로 보여 줄 수 있다.


#temporals, #strings, #numbers는 날짜, 문자열, 숫자를 화면에서 읽기 좋은 형태로 바꿔 준다.


즉, 유틸리티 객체는 값을 새로 만드는 도구가 아니다.
이미 준비된 값을 화면에 맞게 보기 좋게 바꾸는 도구다.


세션 값은 다음 요청에서도 이어진다

이 예제에서는 세션에 저장한 count 값이 요청할 때마다 증가한다.
세션은 한 번 저장한 값을 여러 요청에 걸쳐 유지할 수 있기 때문이다.


컨트롤러는 처음 요청했을 때 count가 없으면 배열을 하나 만들고, 이후 요청부터는 기존 배열을 다시 꺼내 값을 증가시킨다.
그래서 같은 주소를 다시 요청하면 화면에 출력되는 카운트 값이 계속 증가한다.


세션에 저장한 count 값은 다음 요청에서도 유지되기 때문에 새로고침할 때마다 값이 증가한다.


이 흐름을 보면 요청 파라미터와 세션의 차이도 함께 이해할 수 있다.
요청 파라미터는 요청할 때 들어오는 값이고, 세션 값은 여러 요청 사이에서 유지되는 값이다.


종합 흐름으로 다시 정리하기

이 예제에서 기본 객체와 유틸리티 객체는 따로 떨어진 기능이 아니다.
실제 화면에서는 값을 읽는 작업과 값을 가공하는 작업이 함께 일어난다.


정리하면 param은 요청 파라미터를 읽고, session은 세션 값을 읽고, #locale은 현재 지역 정보를 읽는다.
그리고 #temporals, #strings, #numbers는 읽어 온 값을 화면에 맞게 다듬는다.
@my처럼 등록된 spring bean을 호출할 수도 있다.


기본 객체와 유틸리티 객체를 함께 사용하면 값을 읽고, 가공하고, 화면에 출력하는 흐름을 한 화면에서 확인할 수 있다.


핵심은 기본 객체는 이미 존재하는 값을 읽는 도구이고, 유틸리티 객체는 그 값을 화면에 맞게 가공하는 도구라는 점이다.
이 예제는 그 두 기능이 실제 템플릿 안에서 함께 사용되는 흐름을 보여 준다.


여기서 꼭 기억해야 하는 핵심

이 큰 주제에서 먼저 잡아야 할 핵심은 두 가지다.

  • 기본 객체는 이미 존재하는 값을 템플릿에서 바로 읽게 해 주는 도구다.
  • 유틸리티 객체는 읽어 온 값을 화면에 맞게 보기 좋게 가공하는 도구다.

기본 객체에는 요청 파라미터를 읽는 param, 세션 값을 읽는 session, 현재 지역 정보를 확인하는 #locale 등이 있다.
이 값들은 컨트롤러가 매번 Model에 새로 담아 주지 않아도, 요청이나 세션처럼 이미 존재하는 영역에서 꺼내 쓸 수 있다.


유틸리티 객체에는 문자열을 다루는 #strings, 숫자 형식을 다듬는 #numbers, 날짜와 시간을 가공하는 #temporals 등이 있다.
이 객체들은 값을 새로 만드는 것이 아니라, 이미 준비된 값을 화면에서 읽기 좋은 형태로 바꿔 준다.


또 템플릿에서는 @my처럼 등록된 spring bean을 호출할 수도 있다.
이 기능은 템플릿 안에서도 필요한 객체의 메서드를 사용할 수 있다는 점을 보여 준다.
다만 기본 흐름은 여전히 컨트롤러가 값을 준비하고, 템플릿은 그 값을 읽거나 가공해서 화면에 보여 주는 구조다.


즉, 이 파트의 핵심은 기본 객체와 유틸리티 객체를 문법 목록으로 외우는 것이 아니라, “값을 꺼내는 도구”와 “값을 가공하는 도구”로 나눠 이해하는 것이다.
이 기준을 잡으면 Thymeleaf에서 제공하는 객체들이 많아 보여도 역할별로 정리해서 볼 수 있다.




리터럴과 연산

값을 화면에 출력할 수 있게 되면, 그다음에는 화면에 보여 줄 문장을 조금 더 유연하게 만들 수 있어야 한다.
고정된 값을 직접 쓰기도 하고, 여러 문자열을 이어 붙이기도 하고, 숫자를 계산하거나 조건에 따라 다른 문장을 선택할 수도 있다.


이때 사용하는 개념이 리터럴과 연산이다.
리터럴은 코드 안에 직접 적어 둔 고정값이고, 연산은 값을 더하거나 비교하거나 조건에 따라 결과를 고르는 작업이다.


즉, 이 큰 주제의 핵심은 Thymeleaf 표현식 안에서 화면에 보여 줄 값을 직접 쓰고, 조합하고, 계산하고, 조건에 따라 선택할 수 있다는 점이다.
이 기준을 잡아 두면 +, | |, 삼항 연산자, Elvis, _ 같은 문법도 화면 문장을 만들기 위한 도구로 이해할 수 있다.


리터럴은 무엇인가

리터럴은 코드 안에 직접 적어 둔 고정값이다.
즉, 컨트롤러가 Model에 담아 전달한 값이 아니라, 개발자가 표현식 안에 그대로 적어 놓은 값이다.


예를 들어 문자 값인 Hello, 숫자 값인 10, 참과 거짓을 나타내는 true, false, 값이 없음을 나타내는 null이 리터럴에 해당한다.
이 값들은 어디에서 꺼내 오는 것이 아니라 코드에 직접 적혀 있기 때문에 실행할 때 그대로 사용된다.


여기서 Model 값과 리터럴을 구분해야 한다.
${data}는 Model에 담긴 data 값을 꺼내는 표현식이다.
반대로 Hello나 10처럼 코드 안에 직접 적힌 값은 리터럴이다.


Thymeleaf에서는 리터럴을 그대로 출력할 수도 있고, 다른 값과 합쳐서 더 자연스러운 문장을 만들 수도 있다.
그래서 화면 제목, 안내 문구, 상태 메시지처럼 고정 문구와 동적인 값을 함께 보여 줄 때 자주 사용한다.


핵심은 리터럴이 외부에서 전달받는 값이 아니라, 표현식 안에 직접 적어 둔 고정값이라는 점이다.
이 기준을 잡아 두면 뒤에서 문자열을 붙이거나 조건에 따라 다른 문장을 고르는 흐름도 더 쉽게 이해할 수 있다.


문자열을 붙이는 두 가지 방식

문자열을 붙인다는 것은 여러 값을 하나의 문장처럼 이어서 화면에 보여 주는 것이다.
예를 들어 고정 문구인 Hello와 Model에서 꺼낸 값인 ${data}를 합쳐 하나의 문장으로 만들 수 있다.
이때 Thymeleaf에서는 크게 + 방식과 | | 방식 두 가지를 사용할 수 있다.


첫 번째는 +를 사용하는 방식이다.
+는 문자열과 문자열, 또는 문자열과 표현식을 직접 이어 붙인다.
자바에서 문자열을 합치는 방식과 비슷해서 처음에는 이해하기 쉽다.

// exam15.html
<span th:text="'Hello ' + ${data}"></span> <!-- 문자열과 data 값을 직접 이어 붙임 -->

이 방식은 짧은 문장에서는 괜찮다.
하지만 문장이 길어지면 따옴표와 +가 많아져서 읽기 불편해질 수 있다.


두 번째는 | |를 사용하는 방식이다.
이 문법은 리터럴 대체라고 부른다.
리터럴 대체는 문장 전체를 하나의 틀처럼 적고, 그 안에 ${...} 값을 자연스럽게 끼워 넣는 방식이다.

// exam16.html
<span th:text="|Hello ${data}|"></span> <!-- 문장 안에 data 값을 자연스럽게 끼워 넣음 -->

| | 방식은 문장 형태가 그대로 보이기 때문에 사람이 읽기 더 편하다.
특히 고정 문구와 동적인 값을 섞어 긴 문장을 만들 때 + 방식보다 가독성이 좋다.


정리하면 +는 값을 직접 이어 붙이는 방식이고, | |는 문장 틀 안에 값을 끼워 넣는 리터럴 대체 방식이다.
결과가 같을 수 있어도, 문장이 길어질수록 | | 방식이 더 읽기 쉽다.


핵심은 문자열을 단순히 붙이는 것보다, 화면 문장을 읽기 쉽게 만드는 방식까지 함께 고려해야 한다는 점이다.
그래서 짧은 문장은 +로도 충분하지만, 문장 안에 값을 자연스럽게 섞어야 할 때는 | |를 쓰는 편이 더 깔끔하다.


연산은 왜 필요한가

화면에서는 값을 그대로 출력하는 일만 있는 것이 아니다.
숫자를 계산해야 할 수도 있고, 어떤 값이 기준보다 큰지 비교해야 할 수도 있다.
또 조건에 따라 서로 다른 문장을 보여 줘야 할 때도 있다.


이럴 때 사용하는 것이 연산이다.
Thymeleaf 표현식 안에서는 더하기, 빼기 같은 산술 연산뿐 아니라 비교 연산과 조건 연산도 사용할 수 있다.
즉, 연산은 화면에 보여 줄 결과를 상황에 맞게 정하는 도구라고 이해하면 된다.


예를 들어 나이가 20 이상이면 성인, 아니면 미성년자를 보여 줄 수 있다.
점수가 60 이상이면 합격, 아니면 불합격을 보여 줄 수도 있다.
이처럼 화면에 보여 줄 문장이 조건에 따라 달라질 때 연산이 필요하다.


다만 템플릿에서 모든 로직을 처리하는 것은 좋은 방식이 아니다.
복잡한 판단이나 데이터 처리는 컨트롤러나 서비스 쪽에서 하고, 템플릿에서는 화면 출력에 필요한 간단한 계산과 조건 판단 정도만 사용하는 것이 좋다.


정리하면 Thymeleaf의 연산은 아래처럼 볼 수 있다.

  • 산술 연산 : 숫자를 계산한다.
  • 비교 연산 : 두 값을 비교한다.
  • 조건 연산 : 조건에 따라 다른 결과를 선택한다.

핵심은 연산이 단순 계산용 문법이 아니라, 화면에 보여 줄 값을 상황에 맞게 선택하고 표현하기 위한 기능이라는 점이다.
이 기준을 잡아 두면 뒤에서 나오는 삼항 연산자, Elvis, _도 화면 결과를 결정하는 문법으로 이해할 수 있다.


Elvis 연산자와 No-Operation

이 파트에서 처음 보면 낯설 수 있는 문법이 Elvis 연산자와 _다.
둘 다 값이 없을 때 사용하는 문법이지만, 처리 방식은 다르다.
하나는 대체 문구를 직접 정하고, 다른 하나는 원래 적어 둔 내용을 그대로 유지한다.


Elvis 연산자는 값이 있으면 그 값을 사용하고, 값이 없으면 오른쪽에 적어 둔 기본값을 대신 사용한다.
즉, 화면에 빈칸이 나오지 않도록 대체 문구를 정해 두는 문법이다.

// exam17.html
<span th:text="${data} ?: '데이터가 없습니다.'">기본 문구</span> <!-- data가 없으면 오른쪽 문구 출력 -->

위 코드에서 data 값이 있으면 그 값이 출력된다.
하지만 data 값이 없으면 데이터가 없습니다.가 출력된다.
즉, Elvis 연산자는 값이 비어 있을 때 사용할 대체 문구를 직접 지정하는 방식이다.


반대로 _는 No-Operation이라고 부른다.
뜻 그대로 아무 작업도 하지 않겠다는 의미다.
Thymeleaf에서는 값이 없을 때 태그 안에 원래 적어 둔 기본 내용을 그대로 유지하고 싶을 때 사용할 수 있다.

// exam18.html
<span th:text="${data} ?: _">데이터가 없습니다.</span> <!-- data가 없으면 태그 안의 기본 문구 유지 -->

위 코드에서 data 값이 있으면 그 값이 출력된다.
하지만 data 값이 없으면 th:text가 태그 안의 기본 문구를 덮어쓰지 않는다.
그래서 원래 적어 둔 데이터가 없습니다.가 그대로 보인다.


둘은 비슷해 보이지만 기준이 다르다.
Elvis 연산자는 대체 문구를 표현식 안에서 직접 정한다.
반면 _는 표현식이 값을 바꾸지 않게 해서 태그 안의 기본 내용을 유지한다.


정리하면 아래처럼 이해하면 된다.

  • Elvis 연산자 : 값이 없을 때 대신 보여 줄 값을 직접 정한다.
  • _ : 값이 없을 때 태그 안에 원래 적어 둔 기본 내용을 유지한다.

핵심은 Elvis 연산자와 _가 모두 값이 없을 때를 처리하지만, 하나는 대체값을 직접 지정하고 다른 하나는 기존 내용을 유지한다는 점이다.
이 차이를 잡아 두면 뒤의 연산 예제에서 ?: '데이터가 없습니다.'와 ?: _를 헷갈리지 않고 읽을 수 있다.


기본예제로 이해 확인하기

가장 짧은 형태로 먼저 보기

아래 예제는 리터럴 대체와 조건 연산을 가장 짧게 확인하는 예제다.
이 예제에서는 컨트롤러가 Model에 name 값으로 둘리, age 값으로 20을 담아 두었다고 가정한다.


첫 번째 줄은 | |를 사용해 고정 문구인 Hello와 ${name} 값을 하나의 문장으로 만든다.
두 번째 줄은 삼항 연산자를 사용해 age 값이 20 이상인지 판단하고, 조건에 따라 다른 문장을 출력한다.

// exam20.html
<p th:text="|Hello ${name}|">문장</p> <!-- Hello와 name 값을 한 문장으로 출력 -->
<p th:text="${age >= 20 ? '성인' : '미성년자'}">결과</p> <!-- age 조건에 따라 문장 선택 -->
// 출력결과
// Hello 둘리
// 성인

첫 번째 결과는 Hello라는 고정 문구와 둘리라는 값을 합친 결과다.
여기서 |Hello ${name}|는 문장 틀 안에 ${name} 값을 끼워 넣는 리터럴 대체 방식이다.


두 번째 결과는 age 값이 20 이상이기 때문에 성인이 선택된 결과다.
만약 age 값이 20보다 작았다면 미성년자가 출력된다.


즉, Thymeleaf 표현식은 단순히 값을 그대로 출력하는 데서 끝나지 않는다.
화면에 보여 줄 문장을 만들고, 조건에 따라 어떤 문장을 보여 줄지 선택하는 데도 사용할 수 있다.


응용예제로 이해 완성하기

문자열 리터럴과 리터럴 대체 문법 보기

이제 실제 실습 예제로 올라가면 문자열을 직접 이어 붙이는 방식과 리터럴 대체 방식을 한 화면에서 비교할 수 있다.
이 예제는 리터럴이 단순히 코드에 적힌 고정값으로 끝나는 것이 아니라, Model에서 전달받은 값과 합쳐져 화면 문장을 만들 수 있다는 점을 보여 준다.


먼저 컨트롤러는 data라는 이름으로 Spring Boot! 값을 Model에 담는다.
그러면 템플릿에서는 ${data}로 이 값을 꺼내 문자열과 함께 사용할 수 있다.

// ThymeleafController3.java
@GetMapping("/literal") // /literal 요청 처리
public String literal(Model model) {
    model.addAttribute("data", "Spring Boot!"); // data 이름으로 문자열 저장
    return "basic/literal"; // 사용할 템플릿 이름 반환
}

이제 템플릿에서 리터럴과 ${data} 값을 여러 방식으로 조합한다.

// literal.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>리터럴</h1>
<ul>
    <li>'hello' + ' world!' = <span th:text="'Hello' + ' world!'"></span></li> <!-- 리터럴끼리 직접 연결 -->
    <li>'hello world!' = <span th:text="'Hello world!'"></span></li> <!-- 하나의 리터럴 출력 -->
    <li>'hello ' + ${data} = <span th:text="'Hello ' + ${data}"></span></li> <!-- 리터럴과 Model 값 연결 -->
    <li>리터럴 대체 |hello ${data}| = <span th:text="|Hello ${data}|"></span></li> <!-- 문장 안에 data 값 삽입 -->
</ul>
</body>
</html>

첫 번째 줄은 Hello와 world!라는 문자열 리터럴을 +로 직접 이어 붙인다.
두 번째 줄은 이미 하나로 완성된 문자열 리터럴을 그대로 출력한다.
세 번째 줄은 고정 문구인 Hello와 Model에서 꺼낸 ${data} 값을 +로 이어 붙인다.


네 번째 줄은 |Hello ${data}|처럼 리터럴 대체 방식을 사용한다.
이 방식은 문장 틀 안에 ${data} 값을 끼워 넣는 구조라서, 긴 문장을 만들 때 훨씬 읽기 쉽다.

+는 문자열을 직접 이어 붙이고, | | 리터럴 대체는 문장 안에 값을 자연스럽게 끼워 넣는다.


핵심은 리터럴과 Model 값을 함께 사용하면 화면에 보여 줄 문장을 더 자연스럽게 만들 수 있다는 점이다.
짧은 문장은 +로도 충분하지만, 문장 형태를 그대로 보이게 만들고 싶다면 | | 리터럴 대체가 더 읽기 쉽다.


산술, 비교, 조건 연산과 기본값 처리 보기

다음 예제는 Thymeleaf 표현식 안에서 산술 연산, 비교 연산, 조건 연산, 엘비스 연산자, _를 함께 확인하는 예제다.
이 예제에서 봐야 할 핵심은 표현식이 단순 출력만 하는 것이 아니라, 화면에 보여 줄 결과를 계산하고 선택할 수도 있다는 점이다.


먼저 컨트롤러는 nullData, data, num1, num2 값을 Model에 담는다.
여기서 nullData는 값이 없는 상황을 보여 주기 위해 일부러 null로 넣은 값이다.

// ThymeleafController3.java
@GetMapping("/operation") // /operation 요청 처리
public String operation(Model model) {
    model.addAttribute("nullData", null); // 값이 없는 상황 저장
    model.addAttribute("data", "Spring Boot!"); // 값이 있는 문자열 저장
    model.addAttribute("num1", 30); // 숫자 데이터 저장
    model.addAttribute("num2", 0); // 숫자 데이터 저장
    return "basic/operation"; // 사용할 템플릿 이름 반환
}

이제 템플릿에서 숫자를 계산하고, 조건을 비교하고, 값이 없을 때 어떤 결과를 보여 줄지 정한다.

// operation.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    <li>10 + 2 = <span th:text="10 + 2"></span></li> <!-- 숫자 직접 계산 -->
    <li>${num1 + 2} = <span th:text="${num1 + 2}"></span></li> <!-- Model 값과 숫자 계산 -->
    <li>10 % 2 == 0 = <span th:text="10 % 2 == 0"></span></li> <!-- 나머지 계산 후 비교 -->
    <li>${num1 % 2 == 0} = <span th:text="${num1 % 2 == 0}"></span></li> <!-- Model 값으로 짝수 여부 비교 -->
    <li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)? '짝수':'홀수'"></span></li> <!-- 조건에 따라 문장 선택 -->
    <li>${nullData}?: '데이터가 없습니다.' = <span th:text="${nullData}?: '데이터가 없습니다.'"></span></li> <!-- 값이 없으면 대체 문구 출력 -->
    <li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</span></li> <!-- 값이 있으면 data 출력 -->
    <li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가없습니다.</span></li> <!-- 값이 없으면 기본 내용 유지 -->
</ul>
</body>
</html>

이 예제에서 10 + 2는 숫자를 직접 계산하는 산술 연산이다.
${num1 + 2}는 Model에서 꺼낸 num1 값에 2를 더하는 계산이다.
즉, 표현식 안에서는 직접 적은 숫자와 Model에서 꺼낸 값을 함께 계산할 수 있다.


10 % 2 == 0과 ${num1 % 2 == 0}은 나머지 연산과 비교 연산을 함께 사용한 예제다.
%는 나머지를 구하는 연산자다.
어떤 수를 2로 나눴을 때 나머지가 0이면 짝수라고 판단할 수 있다.
그래서 10 % 2 == 0도 true이고, num1이 30이므로 ${num1 % 2 == 0}도 true가 된다.


(10 % 2 == 0)? '짝수':'홀수'는 삼항 연산자다.
조건이 참이면 짝수를 선택하고, 거짓이면 홀수를 선택한다.
즉, 조건에 따라 화면에 보여 줄 문장을 고르는 방식이다.


${nullData}?: '데이터가 없습니다.'는 엘비스 연산자를 사용한 예제다.
nullData는 값이 없기 때문에 오른쪽에 적은 데이터가 없습니다.가 출력된다.
이 방식은 값이 없을 때 대체 문구를 직접 지정할 때 사용한다.


${data}?: _와 ${nullData}?: _는 _를 사용한 예제다.
data처럼 값이 있으면 그 값이 출력된다.
반대로 nullData처럼 값이 없으면 _ 때문에 태그 안에 원래 적어 둔 기본 문구가 그대로 유지된다.
즉, _는 값을 바꾸지 않고 기존 내용을 유지하는 처리라고 보면 된다.

산술 연산, 비교 연산, 조건 연산, 엘비스 연산자, _를 사용하면 화면 결과를 상황에 맞게 정할 수 있다.


핵심은 Thymeleaf 표현식 안에서 계산, 비교, 조건 선택, 기본값 처리를 할 수 있다는 점이다.
다만 복잡한 로직을 템플릿에 많이 넣기보다, 화면 출력에 필요한 간단한 판단 정도로 사용하는 것이 좋다.


여기서 꼭 기억해야 하는 핵심

이 큰 주제에서 먼저 잡아야 할 핵심은 네 가지다.

  • 리터럴은 표현식 안에 직접 적어 둔 고정값이다.
  • +는 문자열을 직접 이어 붙이는 방식이다.
  • | |는 문장 틀 안에 값을 끼워 넣는 리터럴 대체 방식이다.
  • 연산은 계산, 비교, 조건 선택, 기본값 처리를 통해 화면 결과를 정하는 기능이다.

문자열을 만들 때는 +와 | |를 상황에 맞게 사용할 수 있다.
짧은 문장을 단순히 이어 붙일 때는 +도 충분하다.
하지만 문장 안에 값을 자연스럽게 섞어야 한다면 | | 리터럴 대체가 더 읽기 쉽다.


연산도 단순 계산만 의미하지 않는다.
산술 연산은 숫자를 계산하고, 비교 연산은 값을 비교하고, 삼항 연산자는 조건에 따라 다른 결과를 선택한다.
또 Elvis 연산자는 값이 없을 때 대체 문구를 직접 정하고, _는 값이 없을 때 태그 안의 기본 내용을 유지한다.


다만 템플릿은 화면을 만드는 곳이다.
복잡한 비즈니스 로직까지 템플릿에서 처리하기보다, 화면 출력에 필요한 간단한 문장 조합과 조건 판단 정도로 사용하는 것이 좋다.


즉, 이 파트의 핵심은 리터럴과 연산을 단순 문법으로 외우는 것이 아니라, 화면에 보여 줄 문장을 만들고 상황에 맞는 결과를 선택하는 도구로 이해하는 것이다.
이 기준을 잡으면 +, | |, 삼항 연산자, Elvis, _가 각각 왜 필요한지 한 흐름으로 정리된다.




조건 처리와 반복 처리

화면을 만들다 보면 모든 내용을 항상 보여 주는 것은 아니다.
어떤 내용은 조건이 맞을 때만 보여 줘야 하고, 어떤 내용은 여러 데이터를 같은 모양으로 반복해서 보여 줘야 한다.
이때 사용하는 문법이 조건 처리와 반복 처리다.


조건 처리는 값에 따라 태그를 보여 줄지 말지를 결정하는 기능이다.
예를 들어 나이가 기준보다 어리면 시청 불가를 보여 주고, 기준을 넘으면 시청 가능을 보여 줄 수 있다.
반복 처리는 List처럼 여러 값이 들어 있는 데이터를 하나씩 꺼내 같은 구조로 출력하는 기능이다.
예를 들어 회원 목록, 게시글 목록, 상품 목록을 한 줄씩 보여 줄 때 사용한다.


즉, 이 큰 주제의 핵심은 조건 처리는 화면에 남길 내용을 결정하고, 반복 처리는 여러 데이터를 같은 구조로 반복 출력한다는 점을 구분하는 것이다.
이 기준을 잡아 두면 th:if, th:unless, th:switch, th:case, th:each, th:block을 각각 어떤 상황에서 쓰는지 더 쉽게 이해할 수 있다.


조건을 참과 거짓으로 나누는 방식 (th:if와 th:unless)

th:if는 조건이 참일 때만 해당 태그를 화면에 남긴다.
반대로 th:unless는 조건이 거짓일 때 해당 태그를 화면에 남긴다.
둘 다 조건에 따라 화면 결과를 바꾸는 문법이지만, 판단 기준은 서로 반대다.


초보자 기준에서는 이렇게 이해하면 쉽다.
th:if는 “조건이 맞으면 보여 줘”이고, th:unless는 “조건이 맞지 않으면 보여 줘”이다.
즉, 같은 상황을 참 기준으로 표현할 수도 있고, 거짓 기준으로 표현할 수도 있다.


여기서 중요한 점은 단순히 눈에만 안 보이게 숨기는 것이 아니라는 것이다.
조건이 맞지 않으면 해당 태그는 Thymeleaf가 만든 최종 HTML 결과에서 빠질 수 있다.
그래서 조건 처리 문법은 화면에 무엇을 남길지 결정하는 역할을 한다.


예를 들어 나이가 14보다 작으면 시청 불가를 보여 주고, 그렇지 않으면 시청 가능을 보여 줄 수 있다.
이런 식으로 조건 처리 문법은 같은 템플릿을 사용하더라도 전달된 값에 따라 다른 화면을 만들 수 있게 해 준다.


핵심은 th:if와 th:unless가 조건에 따라 태그를 최종 화면에 남길지 말지를 결정하는 문법이라는 점이다.
이 차이를 잡아 두면 뒤에서 같은 조건을 두 방식으로 표현하는 예제도 헷갈리지 않고 읽을 수 있다.


여러 경우 중 하나를 고르는 방식 (th:switch와 th:case)

조건이 여러 개일 때 th:if를 계속 사용할 수도 있다.
하지만 하나의 기준값을 놓고 여러 경우 중 하나를 고르는 상황이라면 th:switch와 th:case를 사용하는 편이 더 읽기 쉽다.


th:switch는 비교할 기준값을 정하는 속성이다.
th:case는 그 기준값과 비교할 각각의 경우를 정하는 속성이다.
즉, th:switch가 “무엇을 기준으로 볼 것인가”를 정하고, th:case가 “그 값이 무엇일 때 무엇을 보여 줄 것인가”를 정한다.


예를 들어 나이가 10이면 10살, 20이면 20살, 그 외의 값이면 기타를 보여 줄 수 있다.
이처럼 하나의 값을 기준으로 여러 결과 중 하나를 선택할 때 th:switch와 th:case가 잘 어울린다.


여기서 th:case="*"는 앞의 어떤 경우에도 해당하지 않을 때 실행되는 기본 경우다.
쉽게 말해 else처럼 마지막에 남은 경우를 처리하는 역할이라고 이해하면 된다.


정리하면 th:if는 조건을 하나씩 검사하는 방식이고, th:switch는 기준값 하나로 여러 경우를 나누는 방식이다.
둘 다 조건 처리 문법이지만, 상황에 따라 더 읽기 쉬운 문법을 선택하면 된다.


핵심은 th:switch는 기준값을 정하고, th:case는 그 기준값에 맞는 경우를 골라 화면에 남기는 문법이라는 점이다.
이 기준을 잡아 두면 표 안에서 나이별 문구를 다르게 보여 주는 예제도 쉽게 읽을 수 있다.


반복 출력과 반복 상태값 (th:each)

th:each는 여러 데이터를 하나씩 꺼내 반복 출력하는 문법이다.
쉽게 말해 Java의 for문처럼 동작한다고 이해하면 된다.
그래서 회원 목록, 게시글 목록, 상품 목록처럼 같은 구조를 여러 번 보여 줘야 할 때 자주 사용한다.


예를 들어 users라는 List에 사용자 객체가 여러 개 들어 있다면, th:each는 그 안의 사용자를 하나씩 꺼내 같은 태그 구조로 출력할 수 있다.
즉, 데이터 개수만큼 같은 화면 구조가 반복해서 만들어진다.


th:each는 반복할 데이터만 꺼낼 수도 있고, 반복 상태값을 함께 사용할 수도 있다.
반복 상태값은 현재 반복이 몇 번째인지, 첫 번째인지, 마지막인지 같은 정보를 알려 주는 값이다.
이 값이 있으면 단순히 목록을 출력하는 것을 넘어서 순번을 붙이거나 특정 줄만 다르게 처리할 수 있다.


대표적인 반복 상태값은 아래와 같다.

  • index : 0부터 시작하는 위치다.
  • count : 1부터 시작하는 순번이다.
  • size : 전체 데이터 개수다.
  • current : 현재 반복 중인 데이터다.
  • even / odd : 현재 반복이 짝수 번째인지 홀수 번째인지 알려 준다.
  • first / last : 현재 데이터가 처음인지 마지막인지 알려 준다.

여기서 index와 count는 꼭 구분해야 한다.
index는 개발에서 자주 쓰는 위치 번호라서 0부터 시작한다.
반대로 count는 화면에 보여 주기 좋은 순번이라서 1부터 시작한다.


예를 들어 첫 번째 데이터라면 index는 0이고, count는 1이다.
그래서 화면에 번호를 붙일 때는 보통 count를 쓰는 편이 더 자연스럽다.


핵심은 th:each가 여러 데이터를 반복 출력하는 문법이고, 반복 상태값을 함께 쓰면 현재 반복 위치와 상태까지 화면에 활용할 수 있다는 점이다.
이 기준을 잡아 두면 뒤에서 표 형태로 사용자 목록을 출력하는 예제도 쉽게 읽을 수 있다.


화면에 태그를 남기지 않고 묶는 방식 (th:block)

조건 처리나 반복 처리를 할 때는 여러 태그를 한 번에 묶어서 제어해야 할 때가 있다.
예를 들어 제목과 설명을 함께 조건에 따라 보여 주거나, 여러 줄의 내용을 한 묶음으로 반복 출력해야 할 수 있다.
이럴 때 사용할 수 있는 문법이 th:block이다.


th:block은 실제 화면에 남기기 위한 태그가 아니다.
Thymeleaf가 템플릿을 처리하는 동안 여러 태그를 묶기 위해 사용하는 임시 태그라고 이해하면 된다.
즉, 조건이나 반복을 적용할 때는 사용되지만, 최종 HTML 결과에는 th:block 자체가 남지 않는다.


이 점이 중요한 이유는 화면 구조를 불필요하게 복잡하게 만들지 않기 위해서다.
일반 <div>로 묶으면 최종 HTML에 <div>가 남는다.
하지만 단순히 조건이나 반복을 적용하기 위해 묶는 것이라면, 화면에 남는 태그가 꼭 필요하지 않을 수 있다.
그럴 때 th:block을 사용하면 불필요한 태그 없이 여러 요소를 함께 제어할 수 있다.


초보자 기준에서는 th:block을 “화면에 보이지 않는 묶음 상자”라고 이해하면 쉽다.
안쪽 태그들을 함께 조건 처리하거나 반복 처리할 수 있지만, 정작 th:block이라는 상자는 최종 화면에 남지 않는다.


핵심은 th:block이 여러 태그를 함께 제어하기 위한 임시 묶음이고, 최종 HTML에는 태그 자체가 남지 않는다는 점이다.
이 기준을 잡아 두면 뒤에서 여러 줄을 반복 출력하는 예제도 쉽게 이해할 수 있다.


기본예제로 이해 확인하기

아주 짧은 조건 예제

아래 예제는 th:if와 th:unless가 조건을 서로 반대 기준으로 처리한다는 점을 보여 준다.
두 줄은 서로 다른 상황을 보여 주는 것이 아니라, 같은 결과를 참 기준과 거짓 기준으로 표현한 예제다.


여기서는 info.age 값이 20이라고 가정한다.
20은 18보다 크기 때문에 첫 번째 줄의 조건은 참이 된다.
또 20은 18 이하가 아니기 때문에 두 번째 줄의 unless 조건도 결과적으로 화면에 남게 된다.

// exam16.html
<p th:if="${info.age > 18}">입장 가능</p> <!-- age가 18보다 크면 출력 -->
<p th:unless="${info.age <= 18}">입장 가능</p> <!-- age가 18 이하가 아니면 출력 -->
// 출력결과
// 입장 가능
// 입장 가능

이 예제에서 중요한 점은 두 문법이 서로 반대 기준을 가진다는 것이다.
th:if는 조건이 참일 때 태그를 남기고, th:unless는 조건이 거짓일 때 태그를 남긴다.


즉, info.age가 20이면 두 조건 모두 입장 가능을 화면에 남길 수 있다.
다만 실제 화면을 만들 때는 같은 문구를 두 번 출력하기보다, 보통 상황에 맞게 둘 중 하나를 선택해서 사용한다.


핵심은 th:if와 th:unless가 같은 상황을 참 기준 또는 거짓 기준으로 표현할 수 있다는 점이다.


아주 짧은 반복 예제

아래 예제는 th:each가 여러 데이터를 하나씩 꺼내 반복 출력하는 방식을 보여 준다.
여기서는 datas 안에 사과, 바나나, 딸기가 들어 있다고 가정한다.


th:each="data, status : ${datas}"는 datas에서 값을 하나씩 꺼내 data라는 이름으로 사용하겠다는 뜻이다.
그리고 status는 현재 반복의 순번이나 위치 같은 반복 상태값을 알려 준다.

// exam17.html
<th:block th:each="data, status : ${datas}"> <!-- datas에서 값을 하나씩 꺼내 반복 -->
    <h1 th:text="|${status.count} ${data}|"></h1> <!-- 순번과 현재 데이터 출력 -->
</th:block>
// 출력결과
// 1 사과
// 2 바나나
// 3 딸기

여기서 status.count는 1부터 시작하는 순번이다.
그래서 첫 번째 데이터인 사과 앞에는 1이 붙고, 두 번째 데이터인 바나나 앞에는 2가 붙는다.


또 th:block을 사용했기 때문에 여러 데이터를 반복 처리할 수 있지만, 최종 HTML에는 th:block 자체가 남지 않는다.
즉, 반복을 적용하기 위한 임시 묶음으로만 사용된다.


핵심은 th:each가 여러 데이터를 하나씩 꺼내 같은 구조로 반복 출력하고, 반복 상태값을 함께 사용하면 순번 같은 정보도 출력할 수 있다는 점이다.


응용예제로 이해 완성하기

같은 템플릿도 전달 값이 달라지면 결과가 달라진다

아래 예제는 같은 템플릿 파일을 사용하지만, 컨트롤러가 어떤 값을 넘기느냐에 따라 화면 결과가 달라지는 흐름을 보여 준다.
조건 처리는 템플릿 파일 자체를 바꾸는 것이 아니라, 전달된 값에 따라 최종 화면에 남는 태그를 다르게 만드는 방식이다.


먼저 두 요청은 모두 basic/ifcondition 템플릿을 사용한다.
하지만 age, movie, today 값이 서로 다르기 때문에 화면 결과도 달라진다.

// ThymeleafController1.java
@GetMapping("/conditiontest1") // 첫 번째 조건 테스트 요청
public String conditiontest1(Model model) {
    model.addAttribute("age", 13); // 나이 저장
    model.addAttribute("movie", "겨울왕국"); // 영화 제목 저장
    model.addAttribute("today", "금요일"); // 요일 저장
    return "basic/ifcondition"; // 같은 템플릿 반환
}

@GetMapping("/conditiontest2") // 두 번째 조건 테스트 요청
public String conditiontest2(Model model) {
    model.addAttribute("age", 14); // 나이 저장
    model.addAttribute("movie", "아기공룡둘리"); // 영화 제목 저장
    model.addAttribute("today", "월요일"); // 요일 저장
    return "basic/ifcondition"; // 같은 템플릿 반환
}

이제 템플릿에서는 전달된 값에 따라 어떤 태그를 화면에 남길지 결정한다.

// ifcondition.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>if, unless</h1>
    <hr>
    <h3 th:text="'시청 불가'" th:if="${age lt 14}"></h3> <!-- age가 14보다 작으면 출력 -->
    <h3 th:text="'시청 가능'" th:unless="${age lt 14}"></h3> <!-- age가 14보다 작지 않으면 출력 -->
    <hr>
    <ul th:if="${movie == '겨울왕국'}"> <!-- 영화가 겨울왕국일 때만 출력 -->
        <li>엘사</li>
        <li>안나</li>
        <li>올라프</li>
    </ul>
    <hr>
    <th:block th:if="${today == '금요일'}"> <!-- 금요일일 때 여러 태그를 함께 출력 -->
        <h2>즐거운 금요일</h2>
        <h3>행복한 금요일</h3>
    </th:block>
</body>
</html>

첫 번째 요청은 age가 13, movie가 겨울왕국, today가 금요일이다.
그래서 시청 불가, 겨울왕국 등장인물 목록, 금요일 문구가 화면에 남는다.

첫 번째 요청은 조건에 맞는 값이 많기 때문에 여러 태그가 함께 화면에 남는다.


두 번째 요청은 age가 14, movie가 아기공룡둘리, today가 월요일이다.
그래서 age lt 14 조건은 거짓이 되고, movie == '겨울왕국'과 today == '금요일' 조건도 거짓이 된다.
결과적으로 화면에는 조건에 맞는 내용만 남는다.

두 번째 요청은 전달 값이 달라졌기 때문에 같은 템플릿을 사용해도 다른 결과가 출력된다.


핵심은 같은 템플릿이라도 Model에 담긴 값이 달라지면 조건 판단 결과가 달라지고, 최종 화면에 남는 태그도 달라진다는 점이다.


표 안에서 조건 처리와 분기 처리 보기

이번 예제는 사용자 목록을 반복 출력하면서, 각 사용자마다 조건 처리와 분기 처리를 함께 적용하는 예제다.
즉, 반복 처리와 조건 처리가 실제 화면에서는 따로 떨어져 있지 않고 함께 사용될 수 있다는 점을 보여 준다.


먼저 컨트롤러는 사용자 목록을 Model에 담고 basic/condition 템플릿을 반환한다.

// ThymeleafController3.java
@GetMapping("/conditiontest3") // 조건 처리 예제 요청
public String condition(Model model) {
    addUsers(model); // 사용자 목록을 Model에 저장
    return "basic/condition"; // 사용할 템플릿 반환
}

템플릿에서는 th:each로 사용자 목록을 반복하고, 각 사용자 나이에 따라 조건문과 분기문을 적용한다.

// condition.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>if, unless</h1>
<table>
    <tr>
        <th>count</th>
        <th>username</th>
        <th>age</th>
    </tr>
    <tr th:each="user, userStat : ${users}"> <!-- 사용자 목록 반복 -->
        <td th:text="${userStat.count}">1</td> <!-- 1부터 시작하는 순번 출력 -->
        <td th:text="${user.userName}">username</td> <!-- 사용자 이름 출력 -->
        <td>
            <span th:text="${user.age}">0</span> <!-- 나이 출력 -->
            <span th:text="'미성년자'" th:if="${user.age lt 20}"></span> <!-- 20보다 작으면 출력 -->
            <span th:text="'미성년자'" th:unless="${user.age ge 20}"></span> <!-- 20 이상이 아니면 출력 -->
        </td>
    </tr>
</table>

<h1>switch</h1>
<table border="1">
    <tr>
        <th>count</th>
        <th>username</th>
        <th>age</th>
    </tr>
    <tr th:each="user, userStat : ${users}"> <!-- 사용자 목록 반복 -->
        <td th:text="${userStat.count}">1</td> <!-- 순번 출력 -->
        <td th:text="${user.userName}">username</td> <!-- 사용자 이름 출력 -->
        <td th:switch="${user.age}"> <!-- 나이를 기준으로 경우 나누기 -->
            <span th:case="10">10살</span> <!-- 나이가 10이면 출력 -->
            <span th:case="20">20살</span> <!-- 나이가 20이면 출력 -->
            <span th:case="*">기타</span> <!-- 앞의 경우가 아니면 출력 -->
        </td>
    </tr>
</table>
</body>
</html>

첫 번째 표는 if와 unless를 함께 보여 주는 예제다.
여기서 미성년자 문구가 th:if와 th:unless에 모두 들어가 있다.
그래서 나이가 20보다 작은 경우에는 같은 문구가 두 번 나올 수 있다.


이것은 실무에서 권장되는 출력 방식이라기보다, th:if와 th:unless가 같은 상황을 참 기준과 거짓 기준으로 표현할 수 있음을 보여 주기 위한 설명용 코드로 보면 된다.
실제 화면에서는 보통 둘 중 하나만 선택해서 사용하는 편이 자연스럽다.


두 번째 표는 th:switch와 th:case를 사용한다.
나이라는 기준값 하나를 두고 10, 20, 그 외의 경우를 나눠서 출력한다.
즉, 여러 조건을 하나씩 나열하기보다 기준값 하나로 경우를 나눌 때 사용하기 좋다.

반복 출력 안에서도 if, unless, switch, case를 함께 사용해 사용자별 화면 결과를 다르게 만들 수 있다.


핵심은 반복으로 여러 데이터를 출력하면서도, 각 데이터의 값에 따라 조건 처리와 분기 처리를 함께 적용할 수 있다는 점이다.


반복 상태값까지 함께 보기

이번 예제는 th:each가 단순히 목록만 반복 출력하는 것이 아니라, 현재 반복 상태까지 함께 다룰 수 있음을 보여 준다.
반복 상태값을 사용하면 순번, 인덱스, 전체 개수, 처음 여부, 마지막 여부 같은 정보를 화면에 출력할 수 있다.


먼저 컨트롤러는 사용자 목록을 Model에 담고 basic/each 템플릿을 반환한다.

// ThymeleafController3.java
@GetMapping("/each") // 반복 처리 예제 요청
public String each(Model model) {
    addUsers(model); // 사용자 목록을 Model에 저장
    return "basic/each"; // 사용할 템플릿 반환
}

템플릿에서는 첫 번째 표로 기본 반복을 보여 주고, 두 번째 표로 반복 상태값을 함께 보여 준다.

// each.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
    <h1>기본 테이블</h1>
    <table>
        <tr>
            <th>username</th>
            <th>age</th>
        </tr>
        <tr th:each="user : ${users}"> <!-- users에서 user를 하나씩 꺼내 반복 -->
            <td th:text="${user.userName}">사용자명</td> <!-- 사용자 이름 출력 -->
            <td th:text="${user.age}">0</td> <!-- 사용자 나이 출력 -->
        </tr>
    </table>

    <h1>반복 상태 유지</h1>
    <table>
        <tr>
            <th>count</th>
            <th>username</th>
            <th>age</th>
            <th>etc</th>
        </tr>
        <tr th:each="user, userStat : ${users}"> <!-- user와 반복 상태값을 함께 사용 -->
            <td th:text="${userStat.count}">username</td> <!-- 1부터 시작하는 순번 출력 -->
            <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>
</body>
</html>

첫 번째 표는 가장 기본적인 반복 출력이다.
users 안의 사용자 객체를 하나씩 꺼내서 이름과 나이를 출력한다.


두 번째 표는 반복 상태값을 함께 출력한다.
userStat.index는 0부터 시작하는 위치이고, userStat.count는 1부터 시작하는 순번이다.
그래서 화면에 순번을 보여 줄 때는 보통 count가 더 자연스럽다.


또 first와 last를 사용하면 현재 데이터가 첫 번째인지 마지막인지 알 수 있다.
이 값들은 실제 화면에서 첫 번째 줄만 강조하거나 마지막 줄만 다르게 처리할 때 활용할 수 있다.

th:each와 반복 상태값을 함께 사용하면 현재 반복 위치와 상태까지 화면에 출력할 수 있다.


핵심은 th:each가 데이터를 반복 출력할 뿐 아니라, 반복 상태값을 통해 현재 위치와 상태도 함께 다룰 수 있다는 점이다.


화면에 태그를 남기지 않고 여러 요소 반복하기

마지막 예제는 th:block을 사용해 여러 태그를 함께 반복하면서도 최종 화면에는 th:block 자체를 남기지 않는 구조를 보여 준다.
즉, 반복 처리를 위해 임시로 묶지만, 실제 결과 HTML에는 불필요한 묶음 태그를 남기지 않는 방식이다.


먼저 컨트롤러는 사용자 목록을 Model에 담고 basic/block 템플릿을 반환한다.

// ThymeleafController3.java
@GetMapping("/block") // block 예제 요청
public String block(Model model) {
    addUsers(model); // 사용자 목록을 Model에 저장
    return "basic/block"; // 사용할 템플릿 반환
}

템플릿에서는 th:block으로 두 개의 <div>를 함께 묶어서 반복한다.

// block.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
    span {
        color : blue;
    }
</style>
</head>
<body>
    <th:block th:each="user : ${users}"> <!-- users에서 user를 하나씩 꺼내 반복 -->
        <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>
</body>
</html>

이 예제에서 th:block은 두 개의 <div>를 한 묶음으로 반복하기 위해 사용된다.
하지만 최종 HTML에는 th:block 태그 자체가 남지 않는다.
화면에는 안쪽의 <div>들만 반복되어 출력된다.


만약 단순히 <div>로 전체를 감싸면 그 <div>도 최종 HTML에 남는다.
하지만 반복 처리를 위해 임시로 묶기만 하면 되는 상황이라면 th:block을 쓰는 것이 더 깔끔하다.

th:block은 여러 요소를 함께 반복할 수 있게 해 주지만, 최종 화면에는 태그 자체가 남지 않는다.


핵심은 th:block이 여러 태그를 함께 조건 처리하거나 반복 처리하기 위한 임시 묶음이고, 최종 HTML에는 남지 않는다는 점이다.


여기서 꼭 기억해야 하는 핵심

이 큰 주제에서 먼저 잡아야 할 핵심은 네 가지다.

  • th:if와 th:unless는 조건에 따라 태그가 최종 HTML에 남을지 빠질지를 결정한다.
  • th:switch와 th:case는 기준값 하나를 놓고 여러 경우 중 맞는 결과를 고른다.
  • th:each는 여러 데이터를 하나씩 꺼내 같은 구조로 반복 출력한다.
  • th:block은 여러 태그를 함께 제어하기 위한 임시 묶음이고, 최종 HTML에는 남지 않는다.

조건 처리는 화면에 무엇을 보여 줄지 결정하는 기능이다.
th:if는 조건이 참일 때 태그를 남기고, th:unless는 조건이 거짓일 때 태그를 남긴다.
th:switch와 th:case는 하나의 기준값을 여러 경우로 나누어 처리할 때 사용한다.


반복 처리는 여러 데이터를 같은 구조로 출력하는 기능이다.
th:each를 사용하면 List 안의 값을 하나씩 꺼내 반복할 수 있고, 반복 상태값을 함께 사용하면 순번, 인덱스, 처음 여부, 마지막 여부 같은 정보도 화면에 활용할 수 있다.


th:block은 조건 처리나 반복 처리를 위해 여러 태그를 묶을 때 사용한다.
하지만 실제 화면에 남는 태그가 아니므로, 불필요한 구조를 만들지 않고 여러 요소를 함께 제어할 수 있다.


즉, 이 파트의 핵심은 조건 처리는 화면에 남길 내용을 결정하고, 반복 처리는 여러 데이터를 같은 구조로 출력하며, th:block은 그 흐름을 태그 없이 묶어 주는 도구라는 점이다.
이 기준을 잡으면 조건 처리와 반복 처리를 실제 화면 구성 흐름 안에서 이해할 수 있다.




링크 표현식과 실제 요청 결과

Thymeleaf에서 다른 요청으로 이동하거나 특정 값을 함께 보내려면 링크 주소를 만들어야 한다.
이때 자주 사용하는 문법이 @{...}이고, 이 문법은 링크 URL 표현식이라고 부른다.
링크 URL 표현식은 화면에서 클릭할 주소를 만들고, 필요하면 요청에 전달할 값까지 함께 붙여 주는 문법이다.


링크는 단순히 주소 문자열을 화면에 적는 것으로 끝나지 않는다.
주소를 만들고, 그 주소를 클릭했을 때 어떤 컨트롤러로 요청이 들어가는지, 그리고 어떤 결과 화면이 나오는지까지 이어서 봐야 한다.
그래야 query parameter와 path variable의 차이도 실제 요청 흐름으로 이해할 수 있다.


즉, 이 큰 주제의 핵심은 @{...}로 요청 주소를 만들고, 값을 어떤 방식으로 전달하며, 그 요청이 실제 화면 결과로 어떻게 이어지는지 확인하는 것이다.
이 기준을 잡아 두면 링크 표현식을 단순 주소 작성 문법이 아니라 요청 흐름을 만드는 문법으로 이해할 수 있다.


요청 주소를 만드는 표현식을 따로 배우는 이유

링크 주소는 일반 문자열로 직접 쓸 수도 있다.
하지만 Thymeleaf에서는 링크를 만들 때 @{...}(링크 URL 표현식)를 자주 사용한다.
이 표현식은 단순히 주소 글자를 출력하는 것이 아니라, 실제 요청에 사용할 주소를 만들어 주는 역할을 한다.


@{...}를 따로 배우는 이유는 애플리케이션의 기본 경로를 고려해서 주소를 만들 수 있기 때문이다.
예를 들어 서버 환경에 따라 앞에 붙는 경로가 달라질 수 있는데, 링크 URL 표현식을 사용하면 이런 기본 경로를 반영한 요청 주소를 만들 수 있다.
그래서 주소를 문자열로 직접 쓰는 것보다 더 안정적으로 링크를 관리할 수 있다.


또 @{...}는 값을 함께 전달하는 주소도 만들 수 있다.
예를 들어 게시글 번호, 페이지 번호, 이름 같은 값을 요청 주소에 붙여 보낼 수 있다.
이때 값은 query parameter 방식으로 붙을 수도 있고, path variable 방식으로 경로 안에 들어갈 수도 있다.


예를 들어 아래처럼 볼 수 있다.

  • @{/hello} : 기본 요청 주소를 만든다.
  • @{/hello(param1=${param1})} : 요청 주소 뒤에 query parameter를 붙인다.
  • @{/hello/{param1}(param1=${param1})} : 값을 경로 안에 넣어 path variable처럼 사용한다.

즉, @{...}는 화면에 보이는 링크 글자를 만드는 문법이 아니다.
클릭했을 때 서버로 실제 요청을 보낼 주소를 만드는 문법이다.


핵심은 @{...}가 단순 주소 문자열이 아니라, 기본 경로와 전달값까지 고려해 실제 요청 URL을 만들어 주는 링크 URL 표현식이라는 점이다.
이 기준을 잡으면 뒤에서 나오는 query parameter와 path variable 예제도 훨씬 쉽게 읽을 수 있다.


네 가지를 먼저 구분하기

링크 예제를 볼 때는 먼저 주소를 만드는 방식을 나눠서 봐야 한다.
@{...}(링크 URL 표현식)는 단순 이동 주소도 만들 수 있고, 값을 함께 보내는 주소도 만들 수 있다.
값을 보내는 방식에 따라 주소 모양이 달라지기 때문에 처음부터 구분해서 보는 것이 중요하다.


먼저 가장 기본은 단순 이동 주소다.
값을 따로 보내지 않고 특정 요청으로 이동하는 방식이다.
예를 들어 @{/hello}는 /hello 요청 주소를 만든다.


두 번째는 query parameter 방식이다.
query parameter는 주소 뒤에 ?key=value 형태로 값을 붙여 보내는 방식이다.
예를 들어 @{/hello(param1=${param1})}처럼 작성하면 /hello?param1=news 같은 주소가 만들어질 수 있다.
즉, 요청 주소는 /hello이고, 전달값은 주소 뒤에 따로 붙는다.


세 번째는 path variable 방식이다.
path variable은 값을 주소 경로 안에 직접 넣는 방식이다.
예를 들어 @{/hello/{param1}(param1=${param1})}처럼 작성하면 /hello/news 같은 주소가 만들어질 수 있다.
즉, 전달값이 ? 뒤에 붙는 것이 아니라 경로의 일부처럼 들어간다.


네 번째는 path variable과 query parameter를 함께 쓰는 방식이다.
값 하나는 경로 안에 넣고, 다른 값은 주소 뒤에 붙여 보낼 수 있다.
이 방식은 경로 자체로 주요 대상을 나타내고, 추가 조건은 query parameter로 전달할 때 사용할 수 있다.


정리하면 아래처럼 볼 수 있다.

  • 기본 주소 : 값 없이 요청 주소만 만든다.
  • query parameter : 주소 뒤에 ?key=value 형태로 값을 붙인다.
  • path variable : 주소 경로 안에 값을 직접 넣는다.
  • path variable + query parameter : 경로 안의 값과 주소 뒤의 값을 함께 사용한다.

핵심은 링크 표현식을 볼 때 “어떤 주소로 이동하는가”와 “값을 어떤 방식으로 전달하는가”를 함께 봐야 한다는 점이다.
이 기준을 잡아 두면 뒤의 링크 예제에서 주소 모양만 보고도 어떤 방식으로 값이 전달되는지 이해할 수 있다.


왜 실제 요청 결과까지 봐야 하는가

링크 URL 표현식은 요청 주소를 만들어 주는 문법이다.
하지만 주소를 만들었다고 해서 그 요청이 항상 정상 처리되는 것은 아니다.
주소를 클릭했을 때 서버에 어떤 요청이 들어가고, 그 요청을 처리할 컨트롤러가 있는지까지 확인해야 한다.


즉, 링크 표현식은 주소를 만드는 단계에서 끝나지 않는다.
만들어진 주소가 실제로 어떤 컨트롤러 매핑으로 연결되는지, 그리고 최종적으로 어떤 화면이 출력되는지까지 이어서 봐야 이해가 완성된다.


예를 들어 @{/select}로 주소를 만들었다면, 서버에는 /select 요청을 처리할 컨트롤러가 있어야 한다.
또 @{/insert(pageno=${pageno})}처럼 값을 함께 보냈다면, 컨트롤러나 템플릿에서 그 pageno 값을 어떻게 읽고 출력하는지도 확인해야 한다.


반대로 링크 주소는 만들어졌지만 해당 요청을 받을 컨트롤러 매핑이 없다면 404 오류가 날 수 있다.
그래서 성공한 요청 결과뿐 아니라, 잘못된 주소로 인한 오류 화면도 같이 보는 것이 중요하다.
오류 화면을 보면 “링크 문법이 틀린 것인지”, “주소를 받을 컨트롤러가 없는 것인지”를 구분할 수 있다.


핵심은 링크 표현식을 주소 생성 문법으로만 보지 말고, 주소 생성 → 실제 요청 → 컨트롤러 처리 → 결과 화면까지 이어지는 흐름으로 봐야 한다는 점이다.
이 기준을 잡으면 뒤에서 성공 화면과 오류 화면을 함께 보는 이유가 분명해진다.


기본예제로 이해 확인하기

가장 짧은 링크 예제

아래 예제는 @{...}(링크 URL 표현식)이 기본 이동 주소와 query parameter가 붙은 주소를 어떻게 만드는지 보여 준다.
이 예제에서는 컨트롤러가 Model에 param1 값으로 value를 담아 두었다고 가정한다.


th:href는 <a> 태그의 href 속성값을 Thymeleaf가 만든 요청 주소로 바꿔 주는 속성이다.
즉, 사용자가 링크를 클릭했을 때 이동할 실제 요청 주소를 만드는 역할을 한다.

// exam18.html
<a th:href="@{/hello}">기본 이동</a> <!-- /hello 요청 주소 생성 -->
<a th:href="@{/hello(param1=${param1})}">파라미터 이동</a> <!-- param1 값을 query parameter로 전달 -->
// 출력결과
// /hello
// /hello?param1=value

첫 번째 링크는 값을 따로 보내지 않고 /hello 요청 주소만 만든다.
그래서 클릭하면 /hello로 요청이 이동한다.


두 번째 링크는 /hello 주소 뒤에 param1=value를 붙인다.
이처럼 주소 뒤에 ?key=value 형태로 붙는 값을 query parameter라고 한다.
즉, 같은 /hello 요청이라도 추가 값을 함께 전달할 수 있다.


핵심은 @{...}가 단순 문자열이 아니라, 클릭했을 때 실제 서버로 보낼 요청 주소를 만들어 주는 링크 URL 표현식이라는 점이다.
이 짧은 예제를 먼저 이해하면 뒤에서 나오는 path variable과 실제 요청 결과도 훨씬 쉽게 읽을 수 있다.


응용예제로 이해 완성하기

링크 목록 화면 먼저 보기

이제 실제 실습 예제로 올라가면 @{...}(링크 URL 표현식)을 사용해 여러 종류의 요청 주소를 만드는 흐름을 볼 수 있다.
이 예제는 기본 주소, query parameter, path variable, path variable과 query parameter를 함께 쓰는 방식을 한 화면에서 비교한다.


먼저 컨트롤러는 링크를 만들 때 사용할 값을 Model에 담는다.
param1, param2, pageno, name, number는 템플릿에서 주소를 만들 때 사용된다.

// ThymeleafController2.java
@GetMapping("/link") // /link 요청 처리
public String link(Model model) {
    model.addAttribute("param1", "news"); // 첫 번째 전달값 저장
    model.addAttribute("param2", "단풍"); // 두 번째 전달값 저장
    model.addAttribute("pageno", 123); // 페이지 번호 저장
    model.addAttribute("name", "line"); // 캐릭터 종류 저장
    model.addAttribute("number", 5); // 캐릭터 번호 저장
    return "basic/link"; // 사용할 템플릿 이름 반환
}

이제 템플릿에서는 th:href와 @{...}를 사용해 실제 요청 주소를 만든다.

// link.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>URL 링크</h1>
<ul>
    <li><a th:href="@{/hello}">basic url</a></li> <!-- 값 없이 /hello 주소 생성 -->
    <li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">hello query param</a></li> <!-- 값을 query parameter로 전달 -->
    <li><a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li> <!-- 값을 경로 안에 전달 -->
    <li><a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li> <!-- 경로값과 query parameter 함께 전달 -->
</ul>
<hr>
<ul>
    <li><a th:href="@{/select}">/select 요청</a></li> <!-- /select 요청 주소 생성 -->
    <li><a th:href="@{/insert(pageno=${pageno})}">Query 문자열로 pageno=123을 지정하여 /insert 요청</a></li> <!-- pageno 값을 query parameter로 전달 -->
    <li><a th:href="@{/character/detail/{name}/{number}(name=${name}, number=${number})}">line 캐릭터중 다섯번째 정보로 요청</a></li> <!-- name과 number를 path variable로 전달 -->
</ul>
</body>
</html>

이 코드에서 첫 번째 목록은 링크 주소를 만드는 네 가지 방식을 비교한다.
@{/hello}는 값 없이 이동하는 기본 요청 주소를 만든다.
@{/hello(param1=${param1}, param2=${param2})}는 주소 뒤에 값을 붙이는 query parameter 방식이다.


@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}는 값을 경로 안에 넣는 path variable 방식이다.
@{/hello/{param1}(param1=${param1}, param2=${param2})}는 param1은 경로 안에 넣고, param2는 주소 뒤에 붙이는 혼합 방식이다.


두 번째 목록은 실제 실습 요청을 만든다.
/select는 단순 요청이고, /insert는 pageno 값을 함께 보내는 요청이다.
/character/detail/{name}/{number}는 경로 안에 name과 number 값을 넣어 보내는 요청이다.

@{...}를 사용하면 기본 주소, query parameter, path variable 방식의 링크를 만들 수 있다.


핵심은 링크 목록 화면에서 주소 모양을 먼저 보고, 각 링크가 값을 어떤 방식으로 전달하는지 구분하는 것이다.


실제 요청 결과까지 같이 보기

링크 표현식은 주소를 만드는 것에서 끝나지 않는다.
링크를 클릭했을 때 실제로 어떤 요청이 들어가고, 그 요청이 어떤 결과 화면으로 이어지는지까지 확인해야 한다.


가장 기본적인 요청은 값을 따로 보내지 않고 정해진 주소로 이동한다.
아래 화면은 기본 요청이 실행되었을 때 결과가 어떻게 보이는지 확인하는 화면이다.

기본 요청은 값을 따로 전달하지 않고 정해진 컨트롤러 요청으로 이동한다.


다음은 query parameter를 붙여 값을 보내는 요청 결과다.
주소 뒤에 ?key=value 형태로 값이 붙고, 서버는 그 값을 요청 파라미터로 읽을 수 있다.

query parameter는 주소 뒤에 값을 붙여 서버로 전달하는 방식이다.


핵심은 링크를 클릭했을 때 주소 모양이 실제 요청 방식으로 이어진다는 점이다.
주소 뒤에 값이 붙으면 query parameter 방식이고, 경로 안에 값이 들어가면 path variable 방식으로 읽을 수 있다.


잘못된 경로로 요청하면 오류도 난다

링크 주소를 만들 수 있다고 해서 모든 요청이 정상 처리되는 것은 아니다.
@{...}는 주소를 만들어 줄 뿐이고, 그 주소를 실제로 처리할 컨트롤러 매핑이 있어야 정상 화면이 나온다.


아래 화면들은 요청 주소와 컨트롤러 매핑이 맞지 않을 때 오류가 날 수 있다는 점을 보여 준다.

요청 주소를 처리할 컨트롤러 매핑이 없으면 404 오류가 발생할 수 있다.


링크 주소와 컨트롤러의 요청 매핑이 맞지 않으면 정상 화면으로 연결되지 않는다.


즉, 오류 화면은 단순히 실패 결과로만 보면 안 된다.
어떤 주소가 만들어졌고, 그 주소를 받을 컨트롤러가 있는지 확인하는 과정으로 봐야 한다.


핵심은 링크 표현식이 주소를 만들더라도, 그 주소를 처리할 컨트롤러 매핑이 있어야 최종 화면까지 정상적으로 이어진다는 점이다.


실제 실습 요청 결과 확인하기

이제 실습용 링크를 클릭했을 때 결과 화면이 어떻게 바뀌는지 확인하면 된다.
이 부분은 링크 표현식이 실제 데이터 전달과 화면 이동으로 어떻게 연결되는지 보여 준다.


먼저 /select 요청은 값을 따로 전달하지 않고 단순히 다른 요청 결과 화면으로 이동한다.

/select 링크를 클릭하면 해당 요청을 처리한 결과 화면으로 이동한다.


다음은 /insert 요청에 pageno 값을 query parameter로 함께 전달한 결과다.
코드에서 pageno 값은 123으로 담겨 있으므로, 결과 설명도 123 기준으로 맞춰야 한다.

query parameter로 전달한 pageno=123 값을 요청 처리 흐름에서 읽어 출력할 수 있다.


마지막은 path variable 방식으로 name과 number를 경로 안에 넣어 전달한 결과다.
name 값은 line, number 값은 5로 담겨 있으므로 요청 주소 안에 이 값들이 들어간다.

path variable은 값을 주소 경로 안에 넣어 서버로 전달하는 방식이다.


여기서 주의할 점은 코드, 요청 주소, 결과 화면의 값이 서로 맞아야 한다는 것이다.
예를 들어 코드에서 pageno가 123이면 설명도 123 기준으로 작성해야 한다.
값이 어긋나면 독자가 코드와 결과를 함께 따라가기 어렵다.


핵심은 링크 표현식이 만든 주소가 실제 요청으로 이어지고, 전달된 값이 컨트롤러 처리 결과와 화면 출력으로 연결된다는 점이다.


여기서 꼭 기억해야 하는 핵심

이 큰 주제에서 먼저 잡아야 할 핵심은 네 가지다.

  • @{...}는 실제 요청 주소를 만들어 주는 링크 URL 표현식이다.
  • query parameter는 주소 뒤에 ?key=value 형태로 값을 붙여 전달하는 방식이다.
  • path variable은 값을 주소 경로 안에 직접 넣어 전달하는 방식이다.
  • 링크 주소가 만들어져도 그 요청을 처리할 컨트롤러 매핑이 있어야 정상 화면으로 이어진다.

th:href는 <a> 태그의 href 값을 Thymeleaf가 만든 요청 주소로 바꿔 준다.
그래서 사용자가 링크를 클릭하면 단순 문자열이 아니라, 실제 서버 요청 주소로 이동하게 된다.


값을 함께 전달할 때는 주소 모양을 보고 방식을 구분해야 한다.
주소 뒤에 ?pageno=123처럼 붙으면 query parameter 방식이고, /character/detail/line/5처럼 경로 안에 값이 들어가면 path variable 방식이다.
둘 다 값을 전달하는 방식이지만, 값이 들어가는 위치가 다르다.


또 링크 표현식은 주소를 만들어 줄 뿐이다.
그 주소를 받을 컨트롤러 매핑이 없으면 정상 화면이 아니라 404 같은 오류 화면이 나올 수 있다.
따라서 링크 문법을 볼 때는 주소가 어떻게 만들어졌는지뿐 아니라, 그 주소가 실제 컨트롤러와 연결되는지도 함께 확인해야 한다.


즉, 이 파트의 핵심은 링크 표현식을 단순 주소 작성 문법으로 보는 것이 아니라, 요청 주소 생성, 값 전달 방식, 컨트롤러 처리, 최종 화면 결과까지 이어지는 흐름으로 이해하는 것이다.
이 기준을 잡으면 @{...}, query parameter, path variable, 오류 화면까지 한 흐름으로 정리할 수 있다.




자바스크립트 인라인

지금까지는 Thymeleaf 값을 HTML 태그 안이나 문장 안에 출력하는 방법을 봤다.
이번에는 컨트롤러가 전달한 값을 JavaScript 코드 안에서 사용하는 방법을 봐야 한다.
이때 사용하는 방식이 JavaScript 인라인 처리다.


HTML 화면에 값을 출력할 때와 JavaScript 코드 안에 값을 넣을 때는 기준이 다르다.
HTML에서는 값이 화면에 보이면 되지만, JavaScript에서는 그 값이 스크립트 문법에 맞는 형태로 들어가야 한다.
예를 들어 문자열은 따옴표가 필요하고, 숫자는 숫자 형태로 들어가야 하며, 객체는 JavaScript가 이해할 수 있는 구조로 들어가야 한다.


즉, 이 큰 주제의 핵심은 Thymeleaf 값을 단순히 화면에 출력하는 것이 아니라, JavaScript 코드 안에서 오류 없이 사용할 수 있는 형태로 넣는 방법을 이해하는 것이다.
이 기준을 잡아 두면 th:inline="javascript"가 왜 필요한지 쉽게 이해할 수 있다.


스크립트 안에서 더 조심해야 하는 이유

HTML 안에서 값을 출력할 때는 비교적 단순하다.
태그 안에 글자를 넣거나, 문장 안에 값을 끼워 넣으면 화면에 보이는 결과를 확인할 수 있다.
하지만 JavaScript 안에 값을 넣을 때는 단순히 화면에 보이는 문제가 아니라, 그 값이 코드로 어떻게 해석되는지가 중요하다.


예를 들어 DUKE라는 문자열을 JavaScript 코드 안에 넣는다고 생각해 보자.
문자열이라면 "DUKE"처럼 따옴표가 있어야 한다.
그런데 따옴표 없이 DUKE만 들어가면, JavaScript는 이것을 문자열이 아니라 변수 이름처럼 해석하려고 한다.
그 변수 이름이 정의되어 있지 않으면 ReferenceError가 발생할 수 있다.


숫자는 문자열과 다르게 따옴표 없이 들어가도 된다.
예를 들어 10은 JavaScript에서 숫자 값으로 해석될 수 있다.
하지만 "10"처럼 따옴표가 붙으면 숫자가 아니라 문자열로 해석된다.
즉, 값이 어떻게 들어가느냐에 따라 자료형이 달라질 수 있다.


객체는 더 조심해야 한다.
Java 객체를 그대로 화면에 출력하는 것과, JavaScript 코드 안에서 사용할 수 있는 객체 형태로 넣는 것은 다르다.
JavaScript 안에서 객체를 사용하려면 JavaScript가 이해할 수 있는 구조로 변환되어야 한다.


그래서 JavaScript 안에 Thymeleaf 값을 넣을 때는 값의 모양을 맞춰 주는 처리가 필요하다.
이때 사용하는 것이 th:inline="javascript"다.
이 설정을 사용하면 문자열은 문자열답게, 숫자는 숫자답게, 객체는 JavaScript가 이해할 수 있는 형태로 들어가도록 도와준다.


핵심은 JavaScript 안에서는 값이 단순히 출력되는 것이 아니라 코드로 해석되기 때문에, 스크립트 문법에 맞는 형태로 들어가야 한다는 점이다.
이 기준을 잡아 두면 문자열에 따옴표가 필요한 이유와 th:inline="javascript"가 필요한 이유를 자연스럽게 이해할 수 있다.


스크립트 인라인 설정이 하는 일

th:inline="javascript"는 Thymeleaf 값을 JavaScript 코드 안에서 사용할 수 있는 형태로 넣어 주는 설정이다.
쉽게 말해 템플릿 값을 단순히 글자로 끼워 넣는 것이 아니라, JavaScript 문법에 맞게 변환해 주는 역할을 한다.


이 설정이 없으면 문자열 값이 따옴표 없이 들어가 오류가 날 수 있다.
예를 들어 DUKE라는 값이 문자열인데 따옴표 없이 들어가면, JavaScript는 이것을 문자열이 아니라 변수 이름으로 해석하려고 한다.
반대로 th:inline="javascript"를 사용하면 문자열이 "DUKE"처럼 문자열 형태로 들어가도록 처리된다.


숫자는 숫자 형태로 들어가고, 객체는 JavaScript에서 사용할 수 있는 객체 형태로 들어갈 수 있다.
그래서 단순 문자열 하나뿐 아니라 사용자 객체처럼 여러 값을 가진 데이터도 스크립트 안에서 다룰 수 있다.
이때 객체 전체를 바로 화면에 보여 주는 것과, 객체 안의 특정 속성을 꺼내 사용하는 것은 다르다.
예를 들어 객체 전체를 alert()로 출력하면 사람이 보기 좋은 문장이 아니라 기본 객체 표현처럼 보일 수 있다.
하지만 user.age처럼 필요한 속성을 꺼내면 그 값은 따로 사용할 수 있다.


또 JavaScript 인라인은 Natural Template 특징과도 연결된다.
예를 들어 /*[[${user.userName}]]*/ "test username"처럼 쓰면 서버가 실행되지 않을 때는 "test username"이 기본값처럼 남아 있다.
서버가 실행되고 Thymeleaf가 처리되면 실제 Model 값으로 바뀐다.


즉, th:inline="javascript"는 단순히 값을 넣는 기능이 아니다.
JavaScript 문법에 맞게 값을 넣고, 서버 없이 볼 때 사용할 기본값도 남길 수 있게 해 주는 설정이다.


핵심은 th:inline="javascript"가 Thymeleaf 값을 JavaScript 코드 안에서 오류 없이 사용할 수 있는 형태로 바꿔 주는 설정이라는 점이다.
이 기준을 잡아 두면 뒤에서 문자열, 숫자, 객체가 스크립트 안에 들어가는 예제를 쉽게 이해할 수 있다.


스크립트 안에서도 기본값을 남길 수 있는 이유

Thymeleaf의 Natural Template 특징은 HTML 태그에서만 적용되는 것이 아니다.
JavaScript 코드 안에서도 서버 없이 파일을 열었을 때 사용할 기본값을 남겨 둘 수 있다.
이때 자주 사용하는 형태가 /*[[...]]*/ 기본값 구조다.


예를 들어 아래 코드를 보면 /*[[${user.userName}]]*/ "test username"처럼 작성되어 있다.
서버가 실행되지 않으면 Thymeleaf가 [[...]] 부분을 처리하지 않는다.
그래서 뒤에 적어 둔 "test username"이 그대로 남아 기본값처럼 동작한다.

// exam21.html
<script th:inline="javascript">
    var username = /*[[${user.userName}]]*/ "test username"; // 서버 처리 전에는 기본값 사용
</script>

반대로 서버가 실행되고 Thymeleaf가 템플릿을 처리하면 /*[[${user.userName}]]*/ 부분이 실제 Model 값으로 바뀐다.
이때 뒤에 있던 기본값은 실제 실행 결과에서 사용되지 않는다.
즉, 서버 실행 전에는 기본값을 보고, 서버 실행 후에는 실제 데이터를 사용하는 구조가 된다.


이 방식이 좋은 이유는 스크립트 코드도 서버 없이 어느 정도 형태를 확인할 수 있기 때문이다.
HTML에서 기본 글자인 Hi를 남겨 두는 것처럼, JavaScript 안에서도 테스트용 기본값을 남겨 둘 수 있다.
그래서 화면 구조와 스크립트 흐름을 미리 확인하기가 더 쉽다.


다만 이 기본값은 실제 서버 데이터가 없을 때 문서 구조를 확인하기 위한 보조값이다.
실제 서버 요청을 거치면 컨트롤러가 Model에 담은 값이 우선 반영된다.
따라서 기본값과 실제 데이터가 서로 어떤 상황에서 사용되는지 구분해야 한다.


핵심은 JavaScript 인라인에서도 서버 실행 전에는 기본값을 남기고, 서버 실행 후에는 실제 Model 값으로 바꿀 수 있다는 점이다.
이 흐름을 이해하면 /*[[...]]*/ "test username" 같은 코드가 왜 필요한지 자연스럽게 읽을 수 있다.


기본예제로 이해 확인하기

가장 짧은 알림창 예제

아래 예제는 Thymeleaf 값을 JavaScript 변수 안에 넣고, 그 값을 알림창으로 확인하는 가장 짧은 예제다.
이 예제에서는 컨트롤러가 Model에 user 객체를 담아 두었고, 그 객체 안의 userName 값이 DUKE라고 가정한다.


중요한 점은 값을 HTML 태그 안에 출력하는 것이 아니라, JavaScript 변수인 username 안에 넣고 있다는 점이다.
그래서 th:inline="javascript"를 사용해 스크립트 문법에 맞게 값이 들어가도록 처리한다.

// ThymeleafController3.java
@GetMapping("/javascript-basic") // /javascript-basic 요청 처리
public String javascriptBasic(Model model) {
    model.addAttribute("user", new User("DUKE", 10)); // user 객체 저장
    return "basic/javascript-basic"; // 사용할 템플릿 이름 반환
}
// javascript-basic.html
<script th:inline="javascript">
    var username = [[${user.userName}]]; // userName 값을 JavaScript 변수에 저장
    alert(username); // 저장된 값을 알림창으로 출력
</script>
// 출력결과
// alert 창에 DUKE가 표시된다.

[[${user.userName}]]는 Model에 담긴 user 객체에서 userName 값을 꺼낸다.
그리고 th:inline="javascript"가 적용되어 있기 때문에, 이 값은 JavaScript 코드 안에서 사용할 수 있는 문자열 형태로 들어간다.


즉, DUKE라는 값이 화면 태그에 바로 출력되는 것이 아니라 username 변수에 저장된다.
그다음 alert(username)이 실행되면서 변수에 들어 있는 값이 알림창에 표시된다.


핵심은 JavaScript 인라인에서는 템플릿 값을 화면에 바로 찍는 것이 아니라, 스크립트 변수 안에 넣고 코드에서 사용할 수 있다는 점이다.
이 흐름을 이해하면 뒤의 인라인 사용 전과 사용 후 비교 예제도 더 쉽게 읽을 수 있다.


응용예제로 이해 완성하기

인라인 사용 전과 사용 후 비교하기

이 예제는 JavaScript 코드 안에 Thymeleaf 값을 넣을 때, 인라인 설정이 없을 때와 있을 때 결과가 어떻게 달라지는지 비교하는 예제다.
핵심은 같은 값을 넣더라도 JavaScript가 이해할 수 있는 형태로 들어가야 오류 없이 사용할 수 있다는 점이다.


먼저 컨트롤러는 user 객체를 Model에 담는다.
userName 값은 DUKE이고, age 값은 10이다.
또 addUsers(model)을 통해 사용자 목록도 함께 담는다.

// ThymeleafController3.java
@GetMapping("/javascript") // /javascript 요청 처리
public String javascript(Model model) {
    model.addAttribute("user", new User("DUKE", 10)); // user 객체 저장
    addUsers(model); // 사용자 목록 저장
    return "basic/javascript"; // 사용할 템플릿 이름 반환
}

이제 템플릿에서는 같은 값을 인라인 설정이 없는 스크립트와 인라인 설정이 있는 스크립트에서 각각 사용한다.

// javascript.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<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}]]; // 객체를 스크립트에서 사용할 수 있는 형태로 저장

    alert(username); // 이름 값 확인
    alert(username2); // 기본값 또는 실제 값 확인
    alert(user); // 객체 전체 확인
    alert(user.age); // 객체 안의 age 값 확인
</script>
</body>
</html>

첫 번째 <script>에는 th:inline="javascript"가 없다.
이 경우 문자열인 DUKE가 따옴표 없이 들어가면 JavaScript는 이것을 문자열이 아니라 변수 이름처럼 해석하려고 할 수 있다.
그래서 정의되지 않은 변수라면 오류가 발생할 수 있다.


두 번째 <script>에는 th:inline="javascript"가 있다.
이 설정을 사용하면 Thymeleaf가 문자열은 문자열답게, 숫자는 숫자답게, 객체는 스크립트에서 사용할 수 있는 형태로 넣어 준다.
즉, 템플릿 값이 JavaScript 문법에 맞게 들어가도록 도와준다.


여기서 username2도 중요하다.
/*[[${user.userName}]]*/ "test username" 구조는 서버가 처리하지 않을 때는 test username이라는 기본값을 남긴다.
반대로 서버가 실행되어 Thymeleaf가 처리하면 실제 Model 값인 DUKE가 사용된다.


또 user 객체 전체를 alert()로 보면 사람이 읽기 좋은 문장이 아니라 [object Object]처럼 보일 수 있다.
이것은 객체 전체가 문자열처럼 예쁘게 풀려 보이는 것이 아니라, JavaScript가 객체를 기본 방식으로 표현한 결과다.
그래서 객체 전체를 바로 보여 주는 것과 user.age처럼 필요한 속성을 꺼내 쓰는 것은 구분해야 한다.

th:inline="javascript"를 사용하면 문자열, 숫자, 객체 값을 스크립트 문법에 맞는 형태로 사용할 수 있다.


핵심은 JavaScript 인라인 설정이 템플릿 값을 단순히 넣는 것이 아니라, 스크립트가 해석할 수 있는 형태로 바꿔 준다는 점이다.


오류가 나는 이유까지 확인하기

성공한 결과만 보면 th:inline="javascript"가 왜 필요한지 정확히 와닿지 않을 수 있다.
그래서 오류가 나는 경우도 같이 봐야 한다.
오류를 보면 값이 들어갔는지보다, 그 값을 JavaScript가 어떤 문법으로 해석했는지가 더 중요하다는 점을 알 수 있다.


예를 들어 문자열 값인 DUKE가 따옴표 없이 들어가면 문제가 생길 수 있다.
JavaScript는 DUKE를 문자열이 아니라 변수 이름으로 보려고 한다.
그런데 DUKE라는 변수가 정의되어 있지 않으면 ReferenceError가 발생한다.


즉, 아래 두 코드는 완전히 다르게 해석된다.

// exam22.js
var username = DUKE; // DUKE를 변수 이름으로 해석하려고 함
var username2 = "DUKE"; // DUKE를 문자열 값으로 해석함
// 출력결과
// 첫 번째 줄은 DUKE 변수가 없으면 ReferenceError가 발생할 수 있다.
// 두 번째 줄은 정상적인 문자열 값이다.

숫자는 문자열과 다르게 따옴표 없이 들어가도 숫자로 해석될 수 있다.
하지만 객체는 JavaScript에서 사용할 수 있는 객체 형태로 들어가야 한다.
그래서 문자열, 숫자, 객체는 각각 들어가는 형태를 맞춰야 한다.


아래 이미지는 인라인 처리가 맞지 않을 때 오류가 생길 수 있다는 점을 보여 준다.

문자열 값이 스크립트 문법에 맞게 들어가지 않으면 ReferenceError 같은 오류가 발생할 수 있다.


핵심은 JavaScript 안에서는 값이 화면에 보이는 글자가 아니라 실행되는 코드로 해석된다는 점이다.
따라서 템플릿 값을 스크립트 안에서 사용할 때는 th:inline="javascript"로 문맥에 맞게 처리하는 것이 중요하다.


여기서 꼭 기억해야 하는 핵심

이 큰 주제에서 먼저 잡아야 할 핵심은 네 가지다.

  • HTML 안에 값을 출력하는 것과 JavaScript 안에 값을 넣는 것은 다르다.
  • JavaScript 안에서는 값이 스크립트 문법에 맞는 형태로 들어가야 한다.
  • th:inline="javascript"는 문자열, 숫자, 객체를 JavaScript에서 사용할 수 있는 형태로 넣어 준다.
  • /*[[...]]*/ 기본값 구조를 사용하면 서버 실행 전에는 기본값을 남기고, 서버 실행 후에는 실제 Model 값을 사용할 수 있다.

문자열은 반드시 문자열 형태로 들어가야 한다.
예를 들어 DUKE가 따옴표 없이 들어가면 JavaScript는 이것을 문자열이 아니라 변수 이름으로 해석하려고 할 수 있다.
그래서 해당 변수가 정의되어 있지 않으면 ReferenceError가 발생할 수 있다.


숫자는 숫자 형태로 들어가야 하고, 객체는 JavaScript에서 사용할 수 있는 객체 구조로 들어가야 한다.
객체 전체를 바로 출력하면 [object Object]처럼 보일 수 있지만, user.age처럼 필요한 속성을 꺼내면 원하는 값을 사용할 수 있다.
즉, 객체 전체를 보는 것과 객체 안의 속성을 꺼내는 것은 다르다.


또 JavaScript 인라인에서도 Natural Template 흐름을 유지할 수 있다.
서버 없이 파일을 볼 때는 "test username" 같은 기본값이 남아 있고, 서버를 거쳐 실행되면 컨트롤러가 Model에 담은 실제 값으로 바뀐다.


즉, 이 파트의 핵심은 Thymeleaf 값을 JavaScript 안에 넣을 때 단순 출력으로 보지 말고, 스크립트가 해석할 수 있는 값의 형태로 변환해야 한다는 점이다.
이 기준을 잡으면 th:inline="javascript", 기본값 처리, 객체 속성 접근, 오류 발생 이유까지 한 흐름으로 이해할 수 있다.




마지막 정리

Thymeleaf를 처음 배울 때 가장 중요한 것은 문법 이름을 많이 외우는 것이 아니다.
먼저 컨트롤러가 값을 준비하고, Model이 그 값을 전달하고, 템플릿이 최종 HTML에 반영한다는 큰 흐름을 이해해야 한다.


이 흐름이 잡히면 th:text, ${...}, [[...]], th:each, @{...}, th:inline="javascript" 같은 문법이 따로 떨어진 조각처럼 보이지 않는다.
각 문법은 결국 값을 읽고, 화면에 넣고, 조건에 따라 남기고, 반복하고, 요청 주소를 만들고, 스크립트 안에서 사용할 수 있게 만드는 도구다.


즉, Thymeleaf는 문법 모음이 아니라 서버에서 준비한 데이터를 화면에 맞게 반영해 최종 HTML을 만드는 흐름으로 이해해야 한다.


지금까지의 흐름 다시 묶어 보기

먼저 Thymeleaf가 무엇인지 봤다.
Thymeleaf는 서버에서 준비한 데이터를 HTML 화면에 자연스럽게 넣어 주는 템플릿 엔진이다.
컨트롤러가 데이터를 준비하고, Model이 그 값을 전달하고, 템플릿이 그 값을 화면에 반영한다.


그다음에는 Natural Template 특징을 정리했다.
서버 없이 템플릿 파일을 열면 기본 HTML처럼 보이고, 서버를 거쳐 실행하면 Model에 담긴 값이 반영된 동적 화면이 된다.
이 특징 때문에 HTML 구조를 유지하면서도 서버 데이터를 화면에 넣을 수 있다.


값 출력 방식에서는 ${...}(변수 표현식), th:text(텍스트 출력 속성), [[...]](텍스트 인라인 표현식), th:value(값 속성 설정)를 구분했다.
${...}는 값을 꺼내는 문법이고, th:text, [[...]], th:value는 꺼낸 값을 어디에 넣을지 정하는 문법이다.


이스케이프와 언이스케이프에서는 같은 값이라도 문자처럼 보여 줄지, HTML 태그처럼 적용할지에 따라 결과가 달라진다는 점을 봤다.
th:text와 [[...]]는 escape 방식이고, th:utext와 [(...)]는 unescape 방식이다.
기본은 안전하게 문자처럼 보여 주는 escape 방식이고, 태그 반영이 꼭 필요할 때만 unescape 방식을 조심해서 사용해야 한다.


변수 표현식과 객체 접근에서는 객체, List, Map 안의 값을 읽는 방법을 정리했다.
객체는 점 표기로 값을 읽고, List는 인덱스로 대상을 찾고, Map은 키로 대상을 찾는다.
th:with는 긴 표현식을 짧은 이름으로 정리하는 지역 변수 기능이고, th:object와 *{...}는 기준 객체 안의 값을 짧게 읽는 방식이다.


기본 객체와 유틸리티 객체에서는 값을 꺼내는 도구와 값을 가공하는 도구를 나눠 봤다.
param, session, #locale은 이미 존재하는 요청값, 세션값, 지역 정보를 읽는 기본 객체다.
#strings, #numbers, #temporals는 문자열, 숫자, 날짜를 화면에 맞게 보기 좋게 다듬는 유틸리티 객체다.


리터럴과 연산에서는 화면에 보여 줄 문장을 만들고, 조건에 따라 결과를 고르는 방법을 봤다.
리터럴은 표현식 안에 직접 적어 둔 고정값이다.
+는 문자열을 직접 이어 붙이고, | |는 문장 틀 안에 값을 끼워 넣는 리터럴 대체 방식이다.
산술 연산, 비교 연산, 삼항 연산자, Elvis 연산자, _는 화면 결과를 상황에 맞게 정하는 데 사용된다.


조건 처리와 반복 처리에서는 화면에 남길 내용을 결정하고, 여러 데이터를 같은 구조로 출력하는 방법을 정리했다.
th:if와 th:unless는 조건에 따라 태그가 최종 HTML에 남을지 빠질지를 결정한다.
th:switch와 th:case는 기준값 하나로 여러 경우 중 맞는 결과를 고른다.
th:each는 여러 데이터를 반복 출력하고, 반복 상태값을 함께 사용하면 순번, 인덱스, 처음 여부, 마지막 여부까지 다룰 수 있다.
th:block은 여러 태그를 함께 제어하지만 최종 HTML에는 남지 않는 임시 묶음이다.


링크 표현식에서는 @{...}(링크 URL 표현식)로 실제 요청 주소를 만드는 방법을 봤다.
query parameter는 주소 뒤에 ?key=value 형태로 값을 붙이는 방식이고, path variable은 값을 주소 경로 안에 넣는 방식이다.
링크 주소를 만드는 것에서 끝나지 않고, 그 주소를 처리할 컨트롤러 매핑이 있어야 최종 화면까지 정상적으로 이어진다.


마지막으로 JavaScript 인라인에서는 Thymeleaf 값을 스크립트 안에서 사용하는 방법을 정리했다.
HTML 화면에 값을 출력하는 것과 JavaScript 코드 안에 값을 넣는 것은 다르다.
JavaScript 안에서는 값이 코드로 해석되기 때문에 문자열, 숫자, 객체가 스크립트 문법에 맞는 형태로 들어가야 한다.
th:inline="javascript"는 이 값을 JavaScript가 이해할 수 있는 형태로 넣어 주는 설정이다.


결국 무엇이 가장 중요한가

지금까지 나온 문법은 많지만, 가장 중요한 기준은 하나다.
Thymeleaf는 값을 직접 만드는 도구가 아니다.
값은 컨트롤러가 준비하고, Model에 담기며, 템플릿은 그 값을 읽고 화면에 맞게 반영한다.



따라서 Thymeleaf 코드를 볼 때는 먼저 값의 흐름을 따라가야 한다.
값이 어디서 만들어졌는지, 어떤 이름으로 Model에 담겼는지, 템플릿에서 어떤 표현식으로 꺼내는지, 최종 화면에서 어디에 들어가는지를 봐야 한다.


그다음 문법의 역할을 나눠서 보면 된다.
값을 읽는 문법인지, 값을 출력하는 문법인지, 조건에 따라 태그를 남기는 문법인지, 여러 데이터를 반복하는 문법인지, 요청 주소를 만드는 문법인지, 스크립트 안에 값을 넣는 문법인지 구분해야 한다.


이 기준이 잡히면 문법을 무작정 외우지 않아도 된다.
각 문법이 왜 필요한지, 어떤 상황에서 쓰이는지, 결과 화면이 왜 그렇게 나오는지 흐름으로 이해할 수 있다.


이 글을 읽을 때 가져가야 할 이해 방식

이번 글은 항상 같은 흐름으로 정리했다.
먼저 개념을 설명하고, 짧은 기본예제로 감을 잡고, 실제 컨트롤러와 템플릿 코드, 결과 화면을 묶어 응용예제로 확인했다.


이 순서가 중요한 이유는 Thymeleaf 문법이 혼자 동작하지 않기 때문이다.
컨트롤러가 어떤 값을 준비했는지 모르면 ${...}가 무엇을 꺼내는지 알기 어렵다.
템플릿 파일이 어떤 위치에 있는지 모르면 반환 문자열이 왜 화면으로 연결되는지 이해하기 어렵다.
결과 화면을 확인하지 않으면 조건문, 반복문, 링크 표현식이 실제로 어떤 결과를 만드는지도 알기 어렵다.


그래서 예제를 볼 때는 코드를 한 줄씩 외우기보다 아래 흐름을 먼저 확인해야 한다.

  • 컨트롤러가 어떤 값을 준비했는가.
  • 그 값이 어떤 이름으로 Model에 담겼는가.
  • 템플릿에서 어떤 표현식으로 값을 꺼냈는가.
  • 꺼낸 값을 화면 어디에 넣었는가.
  • 조건, 반복, 링크, 스크립트 처리에 따라 최종 결과가 어떻게 달라졌는가.

이 흐름을 따라가면 Thymeleaf 문법이 단순 암기 대상이 아니라, 서버 데이터가 화면으로 바뀌는 과정으로 보인다.


최종 한 줄 정리

결국 Thymeleaf는 HTML 구조를 유지하면서 서버 데이터를 화면에 넣고, 조건 처리, 반복 처리, 링크 생성, JavaScript 연결까지 처리할 수 있는 서버 사이드 템플릿 엔진이다.
이 큰 흐름만 잡히면 이후 문법도 훨씬 쉽게 읽힌다.

0개의 댓글