본문 바로가기
  • 오늘도 한걸음. 수고많았어요.^^
  • 조금씩 꾸준히 오래 가자.ㅎ
IT기술/spring

[Thymeleaf] 스프링에서 웹페이지 만드는 방법3, form 에서 유용한 thymeleaf

by 미노드 2024. 2. 19.

일반적으로 폼에서 사용하기 좋은 thymeleaf를 별도로 정리해보려 합니다.

form은 데이터를 등록하거나 수정하는 뷰 페이지에서 주로 사용되며, 데이터를 post/get으로 넘겨주게 됩니다.

타임리프가 form에서 어떤 기능을 도울 수 있을지 정리해보겠습니다.

- input field의 id, name 자동 등록

<input> 안의 id와 name을 자동으로 등록할 수 있습니다.
컨트롤러에서 객체를 model에 넣어 줬을 때, 객체의 맴버변수를 id, name, value를 넣을 수 있습니다.

컨트롤러에서 Item 객체에 itemName 맴버변수의 값만 추가해서 model에 넣었습니다.
Html에서 th:field="*{itemName}"  만 추가한 것으로 id, name, value를 자동으로 렌더링 시켜줍니다.
Html에서 작업을 편하게 할 수 있습니다.

th:field="*{itemName}"
id : th:field 에서 지정한 변수 이름과 같다. id="itemName" 
name : th:field 에서 지정한 변수 이름과 같다. name="itemName" 
value : th:field 에서 지정한 변수의 값을 사용한다. value=""

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

그림처럼 item 객체의 맴버변수가 3개일 때 이를 다 적용할 수도 있습니다.

※ 참고로 label 은 주로 값을 입력받는 곳에 이름을 적는 태그이며
for 옵션을 통해 for 의 value와 id 이름이 같은것을 찾아 연결시키는 역할을 합니다.
label for 에 들어갈 값을 id값과 매칭시켜 미리 적어두던지 하는 것도 좋을 것 같습니다.

그리고 id 처럼 옵션을 수동으로 입력했다면, 수동으로 입력한 값이 우선순위를 가지게 됩니다.
다만, "" 처럼 빈값을 넣었다면 th:field의 값이 더 우선순위를 가지니 참고해야합니다.

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

- input type여러가지 지원 

th:object="${item}"     th:field="*{변수명}"    이부분을 핵심적으로 등록해서 사용합니다.
form 이라는 상위태그에 th:object 객체를 선언해주면, form 내부에서  th:field="*{변수명}"   으로 객체이름 없이 변수명 만으로 id, name, value 옵션을 자동으로 할당할 수 있습니다.

th:object 선언이 없다면, th:value="${객체.멤버변수명}" 으로 값을 태그에 매핑시킬 수도 있습니다.

<form action="item.html" th:action th:object="${item}" method="post">
        <div>
            <label for="상품명2">상품명</label>
            <input type="text" class="form-control" placeholder="이름을 입력하세요" th:field="*{itemName}">
        </div>
        <!-- single checkbox 순수 체크박스, value가 체크시 true, 비체크시 null 로 서버에 온다. -->
        <div>체크 확인</div>
        <div>
            <div class="form-check">
                <input type="checkbox" th:field="*{open}" class="form-check-input"  checked="checked">
                <label class="form-check-label">체크 확인</label>
            </div>
        </div>
</form>

-- check box

체크박스로 단순히 HTML을 이용하다보면, request로 값이 서버로 도착할 때
객체의 맴버변수 Boolean 형식의 값에 매핑 되는 것을 보면, 체크시 true로, 비체크시 null로 들어오게 됩니다.

체크 박스를 체크하면 HTML Form에서 open=on 이라는 값이 넘어가며, 스프링은 on 이라는 문자를 true 타입으 
로 변환해줍니다. (스프링 타입 컨버터가 이 기능을 수행한다고 합니다.)
그러나 체크 박스를 선택하지 않을 때
HTML에서 체크 박스를 선택하지 않고 폼을 전송하면 open 이라는 필드 자체가 서버로 전송되지 않습니다.
그래서 null로 값이 잡힙니다.
HTTP request body를 봐도 확인이 됩니다.

<!-- single checkbox 순수 체크박스, value가 체크시 true, 비체크시 null 로 서버에 온다. -->

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

비체크시 null로 들어오는 문제를 해결하기 위해 히든 필드를 하나 만들어 줘야 합니다.
위 소스기준으로 _open 처럼 기존 체크 박스 이름 앞에 언더스코어( _ )를 붙여서 전송하면 
springMVC 에서 체크를 해제했다고 인식할 수 있습니다.

name="_변수이름"  으로 name 옵션을 설정하여 추가합니다.

<!-- single checkbox 순수 체크박스, value가 체크시 true, 비체크시 null 로 서버에 온다. -->

    <div class="form-check">
        <input type="checkbox" id="open" name="open" class="form-check-input">
        <input type="hidden" name="_open" value="on" />
        <label for="open" class="form-check-label">체크 확인</label>
    </div>

체크 했을 때
체크 안했을 때

체크가 안됬을 경우, _open 만 서버로 요청값에 담겨 전달되며, spring MVC는 이를 'open'은 체크되지 않았다고 인식하게 됩니다. 
로그를 찍어보면 다음처럼 체크되지 않은 경우 false로 값이 매핑된 것을 확인할 수 있습니다.

!! 다만 개발할때마다 매번 이럴때 hidden 필드를 추가하는건 비효율적입니다.
타임리프가 제공하는 폼 기능을 사용해서 해결할 수 있습니다.

th:field="*{변수명}"    객체에 해당하는 변수명을 th:field로 넣으면 자동으로 hidden 필드가 추가됩니다.

<div class="form-check">
    <input type="checkbox" th:field="*{open}"  class="form-check-input">
    <label class="form-check-label">체크 확인</label>
</div>

다만 자동으로 th:field로 주입되면 value가 true로 지정되는것을 볼 수 있는데, 
체크박스라서 value보다는 체크유무로 on이 전달되나 안되나 로 구분하면 됩니다.
체크유무는 checked가 들어가있나 안들어가있나로 구분하기 때문입니다.

체크 안되있어도 value는 true로 자동으로 잡힙니다.
체크 되어있어도 value는 ture로 잡힙니다.

중요한 부분은 체크박스에서 th:field="*{변수명}"  을 사용하게 하려면, th:object를 선언해주는 것이 핵심 입니다.
th:object="${item}"     th:field="*{변수명}"

th:object를 쓰지 않는다면 주입된 객체와 맴버변수명을 직접 입력해서 사용하면 됩니다.
th:value="${item.id}"

-- 멀티 check box

체크박스를 여러개 써야 할 경우도 있습니다.
타임리프에서 지원해줍니다.

아래처럼 div를 여러개로 반복시키며 체크박스를 선택하도록 할 수 있습니다.
model 에 regions에 대한 map을 만들어서 regions 라는 키에 addAttribute 시켰습니다.

<div>
    <div>멀티 체크박스 확인</div>
    <div th:each="region : ${regions}" class="form-check form-check-inline">
        <input type="checkbox" th:field="*{regions}" th:value="${region.key}" class="form-check-input">
        <label th:for="${#ids.prev('regions')}" th:text="${region.value}" class="form-check-label"></label>
    </div>
</div>

그림을 보면, regions라는 name으로 묶여 id, name이 자동으로 할당되었고, hidden input 도 자동으로 들어갔습니다.
비체크시 null로 들어오는 문제를 해결하기 위해 히든 필드를 만들어 주는 것인데, th:field 옵션으로 id, name, hidden 을 만들다보니 hidden 옵션도 여러개가 만들어 진 것을 확인할 수 있습니다.
하나만 있어도 충분하나 여러개 만들어져도 상관없습니다. 체크 하나도 없을시 null이 아닌 빈값으로 멤버변수에 저장되도록 설정하기 위한 것이기 때문입니다. 사진으로 예시를 보여드리면 다음과 같습니다.

체크 있을 경우
체크 없을 경우

만일 th:object로 등록한게 아니면 직접 멤버변수를 찾아서 이어줘도 됩니다 
th:field="*{regions}"   ->  th:field="${item.regions}" 

<div>
    <div>멀티 체크박스 확인</div>
    <!--/* org.thymeleaf.expression.Ids #ids 타임리프 지원기능으로 보입니다. */-->
    <div th:each="region : ${regions}" class="form-check form-check-inline">
        <input type="checkbox" th:field="${item.regions}" th:value="${region.key}" class="form-check-input" disabled>
        <label th:for="${#ids.prev('regions')}" th:text="${region.value}" class="form-check-label"></label>
    </div>
</div>

 

-- radio box

라디오 버튼에도 적용 가능합니다.
th:field="${객체명.멤버변수명}"   으로 값을 할당하면,  id, name, value를 자동으로 매핑시켜 줍니다.
라디오 버튼은 id와 value가 중요하며, 넘길때 어떤 값을 넘겨야되는지가 확실해야 합니다.

그런데 선택하지 않을 경우 null이 반환되며, thymeleaf에서 자동으로 hidden _name 태그를 만들어 주지 않습니다.
다만 한번 선택하면, 취소가 안되는 radio 버튼 특성이 있다보니, hidden 필드를 자동으로 안만들어 준다고 합니다.

저장되는 상태가 true false상태가 아니다보니, null이 나을수도 있다고는 하나, 저장될 때 null이 저장되는걸 원지 않는 경우
내부 자바 로직에서 처리할 수 있도록 조치를 하는게 필요해보입니다.

반복을 위한 array나, List, map 을 불러와서 반복시킬 수 있습니다.

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

타임리프에서 Enum을 Model에담아서  전달하는 대신, 자바 객체에 직접 접근해서 사용하는 방법도 있다고합니다.

${T(hello.itemservice.domain.item.ItemType).values()} 스프링EL 문법으로 ENUM을 직접 사용 
할 수 있다. ENUM에 values() 를 호출하면 해당 ENUM의 모든 정보가 배열로 반환된다고 합니다.

그런데 이렇게 사용하면 ENUM의 패키지 위치가 변경되거나 할때 자바 컴파일러가 타임리프까지 컴파일 오류를 잡을 
수 없으므로 유지보수에 좋지 않은 방법 같습니다.

-- select box

라디오 버튼처럼 여러 선택지 중 하나를 선택할 때 사용합니다.

th:field="${객체명.멤버변수명}"   으로 값을 자동으로 할당하는게 가능한데, select 태그에 직접 할당해야 합니다.
each 를 통해서 반복적으로 여러 option을 설정할 수 있습니다.
다만 value와 text를 직접 할당해주는 부분이 중요합니다.
라디오 버튼처럼 자동으로 hidden 옵션이 생성되는것도 아니므로, 빈값설정을 위해 option을 더 넣어주던지,
내부자바로직에서 처리시키던지 하는 방법이 필요합니다.

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

신기한 점은 <select> 태그에 th:field="${객체명.멤버변수명}" 를 넣어 줬을 경우
th:each="deliveryCode : ${deliveryCodes}"   이 코드에 따라서 모델에 넣어진 deliveryCodes가 출력 될텐데, 거기에 value에 맞는 옵션이 있다면 자동으로 selected를 설정해 준다는 것입니다.

selected="selected"

이 기능으로 수정할 값을 선택해서 수정하는 것도 가능합니다.

<select th:field="${item.deliveryCode}" class="form-select" disabled>
    <option value="">==Select Box 선택==</option>
    <option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}"
            th:text="${deliveryCode.displayName}">FAST</option>
</select>