본문 바로가기
Spring/Thymeleaf

Thymeleaf - Ajax Response 값 활용하여 View 갱신하기

by WangTak 2022. 3. 4.
반응형

타임리프가 자바 진영의 서버 사이드 템플릿인 만큼 타임리프를 사용하면 필수적으로 HTML, CSS, JavaScript, jQuery 등과 같은 기술도 같이 사용해야 합니다. 그래서 다른 전문적인 프론트 개발자분들처럼은 못해도 뷰단을 코딩할 때도 최대한 컨벤션을 지키거나 유지 보수하기에 용이하도록 깔끔하게 코드를 짜려고 노력하는 편입니다.

 

다만, Ajax를 사용하여 웹 페이지 일부분을 렌더링하고자 할 때 유지보수는 꿈꿀 수 없을 정도로 더럽게 코드를 짜고 있는 제 모습을 볼 수 있었습니다. 그래서 코드를 깔끔하게 짜고자 모듈화를 한 경험에 대해 정리해볼까 합니다.  

 

프로젝트 버전은 다음과 같습니다.

  • 개발 도구: IntelliJ
  • Spring Boot: 2.5.10
  • Java 11
  • Thymeleaf
  • Spring Web, Lombok

 

※ DB, JPA는 따로 사용하지 않고 Repository에서 하드코딩된 임시 객체 리스트를 반환하도록 하겠습니다.

 

예시 화면

먼저 이번에 해볼 프로젝트는 예시 화면 사진처럼 "모든 멤버 찾기" 버튼을 눌렀을 때 Ajax를 이용하여 API를 호출한 뒤에 그 결과를 페이지 이동 없이 렌더링 하는 아주 간단한 프로젝트입니다.

흐름도

 

흐름도는 위 사진과 같습니다. 사용자가 메인 화면 ①번 ("/")을 요청했을 때 index.html을 보여주게 됩니다. index.html에 있는 "모든 멤버 찾기" 버튼을 누르는 것이 흐름도의 ⑥번입니다. 그러면 위 흐름도의 순서대로 각각의 영역에서 맡은 역할을 수행하고 MemberApiController에서는 MemberService를 통해 반환된 결과 값을 그대로 리턴하는 것이 아닌 (MemberApiController는 @RestController가 아닌 @Controller입니다.) Model에 결과 값을 넣어주고 member-list.htmlView를 리턴하도록 합니다. 그러면 member-list.html에서는 Model을 통해 넘어온 결과 값을 이용해 View를 그리게 됩니다. index.html에서 사용한 Ajax의 응답 값은 바로 member-list.html이며, replaceWith로 보여주고자 하는 영역을 뿅하고 바꿔주는 것이 전체 흐름입니다.

 

MemberController

@Controller
public class MemberController {

    @GetMapping("")
    public String home() {
        return "index";
    }
}

 

MemberApiController

@Controller
@RequiredArgsConstructor
public class MemberApiController {

    private final MemberUseCase memberUseCase;

    @PostMapping("/members/search")
    public String searchMembers(Model model) {
    	/*
        	Member member1 = new Member("wangtak@gmail.com", "왕탁이");
	    	Member member2 = new Member("admin@gmail.com", "관리자");
    		Member member3 = new Member("manager@gmail.com", "매니저");
        	Member member4 = new Member("user@gmail.com", "사용자");

	        Arrays.asList(member1, member2, member3, member4);
        */
        List<Member> findMembers = memberUseCase.findAll();

        model.addAttribute("members", findMembers);

        return "member-list";
    }
}

 

※ memberUseCase.findAll()의 반환 값은 주석 처리된 방식의 그냥 하드 코딩된 4개의 Member 객체 리스트입니다.

 

index.html

<body>

<div class="container">
    <h1>Ajax 호출</h1>
    <button type="button" onclick="getMemberList()">모든 멤버 찾기</button>

    <div id="memberListContent"></div>
</div>

</body>
<script type="text/javascript">
    function getMemberList() {
        $.ajax({
            type: "POST",
            url: "/members/search",
            // data: form, // api 호출을 위한 요청 변수가 필요하다면 사용해주세요.
            dataType: "text"
        })
            .done(function (result) {
                console.log(result);
                $("#memberListContent").replaceWith(result);
            })
            .fail(function(jqXHR) {
                console.log(jqXHR);
            })
            .always(function() {
                console.log("요청, 응답 결과에 상관없이, 이건 항상 실행됩니다.");
            })
    }
</script>

 

member-list.html

<!doctype html>
<html lang="ko"
      xmlns:th="http://www.thymeleaf.org">

<div id="memberListContent">

    <ul>
        <li th:each="member: ${members}" th:text="|${member.email} , ${member.name}|"></li>
    </ul>

    <!-- Page Marker -->

</div>

</html>

 

그러면 실제로 위 코드를 실행해서 확인해보겠습니다. Spring을 실행시켜주고 localhost:8080으로 접속해보면 위 "예시 화면" 사진의 왼쪽처럼 li 태그가 없는 "Ajax 호출", "모든 멤버 찾기" 버튼이 있을겁니다. 소스 코드 보기를 해보면 다음과 같습니다.

버튼 누르기 전의 소스

 

index.html의 코드가 그대로 적혀있는 것을 확인할 수 있습니다. 여기서 "모든 멤버 찾기" 버튼을 누르게 되면 다음과 같이 코드가 바뀌게 됩니다.

 

버튼 누른 후의 소스

 

공백으로 존재하던 div#memberListContent 밑에 ul, li의 리스트 형식의 태그가 추가된 것을 알 수 있습니다. 이것은 member-list.html의 코드가 쏙하고 들어온 것을 의미하며, 그 코드는 Model로 넘겨 받은 Member 객체 리스트를 타임리프 문법인 forEach가 적용된 member-list.html 인 것을 알 수 있습니다.

 

추가적으로 "버튼 누른 후의 소스 사진"처럼 Page Marker를 추가하여 Paging까지 적용할 수 있습니다. 위 방법을 통해 좀 더 다양하고 동적인 웹 화면을 유지보수하기 쉬운 코드로 만들 수 있을 거 같습니다.

 

주의할 점!

현재 Ajax의 done callback 함수에서 #memberListContent를 결과에 따라 replaceWith를 하기 때문에 전환되는 영역의 id(index.html의 div#memberListContent)와 그려주는 영역의 id(member-list.html의 div#memberListContent)는 모두 같아야합니다.

 

 

반응형