♻️ JSP → Thymeleaf 마이그레이션

1. 마이그레이션을 결심한 이유
기존 프로젝트는 JSP 기반의 View를 사용했고, 프론트엔드단을 전부 혼자 만들었으니 나중에 혹 레거시를 다루게 된다면 낯설지 않게 작업할 정도는 되지 않을까 했다.
프로젝트 전반적인 리팩토링을 위해 STS(Spring Tool Suite)에서 IntelliJ로 프로젝트를 옮겨가면서, 초기 설정 시 큼직한 프로젝트 구성요소를 많이 변경했다. (MyBatis → JPA, 로컬DB → AWS 등)
그러면서 익숙했던 JSP 대신, 다음과 같은 이유로 Thymeleaf로의 전환을 결정했다.
- 긴 코드 → 템플릿의 가독성과 유지보수성 부족
- 백엔드-프론트엔드 간 역할 분리 미비
- HTML 표준 위반이 많아 프론트 개발에 제약 발생
나머지는 마이그레이션 하면서 추가적으로 생각해 봤던 것이지만, 특히 1번 같은 경우는 JSP를 사용하는 내내 체감한 바이기도 했다.
그러나 JPA 때처럼, Thymeleaf의 존재를 몰랐기에 알고 있던 JSP로 처음 프로젝트를 만들었었다.
2. 타임리프(Thymeleaf)란 무엇인가?
그러면 Thymeleaf가 어떤 템플릿인가? 하면, 정확히 말하면 Spring Boot에서 주로 사용하는 서버사이드 템플릿 엔진 중 하나다.
쉽게 말하면 백엔드에서 HTML 파일을 동적으로 생성할 수 있도록 도와주는 도구라고 할 수 있다.
HTML을 템플릿처럼 사용하면서 서버에서 데이터를 주입해 완성된 HTML을 만들어주는 방식이기 때문에, 컨트롤러에서 데이터를 Model에 담아서 View로 넘기면 .html 파일에서 그 데이터를 꺼내서 화면에 출력할 수 있다.
- HTML5 표준을 준수함 → 브라우저에서 열어도 일반 HTML처럼 잘 보인다(<%%>와 같은 태그가 없음).
- 문법이 직관적이고 깔끔하다.
- Spring Boot 기본 뷰 리졸버가 Thymeleaf → Spring MVC 패턴과 용이하게 통합된다.
- 백엔드가 HTML 뷰까지 직접 렌더링함 → SSR에 적합하다!
BE와 FE를 개인 프로젝트로 묶어 한꺼번에 관리하고 있었고, FE단이 게시판 이상으로 복잡하지 않았기 때문에, 리액트로 이전할 게 아니라면 Thymeleaf는 좋은 선택이었다.
3. CSR vs. SSR
이 마이그레이션을 기획하면서 SSR(서버 사이드 렌더링)과 CSR(클라이언트 사이드 렌더링)이라는 개념을 접할 수 있었다.
추후 작성할 커뮤니티 섹션의 댓글에서는 CSR을 적용했는데, (자세한 건 커뮤니티 게시물에서)
최근 CSR 방식의 웹사이트를 더 많이 보기도 했고, 그 방식이 해당 기능에는 적합하다고 생각했기 때문이었다.
그러나 내 프로젝트 전반의 경우, 개발의 복잡성과 성능을 트레이드 오프해 봤을 때는 성능 증가에 비해 복잡성을 낮추는 방향으로 가는 것이 좋겠다고 판단했다.
| CSR (Client Side Rendering) | SSR (Server Side Rendering) |
| - 클라이언트에서 데이터를 렌더링해서 보여줌 - REST API를 호출해서 데이터를 JSON으로 응답받아 화면에 표시 - 클라이언트에 데이터가 늦게 노출됨 |
- 서버에서 데이터를 렌더링해서 보여줌 - 화면의 깜빡임이 존재 - 포털 검색에 걸림 - 컨트롤러에서 넘겨받은 model을 포함한 view를 서버에서 전부 렌더링한 상태에서 표시 |
리팩토링 시 다양한 기능을 REST API로 바꿔두었기 때문에, 이번에는 BE단에 집중하려고 FE는 타임리프로 가는 쪽을 선택했지만 추후 리액트로의 마이그레이션도 한 번 시도해보고 싶다는 생각이 있다.
4. 전환 과정 요약
아래와 같은 과정을 거쳐서 JSP 파일을 HTML으로 변경, Thymeleaf 템플릿을 적용했다.
| 단계 | 설명 |
| 템플릿 구조 분석 | 기존 JSP 내 include, taglib 구조 파악 |
| 공통 레이아웃 도입 | Thymeleaf layout: 구조 설계 |
| 개별 View 변환 | JSTL → Thymeleaf 문법 변환 |
| 컨트롤러 수정 | Model 속성 키 변경, 경로 수정 |
| JS 렌더링 호환성 대응 | 템플릿 내 ${} → [[...]] or th:... 변경 |
단순한 변환 과정이었기 때문에 생성형 AI의 도움을 좀 받으려 했는데, 중간중간 빠뜨리고 빼먹는 부분이 예상 외로 많아 품이 두 배로 들었다.
5. 예상하지 못한 문제와 해결
위에서 언급한 대로, 원 코드와 몇 가지 충돌이 생겨서 REST API들도 적용하는 김에 FE단을 적당히 갈아엎었다.
한 번 이 작업을 하고 나니 그 뒤론 손댈 게 많지 않고, 작업이 JSP보다 한결 편했다.
| 문제 | 해결 방법 |
| JS 내부 ${} 충돌 | [[...]], th:attr 활용 |
| 공통 레이아웃 안의 js/css 경로 깨짐 | @{...} 문법으로 경로 지정 or 수동으로 수정 |
| 기존 JSTL 커스텀 태그 미지원 | Controller에서 Model 처리 후 전달 |
| Form 관련 태그 호환 문제 | th:태그 등 명시적 설정 사용 |
6. 주요 변경 사항
(1) include → layout
마이그레이션 전(JSP):
<%@include file ="../header.jsp" %>
마이그레이션 후(Thymeleaf):
<th:block layout:fragment="content">
(2) 변수 출력 방식
마이그레이션 전(JSTL):
// path 지정 등에 JSTL 사용
<c:set var="path" value="${pageContext.request.contextPath}"/>
마이그레이션 후 (Thymeleaf):
// JSTL 없이 주소 직접 사용
if (firstimage2 == "") {firstimage2 = "/img/map/infoEmpty.png";}
var image = $('<img>').addClass('card-image').attr('src', firstimage2);
(3) 조건문 및 반복문
마이그레이션 전 (JSTL):
// C:forEach 태그 사용
function addMarker(position, title, map, icon, id, type, name_kor) {
var marker = new google.maps.Marker({
position: position,
map: map,
title: title,
icon: icon,
type: type, // 마커 유형 지정
name_kor: name_kor
});
var metStations = [
<c:forEach var="metStation" items="${metNear}" varStatus="status">
{
id: "${metStation.met_id}", // 메트로 스테이션의 ID 추가
lat: parseFloat("${metStation.latitude}"),
lng: parseFloat("${metStation.longitude}"),
name: "${metStation.met_name}",
type: 'metroStation', // 마커 유형 지정
name_kor: "${metStation.met_name_kor}",
}<c:if test="${!status.last}">,</c:if>
</c:forEach>
];
마이그레이션 후(Thymeleaf):
// JavaScript단 해석이 용이
function addMarker(position, title, map, icon, id, type, name_kor) {
var marker = new google.maps.Marker({
position: position,
map: map,
title: title,
icon: icon,
type: type, // 마커 유형 지정
name_kor: name_kor
});
function updateMapWithLocations(metroStations, busStops) {
metroStations.forEach(function(metroStation) {
addMarker(
{lat: metroStation.latitude, lng: metroStation.longitude},
metroStation.metName,
map, metroStationIcon,
metroStation.metId,
metroStation.type,
metroStation.metNameKor);
});
7. 마이그레이션 효과
기대한 것과 나타난 효과가 거의 같았다.
초반에 Thymeleaf 문법이 익숙해지기까지 시간이 조금 걸린 것 외에는 매우 만족스러운 개선점이었다.
- HTML5 표준 기반 템플릿으로 프론트 작업이 쉬워짐
- 백엔드-프론트 간 책임 분리 강화
- 유지보수 및 확장성 증가 (특히 헤더, 푸터 등 레이아웃 부분에서 코드 작성 용이해짐)
- 더 깔끔한 템플릿 구조 → 학습 곡선은 있으나 이득이 큼
∴ 리팩토링 요약
JSP 다루는 법과 Thymeleaf 다루는 법을 둘 다 익혀서 좋았다.
Thymeleaf를 써 보니 JSP의 단점이 상대적으로 부각되었는데, 실무 레거시 프로젝트에서는 여전히 점유율이 꽤 있는 기술이라서 아무리 내가 BE 파트여도 잘 알고 있어야 할 것 같았다.
또한 추후 리액트나 다른 기술로 마이그레이션이 진행되면, JSP를 어떻게 바꿔야 할지 자신감도 붙고 좋았다.
| 항목 | JSP | Thymeleaf |
| 템플릿 문법 | JSTL | HTML5 + th: 태그 |
| 공통 레이아웃 | include: | layout: |
| 변수 표현 | ${} | th:text, [[...]] |
| 확장성/가독성 | 낮음 | 높음 |
'개인 프로젝트 리팩토링 > Seoulbara' 카테고리의 다른 글
| 지연 로딩(Lazy Loading), 프록시, 그리고 OSIV 설정 (0) | 2024.11.29 |
|---|---|
| AWS 배포: 책만큼 서버도 잘 파네 (3) | 2024.11.14 |
| MyBatis에서 JPA로: SQL문을 다 쓸 필요가 없었다니 (0) | 2024.11.13 |