[IntelliJ_Springboot_MariaDB] Thymleaf 하이퍼 링크, read.html 예제 코드, Flash Attributes , HTML history, 버블링, 이벤트 흐름: 캡처링과 버블링, modify.html, BoardController 예제코드
1. Thymleaf 하이퍼 링크 적용 예제 코드
<div class="my-4">
<div class="float-end" th:with="link=${pageRequestDTO.getLink()}">
<a th:href="|@{/board/list}?${link}|" class="text-decoration-none">
<button type="button" class="btn btn-primary">List</button>
</a>
<a th:href="|@{/board/modify(bno=${dto.bno})}&${link}|" class="text-decoration-none">
<button type="button" class="btn btn-secondary">Modify</button>
</a>
</div>
</div>
주요 부분 설명
1. th:with 구문
<div class="float-end" th:with="link=${pageRequestDTO.getLink()}">
- th:with 구문을 사용하여 일시적인 변수를 정의합니다.
- 여기서는 link라는 변수를 정의하고, pageRequestDTO.getLink() 메서드의 반환값을 이 변수에 할당합니다.
- 이 변수는 이 블록 내에서만 유효합니다. 즉, <div class="float-end"> 내부에서만 사용할 수 있습니다.
2. th:href 구문
<a th:href="|@{/board/list}?${link}|" class="text-decoration-none">
<button type="button" class="btn btn-primary">List</button>
</a>
- th:href는 HTML의 href 속성을 동적으로 설정하기 위해 사용됩니다.
- |...| 구문은 URL 표현식을 나타냅니다. 이는 경로와 변수를 포함한 URL을 생성할 때 사용됩니다.
- @{/board/list}는 Spring의 URL 매핑을 사용하여 /board/list 경로로 이동하는 링크를 생성합니다.
- ?${link}는 link 변수의 값을 쿼리 문자열로 추가합니다. 예를 들어, link의 값이 page=1&size=10이라면 최종 URL은 /board/list?page=1&size=10이 됩니다.
3. 또 다른 th:href 예제
<a th:href="|@{/board/modify(bno=${dto.bno})}&${link}|" class="text-decoration-none">
<button type="button" class="btn btn-secondary">Modify</button>
</a>
- 이 부분은 /board/modify 경로에 bno 파라미터와 함께 이동하는 링크를 생성합니다.
- @{/board/modify(bno=${dto.bno})}는 경로에 bno라는 이름의 URL 파라미터를 추가합니다. dto.bno의 값을 bno 파라미터로 사용합니다. 예를 들어, dto.bno의 값이 123이라면, 이는 /board/modify?bno=123이 됩니다.
- &${link}는 추가 쿼리 문자열을 연결합니다. 여기서 link 변수의 값이 추가됩니다. 예를 들어, link의 값이 page=1&size=10이라면 최종 URL은 /board/modify?bno=123&page=1&size=10이 됩니다.
요약
이 Thymeleaf 템플릿 코드는 다음을 수행합니다:
- pageRequestDTO.getLink() 메서드에서 반환된 값을 link 변수에 저장합니다.
- /board/list와 /board/modify 경로로 이동하는 두 개의 링크를 생성합니다. 각 링크에는 link 변수의 값이 쿼리 문자열로 포함됩니다.
- 첫 번째 링크는 List 버튼을 클릭하면 /board/list 페이지로 이동하며, 쿼리 문자열이 추가됩니다.
- 두 번째 링크는 Modify 버튼을 클릭하면 /board/modify 페이지로 이동하며, bno 파라미터와 추가 쿼리 문자열이 포함됩니다.
이렇게 하면 사용자 인터페이스에서 동적으로 URL을 생성하고, 필요한 쿼리 문자열을 추가하여 요청을 보낼 수 있습니다.
2. read.html 예제 코드
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/basic.html}">
<head>
<title>Board Read</title>
</head>
<body>
<div layout:fragment="content">
<div class="row mt-3">
<div class="col">
<div class="card">
<div class="card-header">
Board Read
</div>
<div class="card-body">
<div class="input-group mb-3">
<span class="input-group-text">Bno</span>
<input type="text" name="bno" class="form-control" th:value="${dto.bno}" readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control" th:value="${dto.title}" readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Content</span>
<textarea name="content" class="form-control col-sm-5"
rows="5" readonly>[[${dto.content}]]</textarea>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control" th:value="${dto.writer}" readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">RegDate</span>
<input type="text" name="regdate" class="form-control"
th:value="${#temporals.format(dto.regDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">ModDate</span>
<input type="text" name="moddate" class="form-control"
th:value="${#temporals.format(dto.modDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
</div>
<div class="my-4">
<div class="float-end" th:with="link=${pageRequestDTO.getLink()}">
<a th:href="|@{/board/list}?${link}|" class="text-decoration-none">
<button type="button" class="btn btn-primary">List</button>
</a>
<a th:href="|@{/board/modify(bno=${dto.bno})}&${link}|" class="text-decoration-none">
<button type="button" class="btn btn-secondary">Modify</button>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
- <div layout:fragment="content">는 basic.html의 content라는 레이아웃 프래그먼트를 채우는 부분입니다.
- card-header와 card-body를 사용하여 카드 스타일을 적용합니다.
- input-group을 사용하여 게시글 번호(Bno), 제목(Title), 내용(Content), 작성자(Writer), 등록일(RegDate), 수정일(ModDate)을 각각 표시합니다.
- th:value 속성을 사용하여 서버에서 전달된 dto 객체의 속성값을 표시합니다.
- textarea를 사용하여 게시글의 내용을 표시합니다.
- [[${dto.content}]]는 내용 텍스트를 채웁니다.
- #temporals.format 함수를 사용하여 날짜를 지정된 형식(yyyy-MM-dd HH:mm:ss)으로 표시합니다.
- th:with 구문을 사용하여 link 변수를 정의하고, pageRequestDTO.getLink() 메서드의 반환값을 할당합니다.
- th:href 구문을 사용하여 동적으로 생성된 URL을 href 속성에 할당합니다.
- /board/list 링크는 List 버튼을 클릭하면 게시판 목록 페이지로 이동합니다.
- /board/modify 링크는 Modify 버튼을 클릭하면 수정 페이지로 이동하며, 게시글 번호(bno)와 추가 쿼리 문자열(link)이 포함됩니다.
3. Flash Attributes
- 리다이렉트된 후, 다음 요청에서 플래시 속성에 접근하여 데이터를 사용할 수 있습니다. 예를 들어, @GetMapping("/modify") 메서드에서 플래시 속성을 읽어올 수 있습니다.
@GetMapping("/modify")
public String modifyForm(@ModelAttribute("errors") List<ObjectError> errors,
@ModelAttribute("bno") Long bno,
Model model) {
// 플래시 속성인 errors와 bno를 사용하여 모델에 추가
model.addAttribute("errors", errors);
model.addAttribute("bno", bno);
return "modifyForm";
}
- @ModelAttribute를 사용하여 플래시 속성에 저장된 데이터를 메서드 매개변수로 받습니다.
- 이를 통해 modifyForm 뷰에서 유효성 검사 오류 메시지와 게시글 번호를 사용할 수 있습니다.
요약
FlashAttribute는 일회성 데이터를 리다이렉트 후에 전달할 때 사용됩니다. 유효성 검사 실패 시 오류 메시지와 같은 데이터를 리다이렉트된 요청에서 사용하려면 RedirectAttributes 객체의 addFlashAttribute 메서드를 사용하여 데이터를 추가하고, 리다이렉트된 컨트롤러에서 이를 받아 처리할 수 있습니다. 이를 통해 사용자는 유효성 검사 오류 메시지를 볼 수 있으며, 필요한 데이터를 유지할 수 있습니다.
4. HTML history
<script layout:fragment="script" th:inline="javascript">
const errors = [[${errors}]]
let errorMsg = ''
if(errors){
for(let i=0; i<errors.length; i++){
errorMsg += `${errors[i].field}은(는) ${errors[i].code} \n`
}
history.replaceState({}, null, null)
alert(errorMsg)
}
</script>
- history.replaceState()는 HTML5 History API의 한 메서드입니다. 이 API는 브라우저의 세션 기록 스택을 조작할 수 있는 기능을 제공합니다. history.replaceState()는 현재 브라우저의 URL을 변경하지만 페이지를 새로 고치지 않고, 브라우저의 히스토리 스택에 기록도 남기지 않습니다. 즉, 사용자에게는 URL만 바뀌고, 뒤로 가기 버튼을 눌렀을 때 이 URL이 기록되지 않으므로 브라우저 히스토리에는 영향을 미치지 않습니다.
- 이 코드에서 history.replaceState({}, null, null)의 역할과 의미는 다음과 같습니다:
- errors 배열:
- 서버에서 전달된 유효성 검사 오류 목록입니다.
- 이 오류 목록을 자바스크립트 변수 errors에 할당합니다.
- 오류 메시지 생성:
- errors 배열을 순회하며 각 오류의 필드(field)와 코드(code)를 문자열로 조합하여 errorMsg에 추가합니다.
- history.replaceState({}, null, null):
- 브라우저의 히스토리 스택에서 현재 상태를 새 상태로 교체합니다.
- 첫 번째 매개변수 {}: 새로운 상태 객체입니다. 비어 있는 객체를 전달하므로 상태 정보가 없습니다.
- 두 번째 매개변수 null: 페이지 제목을 설정하는 매개변수입니다. null을 전달하여 제목을 변경하지 않습니다.
- 세 번째 매개변수 null: 브라우저의 URL을 변경하는 매개변수입니다. null을 전달하여 현재 URL을 그대로 유지합니다.
- 여기서 중요한 점은 히스토리 스택에 새로운 항목을 추가하지 않고, 현재 상태를 덮어쓴다는 것입니다. 즉, 브라우저 히스토리에는 변화가 없으나 현재 페이지 상태를 변경합니다.
- 오류 메시지 표시:
- alert(errorMsg)를 통해 생성된 오류 메시지를 사용자에게 경고창으로 보여줍니다.
- 히스토리 조작: 사용자가 브라우저의 뒤로 가기 버튼을 눌렀을 때 특정 상태로 돌아가기를 원하지 않는 경우에 유용합니다.
- URL 변경 없이 상태 업데이트: 페이지를 새로고침하지 않고도 상태를 업데이트할 수 있습니다. 예를 들어, 유효성 검사 오류가 발생했을 때 URL을 그대로 유지하면서 오류 메시지를 표시할 수 있습니다.
- SPA (Single Page Application): SPA에서 페이지 전환 없이 URL을 변경하거나 상태를 업데이트할 때 사용됩니다.
- history.replaceState({}, null, null)는 브라우저 히스토리 스택을 조작하여 현재 상태를 새로운 상태로 교체합니다.
- 페이지를 새로 고침하거나 히스토리에 새로운 항목을 추가하지 않고 URL을 변경하거나 상태를 업데이트하는 데 사용됩니다.
- 이 코드에서는 유효성 검사 오류가 있을 때 경고창을 표시하고, 현재 URL을 히스토리 스택에 기록되지 않도록 합니다.
- errors 배열:
5. 버블링
document.querySelector(".modBtn").addEventListener("click", function (e){
e.preventDefault()
e.stopPropagation()
formObj.action = `/board/modify?${link}`
formObj.method = 'post'
formObj.submit()
}, false)
캡처링 vs 버블링
- 이벤트 버블링 (Bubbling): 이벤트가 가장 깊은 요소에서 시작하여 상위 요소로 전달됩니다.
- 이벤트 캡처링 (Capturing): 이벤트가 최상위 요소에서 시작하여 하위 요소로 전달됩니다.
addEventListener의 세 번째 인자
- true: 이벤트를 캡처링 단계에서 처리합니다.
- false (또는 생략): 이벤트를 버블링 단계에서 처리합니다.
- document.querySelector(".modBtn"): .modBtn 클래스를 가진 첫 번째 요소를 선택합니다.
- addEventListener("click", function (e) { ... }, false): 클릭 이벤트 리스너를 추가합니다.
이벤트 핸들러 함수
- e.preventDefault(): 기본 동작(예: 링크를 클릭했을 때 페이지 이동)을 막습니다.
- e.stopPropagation(): 이벤트가 상위 요소들로 전파되는 것을 막습니다.
- 폼 제출 설정:
- formObj.action = /board/modify?${link}```: 폼의 액션 URL을 /board/modify?${link}로 설정합니다.
- formObj.method = 'post': 폼의 전송 방식을 POST로 설정합니다.
- formObj.submit(): 폼을 제출합니다.
false의 역할
- false는 useCapture 옵션으로, 이벤트를 버블링 단계에서 처리하도록 합니다.
- 기본값이 false이므로, 생략해도 동일한 동작을 합니다.
요약
- false는 이벤트 리스너가 이벤트 버블링 단계에서 호출됨을 의미합니다.
- 이는 일반적으로 이벤트 핸들링의 기본 동작이며, 여기서는 명시적으로 설정한 것입니다.
- 생략해도 기본적으로 false로 처리됩니다. 따라서, 코드의 가독성을 위해 명시적으로 적는 경우도 있습니다.
결론
- false 또는 생략: 이벤트 리스너가 이벤트 버블링 단계에서 호출됩니다.
- true: 이벤트 리스너가 이벤트 캡처링 단계에서 호출됩니다.
5.1 이벤트 버블링
이벤트 버블링은 웹 브라우저에서 이벤트가 발생했을 때, 그 이벤트가 특정 요소에서 시작되어 상위 요소들로 전달되는 방식을 의미합니다. 이벤트 버블링 단계에서는 가장 구체적인 요소에서 발생한 이벤트가 점점 더 추상적인 상위 요소로 전파됩니다. 이를 "버블링"이라고 하는 이유는 이벤트가 가장 깊은 요소에서 시작하여 위로 "버블"처럼 떠오르기 때문입니다.
이벤트 흐름: 캡처링과 버블링
- 이벤트 캡처링 (Capturing Phase):
- 이벤트가 최상위 요소에서 시작하여 목표 요소(이벤트가 발생한 요소)로 전달됩니다.
- 이 단계에서는 addEventListener 메서드의 세 번째 인자로 true를 사용하여 이벤트를 처리합니다.
- 타겟 단계 (Target Phase):
- 이벤트가 실제로 발생한 목표 요소에서 이벤트가 처리됩니다.
- 이벤트 버블링 (Bubbling Phase):
- 이벤트가 목표 요소에서 시작하여 최상위 요소로 전파됩니다.
- 이 단계에서는 addEventListener 메서드의 세 번째 인자로 false를 사용하여 이벤트를 처리합니다(기본값).
예시
HTML 구조:
<div id="outer">
<button id="inner">Click me</button>
</div>
JavaScript 코드:
document.getElementById("outer").addEventListener("click", function() {
alert("Outer div clicked!");
}, false);
document.getElementById("inner").addEventListener("click", function(e) {
alert("Inner button clicked!");
}, false);
버튼을 클릭하면 두 개의 경고창이 뜹니다:
- "Inner button clicked!"
- "Outer div clicked!"
이벤트 버블링 단계에서 버튼 클릭 이벤트가 먼저 버튼 요소에서 처리되고, 이후에 그 상위 요소인 div에서도 처리됩니다.
이벤트 버블링을 사용하는 이유
- 편의성:
- 한 번의 이벤트 리스너로 여러 자식 요소의 이벤트를 처리할 수 있습니다. 예를 들어, 여러 버튼이 있는 경우 각 버튼에 이벤트 리스너를 따로 붙이는 대신 부모 요소에 한 번만 붙여서 처리할 수 있습니다.
- 메모리 효율:
- 많은 요소에 각각 이벤트 리스너를 추가하는 것보다 상위 요소에 한 번만 추가하는 것이 메모리 사용 측면에서 더 효율적입니다.
- 유연성:
- 이벤트가 어디서 발생했는지 쉽게 파악할 수 있습니다. 이벤트 객체의 target 속성을 사용하면 실제로 이벤트가 발생한 요소를 알 수 있습니다.
예제 코드의 의미
document.querySelector(".modBtn").addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
formObj.action = `/board/modify?${link}`;
formObj.method = 'post';
formObj.submit();
}, false);
- e.preventDefault(): 기본 동작(예: 링크를 클릭했을 때 페이지 이동)을 막습니다.
- e.stopPropagation(): 이벤트가 상위 요소로 전파되는 것을 막습니다. 이는 버블링 단계를 중지시킵니다.
- 폼 설정:
- formObj.action = /board/modify?${link}```: 폼의 액션 URL을 /board/modify?${link}로 설정합니다.
- formObj.method = 'post': 폼의 전송 방식을 POST로 설정합니다.
- formObj.submit(): 폼을 제출합니다.
- false: 이벤트 리스너가 이벤트 버블링 단계에서 호출되도록 합니다. 기본값이 false이므로 생략해도 동일한 동작을 합니다.
결론
이벤트 버블링은 웹 개발에서 중요한 개념으로, 효율적이고 편리한 이벤트 처리를 가능하게 합니다. 코드에서 false를 사용하는 이유는 이벤트가 버블링 단계에서 처리되도록 하기 위함이며, 이는 일반적으로 사용되는 방식입니다.
6. modify.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/basic.html}">
<head>
<title>Board Modify</title>
</head>
<div layout:fragment="content">
<div class="row mt-3">
<div class="col">
<div class="card">
<div class="card-header">
Board Modify
</div>
<div class="card-body">
<form th:action="@{board/modify}" method="post" id="f1">
<div class="input-group mb-3">
<span class="input-group-text">Bno</span>
<input type="text" name="bno" class="form-control" th:value="${dto.bno}" readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control" th:value="${dto.title}" >
</div>
<div class="input-group mb-3">
<span class="input-group-text">Content</span>
<textarea name="content" class="form-control col-sm-5"
rows="5" >[[${dto.content}]]</textarea>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control" th:value="${dto.writer}" readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">RegDate</span>
<input type="text" name="regdate" class="form-control"
th:value="${#temporals.format(dto.regDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
</div>
<div class="input-group mb-3">
<span class="input-group-text">ModDate</span>
<input type="text" name="moddate" class="form-control"
th:value="${#temporals.format(dto.modDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
</div>
<div class="my-4">
<div class="float-end">
<button type="button" class="btn btn-primary listBtn">List</button>
<button type="button" class="btn btn-secondary modBtn">Modify</button>
<button type="button" class="btn btn-danger removeBtn">Delete</button>
</div>
</div>
</form>
</div> <!--end cardBody-->
</div> <!--end card-->
</div> <!--end col-->
</div> <!--end row mt-3-->
</div>
<script layout:fragment="script" th:inline="javascript">
const errors = [[${errors}]]
let errorMsg = ''
if(errors){
for(let i=0; i<errors.length; i++){
errorMsg += `${errors[i].field}은(는) ${errors[i].code} \n`
}
history.replaceState({}, null, null)
alert(errorMsg)
}
const link = [[${pageRequestDTO.getLink()}]]
const formObj = document.querySelector("#f1")
//Modify 버튼을 눌렀을 때, 이벤트 처리
document.querySelector(".modBtn").addEventListener("click", function (e){
e.preventDefault()
e.stopPropagation()
formObj.action = `/board/modify?${link}`
formObj.method = 'post'
formObj.submit()
}, false)
</script>
</html>
- read.html과 코드가 거의 같다. script 부분만 차이가 있다.
- form 타입을 post로 보낼 때, BoardDTO를 매개변수로 보낸다. 따라서 컨트롤러에서 @Valid 어노테이션으로 보내는 타입과 name의 유효성을 검사하고, BindingResult로 에러가 있는지 반환한다.(list.html에서 했던 것 반복)
- form의 button type을 submit으로 하지 않고, 스크립트로 처리해서 querySelector로 이벤트 "click"을 감지하고 콜백 함수를 실행한다.
- 연결 link 처리를 스크립트로 처리했다.
7. BoardController 예제코드
@Controller
@RequestMapping("/board")
@Log4j2
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
~
@GetMapping({"/read", "/modify"}) //read, modify 둘다 사용하겠다.
public void read(Long bno, PageRequestDTO pageRequestDTO, Model model){
BoardDTO boardDTO = boardService.readOne(bno);
model.addAttribute("dto", boardDTO);
}
@PostMapping("/modify")
public String modify(PageRequestDTO pageRequestDTO,
@Valid BoardDTO boardDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes){
if(bindingResult.hasErrors()){
String link = pageRequestDTO.getLink();
redirectAttributes.addFlashAttribute("errors",
bindingResult.getAllErrors());
redirectAttributes.addAttribute("bno", boardDTO.getBno());
return "redirect:/board/modify?"+link;
}
boardService.modify(boardDTO);
redirectAttributes.addFlashAttribute("result", "modified");
redirectAttributes.addAttribute("bno", boardDTO.getBno());
return "redirect:/board/read";
}
}
- @GetMapping({"/read", "/modify"} public void read, @PostMapping("/modify") public String modify 코드가 추가되었다.