본문 바로가기
실습/리눅스 서버 + 스프링 부트

[Thymeleaf] 데이터를 동적으로 바꾸기 - replaceWith

by 이민우 2024. 9. 14.
728x90
반응형

재직중인 회사에서는 웹 UI 개발 시 거의 전부 Thymeleaf\ 사용을 선호한다. 그리고 DOM 관리에는 JQuery를, 데이터 로딩에는 Ajax를 사용하는 것을 선호한다.

 

사실 처음부터 웹 UI 개발을 주로 할 생각은 없었다. 원래는 백엔드 개발자가 되고 싶었고, 현재 회사도 원래는 백엔드 개발자로 들어오게 되었다. 하지만 만성 인력난에 허덕이는 중소기업의 특성상 백엔드 개발만 할 수는 없고, 제안서 작성부터 요구사항 식별, 설계, 개발, 테스트, 배포 등 전과정을 해야만 했다. 웹 UI 개발도 별로 원하지는 않았지만 어쩔 수 없이 해야하는 부분 중 하나였다.

 

그동안 웹 UI를 개발할 때마다 나는 한 가지 말을 입에 달고 살았다.

저는 원래 웹 개발자가 아니에요.

 

여러 가지 뜻이 함축된 말이었다. 백엔드 시켜준대서 왔는데 이런 것까지 해야하느냐? 나는 웹 개발을 제대로 하지 못하니 너무 좋은 퀄리티의 코드는 기대하지 말아라.  등.

 

이번 프로젝트에서도 어쩌다보니 웹 개발을 전담하게 되었다. 그래서 똑같이 말을 하고 개발을 진행했다.

 

다만 평소와 다른 것은, 이번에는 참고할만한 코드가 존재한다는 점이었다.

 

같은 업체에서 발주된 비슷한 프로젝트가 두 개가 있다. 내가 현재 참여해서 웹 개발을 전담하고 있는 것은 두 프로젝트 중 뒤늦게 시작된 프로젝트였다.

 

뒤늦게 시작되었으나 비슷한 프로젝트가 진행되고 있는만큼, 상사분들께서는 앞서 개발된 코드를 참고해서 빠르게 개발하기를 원하셨고, 이로 인해 해당 코드를 분석하고 수정해서 개발을 진행하게 되었다.

 

그리고 그렇게 남의 코드를 기반으로 개발을 진행하다보니 스스로 많이 잘못 개발하고 있다는 사실을 인지했다.

 

위에서 언급한 "나는 원래 웹 개발자가 아니다" 라는 말에 스스로 안주해서 다음과 같이 생각하고 있었던 것이다.

나는 원래 웹 개발자가 아님에도 웹 개발을 "해주고" 있는 것이므로, 그 전문성이 깊을 필요는 없다.

 

백엔드가 하고싶어 그에 대한 공부는 충분히 했고, 지금도 계속해서 하고있다고 자신한다. 하지만 웹 개발에 대해서는 딱히 공부를 할 생각도 없었고, 그냥 "잘되면 그만" 이라는 생각에 사로잡혀 있었던 것이다. 이로 인해  진짜 "웹 개발만 하신 개발자들"의 코드를 보니 신세계가 펼쳐진 기분이었다. 그 중 대표적인 예시는 바로 Thymeleaf의 replaceWith였다.

 

오늟은 반성의 의미로 지금까지 어떻게 개발을 잘못하고 있었는지와, replaceWith를 사용하면 해당 부분이 얼마나 쉽게 개발이 가능하게 개선이 되는지를 포스팅해보고자 한다.

 

 

Thymeleaf에서 데이터를 출력하는 방식

Thymeleaf에는 th:text 등의 키워드를 통해 정적인 데이터를 화면에 출력할 수 있다.

 

예를 들어 컨트롤러에서 다음과 같은 데이터를 넘겼다고 가정해보자.

 

그러면 th:text, th:value 등의 키워드를 통해 아래와 같이 데이터를 추출할 수 있게된다.

span에 test_data를 출력

 

그리고 결과는 아래와 같이 추출된다.

 

기존에 사용하던 방식 (직접 html을 작성해 append)

하지만 위 방식을 사용할 일은 그렇게 많지 않았다.

 

이유라면 대부분의 데이터는 동적인 데이터였기 때문이다. 그렇다면 정적인 데이터는? 이라고 말할 수 있겠지만 이 부분은 옛날에 장과장님에게 들었던 조언이 있었다.

정적인 데이터를 Model에 담아서 페이지 출력 시에 보여주는 건 좋아.

그런데 만약 그 정적인 데이터 자체나 로딩 방식에 문제가 있으면 어떻게 될까? 페이지는 안뜨고 묻지도 따지지도 않고 에러 페이지로 넘어가겠지? 그럼 사용자는 뭐가 문제인지 알 수 있을까? 무슨 말인지 알지?

그래서 페이지 첫 페이지 로딩에는 페이지만 return해버려야 하는거야. 그리고 그 외 데이터는 아무리 정적인 데이터라도 ajax로 가져오게 만들어. 그러면 만약 에러가 발생하면 modal이나 alert로 사용자한테 뭐가 문제인지 알람을 띄워줄 수가 있거든.

 

해당 조언을 듣기 전까지는 페이지 안에서 변하지 않는 정적인 데이터나, 동적인 데이터라도 첫 페이지 로딩 시 보여주는 데이터, 예를 들어 테이블의 1페이지 정도는 위와 같이 Model에 담아서 페이지 진입 시 함께 출력했었다.

 

하지만 저 조언 이후로 크게 꺠달아서 페이지는 그저 페이지만 return해주고, 그 외 필요한 데이터는 전부 Ajax로 호출하는 방식을 취했다. 만약 에러가 발생하면 에러의 원인을 사용자에게 출력할 수 있도록.

 

그래서 위 방식을 크게 사용하지 않았다.

 

그렇다면 데이터를 출력할 떄 어떻게 했는가?

 

그냥 DOM Content에 .html()를 호출했다.

 

예를 들면 아래와 같은 식이었다.

 

아래와 같이 페이지에 따라서 각기 다른 데이터를 주도록 Controller를 작성하자.

@Controller
public class TestController {

	@GetMapping("/")
	public String getUI() {
		return "test";
	}
	
	@GetMapping("/data/{page}")
	public @ResponseBody Map<String, Object> getData(@PathVariable(name = "page", required = true) int page) {
		
		// 페이지 수 가 ( 데이터가 2페이지만큼 있다고 가정 )
		int totalPage = 2;
		
		// DB에서 가져온 데이터 가정
		List<Map<String, Object>> dataList = new ArrayList<>();

		Map<String, Object> dataOne = new HashMap<>();
		Map<String, Object> dataTwo = new HashMap<>();
		Map<String, Object> dataThree = new HashMap<>();
		
		switch(page) {
		case 1: 
			// 1페이지
			dataOne.put("id", "111");
			dataOne.put("name", "lee");
			dataTwo.put("id", "222");
			dataTwo.put("name", "kim");
			dataThree.put("id", "333");
			dataThree.put("name", "park");
			break;
		case 2:
			// 1페이지
			dataOne.put("id", "444");
			dataOne.put("name", "kim");
			dataTwo.put("id", "555");
			dataTwo.put("name", "choi");
			dataThree.put("id", "666");
			dataThree.put("name", "yu");
			break;
		}
		
		dataList.add(dataOne);
		dataList.add(dataTwo);
		dataList.add(dataThree);
		
		// 데이터를 전부 담아서 return
		Map<String, Object> data = new HashMap<>();
		data.put("totalPage", totalPage);
		data.put("data", dataList);
		
		return data;
		
	}
}

 

 

그리고 평소 하던대로 Html을 작성했다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<title>Thymeleaf</title>
	<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
	<div id="data-container">
		<!-- 동적으로 데이터가 뿌려지는 부분 -->
	</div>

	<div id="pagination-container">
		<!-- 동적으로 페이지가 뿌려지는 부분 -->
	</div>

	<script>
		// 페이지 이동 시 해당 페이지에 맞는 데이터 로딩 후 data-container에 삽입하는 코드
		function goPage(page) {
			$.ajax({
				url: '/data/' + page,
				method: 'GET',
				success: function(response) {
					var totalPage = response.totalPage;
					var dataList = response.data;
	
					var table = '<table border="1"><tr><th>ID</th><th>Name</th></tr>';
	
					$.each(dataList, function(index, item) {
						table += '<tr><td>' + item.id + '</td><td>' + item.name + '</td></tr>';
					});
	
					table += '</table>';
	
					$('#data-container').html(table);
	
					setPagination(totalPage);
				},
				error: function() {
					alert('Error loading data');
				}
			});
		}

		// goPage 이후 불러온 페이지를 pagination-container에 삽입하는 코드
		function setPagination(totalPage) {
			var pagination = '';
			for (var i = 1; i <= totalPage; i++) {
				$('#pagination-container').append(
					pagination += '<button class="page-btn" data-page="' + i + '">' + i + '</button>'
				);
			}
			
			$('#pagination-container').html(pagination); 

			$('.page-btn').click(function() {
				var page = $(this).data('page');
				goPage(page);
			});
		}
		
		$(document).ready(function() {
			// 최초 데이터 로든
			goPage(1);
		});
	</script>
</body>
</html>

 

보는 것과 같이, html 코드를 직접 javaScript 내에서 작성하고 있다. 그리고 마지막에 .html() 문법을 이용해 해당 div의 html을 만들어진 html로 대체하고 있는 방식이다

 

 

새로 알게된 방식

.당연한 말이지만, 위 방식은 꽤 귀찮다.

 

사전에 퍼블리싱된 html을 긁어와서 하나하나 javascript 내에서 동적으로 생성하는 코드로 만들어줘야 하는 방식이다.

 

위 예시야 간단한 데이터를 다루니 괜찮지만, 실제 데이터는 한 두개로 끝나지 않을 뿐더러, 각 데이터 클릭 시 이벤트에 대한 함수가 들어가니 이는 너무 귀찮은 방법이다.

 

하지만 다른 방법이 없었고, 아니 굳이 정확히 말하자면 찾을 생각조차 하지 않았기에 위 방법을 그대로 사용하고 있었다.

 

다만 이번 프로젝트에서 "replaecWith" 함수를 알게 되었고, 참 좋은 방법이라는 생각이 들었다.

 

우선 위의 데이터를 th:text 같은 타임리프 문법으로 html을 만들면 아래와 같은 모양새가 될 것이다.

<!-- 동적으로 데이터가 뿌려지는 부분 -->
<table border="1">
	<tr>
		<th>ID</th>
		<th>Name</th>
	</tr>
	<tr th:each="item: ${data}">
		<td th:text="${item.id}"></td>
		<td th:text="${item.name}"></td>
	</tr>
</table>
		
<!-- 동적으로 페이지가 뿌려지는 부분 -->
<span th:each="pageNum : ${totalPage}">
	<button class="page-btn" th:text="${pageNum}" th:data-page="${pageNum}"></button>
</span>

 

그리고 이를 하나의 id로 묶어준다. 원래대로라면 pagination 부분은 UI 내 페이지가 존재하는 모든 곳에 존재해야하므로 따로 뗴서 html로 만들어야겠지만, 지금은 합쳐서 사용하겠다.

 

<div id="data-container">
	<!-- 동적으로 데이터가 뿌려지는 부분 -->
	<table border="1">
		<tr>
			<th>ID</th>
			<th>Name</th>
		</tr>
		<tr th:each="item: ${data}">
			<td th:text="${item.id}"></td>
			<td th:text="${item.name}"></td>
		</tr>
	</table>
	
	<!-- 동적으로 페이지가 뿌려지는 부분 -->
	<span th:each="pageNum : ${totalPage}">
		<button class="page-btn" th:text="${pageNum}" th:data-page="${pageNum}"></button>
	</span>
</div>

 

그리고 Controller을 아래와 같이 변경한다. 어떻게 변경되었는지는 주석으로 적어놓았다.

/* <변경> return을 Map에서 String으로 변경 */
@GetMapping("/data/{page}")
public String getData(@PathVariable(name = "page", required = true) int page, /* <변경> 데이터를 담을 Model 추가 */Model model) {
	
	/* <변경> 그냥 가져가서 #numbers.sequence를 돌리면 에러가 발생하므로 List로 변경 */
	// 페이지 수 가 ( 데이터가 2페이지만큼 있다고 가정 )
	int totalPage = 2;
	List<Integer> pageList = new ArrayList<>();
	for(int i=1; i<=totalPage; i++) {
		pageList.add(i);
	}
	
	// DB에서 가져온 데이터 가정
	List<Map<String, Object>> dataList = new ArrayList<>();

	Map<String, Object> dataOne = new HashMap<>();
	Map<String, Object> dataTwo = new HashMap<>();
	Map<String, Object> dataThree = new HashMap<>();
	
	switch(page) {
	case 1: 
		// 1페이지
		dataOne.put("id", "111");
		dataOne.put("name", "lee");
		dataTwo.put("id", "222");
		dataTwo.put("name", "kim");
		dataThree.put("id", "333");
		dataThree.put("name", "park");
		break;
	case 2:
		// 1페이지
		dataOne.put("id", "444");
		dataOne.put("name", "kim");
		dataTwo.put("id", "555");
		dataTwo.put("name", "choi");
		dataThree.put("id", "666");
		dataThree.put("name", "yu");
		break;
	}
	
	dataList.add(dataOne);
	dataList.add(dataTwo);
	dataList.add(dataThree);
	
	/* <변경> Map이 아니라 Model에 담아서 return */
	// 데이터를 전부 담아서 return
	model.addAttribute("totalPage", pageList);
	model.addAttribute("data", dataList);
	
	/* <변경> html명 :: 데이터를 뿌릴 dom의 id */
	return "test :: #data-container";
}

 

 

그리고 html도 직접 string을 이어붙여가며 html을 작성하는 것이 아니라, 간단하게 replaceWith만 불러와서 사용하도록 아래와 같이 변경한다.

 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<title>Thymeleaf</title>
	<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
	<div id="data-container">
		<!-- 동적으로 데이터가 뿌려지는 부분 -->
		<table border="1">
			<tr>
				<th>ID</th>
				<th>Name</th>
			</tr>
			<tr th:each="item: ${data}">
				<td th:text="${item.id}"></td>
				<td th:text="${item.name}"></td>
			</tr>
		</table>
		
		<!-- 동적으로 페이지가 뿌려지는 부분 -->
		<span th:each="pageNum : ${totalPage}">
			<button class="page-btn" th:text="${pageNum}" th:data-page="${pageNum}"></button>
		</span>
	</div>

	<script>
		// 페이지 이동 시 해당 페이지에 맞는 데이터 로딩 후 data-container에 삽입하는 코드
		function goPage(page) {
			$.ajax({
				url: '/data/' + page,
				method: 'GET',
				success: function(response) {
					$("#data-container").replaceWith(response);

					$('.page-btn').click(function() {
						var page = $(this).data('page');
						goPage(page);
					});
				},
				error: function() {
					alert('Error loading data');
				}
			});
		}
		
		$(document).ready(function() {
			// 최초 데이터 로든
			goPage(1);
		});
	</script>
</body>
</html>

 

이제 결과를 확인하면 직접 string을 이어붙여가며 html을 만들고 .html()로 갈아끼는 것처럼 똑같이 잘 나옴을 확인할 수 있다.

728x90
반응형