Coding 공부/IntelliJ

[IntelliJ_Springboot_MariaDB] Thymleaf 하이퍼 링크, read.html 예제 코드, Flash Attributes , HTML history, 버블링, 이벤트 흐름: 캡처링과 버블링, modify.html, BoardController 예제코드

CBJH 2024. 5. 21.
728x90
반응형

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)의 역할과 의미는 다음과 같습니다:
    1. errors 배열:
      • 서버에서 전달된 유효성 검사 오류 목록입니다.
      • 이 오류 목록을 자바스크립트 변수 errors에 할당합니다.
    2. 오류 메시지 생성:
      • errors 배열을 순회하며 각 오류의 필드(field)와 코드(code)를 문자열로 조합하여 errorMsg에 추가합니다.
    3. history.replaceState({}, null, null):
      • 브라우저의 히스토리 스택에서 현재 상태를 새 상태로 교체합니다.
      • 첫 번째 매개변수 {}: 새로운 상태 객체입니다. 비어 있는 객체를 전달하므로 상태 정보가 없습니다.
      • 두 번째 매개변수 null: 페이지 제목을 설정하는 매개변수입니다. null을 전달하여 제목을 변경하지 않습니다.
      • 세 번째 매개변수 null: 브라우저의 URL을 변경하는 매개변수입니다. null을 전달하여 현재 URL을 그대로 유지합니다.
      • 여기서 중요한 점은 히스토리 스택에 새로운 항목을 추가하지 않고, 현재 상태를 덮어쓴다는 것입니다. 즉, 브라우저 히스토리에는 변화가 없으나 현재 페이지 상태를 변경합니다.
    4. 오류 메시지 표시:
      • alert(errorMsg)를 통해 생성된 오류 메시지를 사용자에게 경고창으로 보여줍니다.
    history.replaceState의 의미와 사용 사례
    • 히스토리 조작: 사용자가 브라우저의 뒤로 가기 버튼을 눌렀을 때 특정 상태로 돌아가기를 원하지 않는 경우에 유용합니다.
    • URL 변경 없이 상태 업데이트: 페이지를 새로고침하지 않고도 상태를 업데이트할 수 있습니다. 예를 들어, 유효성 검사 오류가 발생했을 때 URL을 그대로 유지하면서 오류 메시지를 표시할 수 있습니다.
    • SPA (Single Page Application): SPA에서 페이지 전환 없이 URL을 변경하거나 상태를 업데이트할 때 사용됩니다.
    요약
    • history.replaceState({}, null, null)는 브라우저 히스토리 스택을 조작하여 현재 상태를 새로운 상태로 교체합니다.
    • 페이지를 새로 고침하거나 히스토리에 새로운 항목을 추가하지 않고 URL을 변경하거나 상태를 업데이트하는 데 사용됩니다.
    • 이 코드에서는 유효성 검사 오류가 있을 때 경고창을 표시하고, 현재 URL을 히스토리 스택에 기록되지 않도록 합니다.

 

 

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): 클릭 이벤트 리스너를 추가합니다.

이벤트 핸들러 함수

  1. e.preventDefault(): 기본 동작(예: 링크를 클릭했을 때 페이지 이동)을 막습니다.
  2. e.stopPropagation(): 이벤트가 상위 요소들로 전파되는 것을 막습니다.
  3. 폼 제출 설정:
    • 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 이벤트 버블링

 

이벤트 버블링은 웹 브라우저에서 이벤트가 발생했을 때, 그 이벤트가 특정 요소에서 시작되어 상위 요소들로 전달되는 방식을 의미합니다. 이벤트 버블링 단계에서는 가장 구체적인 요소에서 발생한 이벤트가 점점 더 추상적인 상위 요소로 전파됩니다. 이를 "버블링"이라고 하는 이유는 이벤트가 가장 깊은 요소에서 시작하여 위로 "버블"처럼 떠오르기 때문입니다.

이벤트 흐름: 캡처링과 버블링

  1. 이벤트 캡처링 (Capturing Phase):
    • 이벤트가 최상위 요소에서 시작하여 목표 요소(이벤트가 발생한 요소)로 전달됩니다.
    • 이 단계에서는 addEventListener 메서드의 세 번째 인자로 true를 사용하여 이벤트를 처리합니다.
  2. 타겟 단계 (Target Phase):
    • 이벤트가 실제로 발생한 목표 요소에서 이벤트가 처리됩니다.
  3. 이벤트 버블링 (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);

버튼을 클릭하면 두 개의 경고창이 뜹니다:

  1. "Inner button clicked!"
  2. "Outer div clicked!"

이벤트 버블링 단계에서 버튼 클릭 이벤트가 먼저 버튼 요소에서 처리되고, 이후에 그 상위 요소인 div에서도 처리됩니다.

이벤트 버블링을 사용하는 이유

  1. 편의성:
    • 한 번의 이벤트 리스너로 여러 자식 요소의 이벤트를 처리할 수 있습니다. 예를 들어, 여러 버튼이 있는 경우 각 버튼에 이벤트 리스너를 따로 붙이는 대신 부모 요소에 한 번만 붙여서 처리할 수 있습니다.
  2. 메모리 효율:
    • 많은 요소에 각각 이벤트 리스너를 추가하는 것보다 상위 요소에 한 번만 추가하는 것이 메모리 사용 측면에서 더 효율적입니다.
  3. 유연성:
    • 이벤트가 어디서 발생했는지 쉽게 파악할 수 있습니다. 이벤트 객체의 target 속성을 사용하면 실제로 이벤트가 발생한 요소를 알 수 있습니다.

예제 코드의 의미

document.querySelector(".modBtn").addEventListener("click", function (e) {
    e.preventDefault();
    e.stopPropagation();
    formObj.action = `/board/modify?${link}`;
    formObj.method = 'post';
    formObj.submit();
}, false);
  1. e.preventDefault(): 기본 동작(예: 링크를 클릭했을 때 페이지 이동)을 막습니다.
  2. e.stopPropagation(): 이벤트가 상위 요소로 전파되는 것을 막습니다. 이는 버블링 단계를 중지시킵니다.
  3. 폼 설정:
    • formObj.action = /board/modify?${link}```: 폼의 액션 URL을 /board/modify?${link}로 설정합니다.
    • formObj.method = 'post': 폼의 전송 방식을 POST로 설정합니다.
    • formObj.submit(): 폼을 제출합니다.
  4. 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 코드가 추가되었다.

댓글