개발 slecs

Thymeleaf JSON-LD 이스케이프 오류로 깨진 구조화 데이터 수정

목차

Thymeleaf 템플릿에서 JSON-LD 구조화 데이터를 잘못 렌더링하고 있다는 걸 발견했다.


배경: JSON-LD와 Thymeleaf, 왜 충돌이 생기나

코스 상세 페이지에는 검색엔진 최적화(SEO) 목적으로 <script type="application/ld+json"> 블록이 들어가 있었다. 구조화 데이터를 넣어두면 구글 같은 검색엔진이 페이지 콘텐츠를 더 잘 이해하고, 검색 결과에 리치 스니펫으로 노출될 가능성이 높아진다. 강의 플랫폼 특성상 Course 스키마를 마크업해두는 건 꽤 중요한 작업이다.

문제는 Thymeleaf의 th:inline="javascript" 혹은 th:inline="text" 방식으로 JSON-LD 블록 안에 동적 데이터를 주입하려 했을 때 발생했다. th:inline[[...]] 문법으로 값을 바인딩하는데, 이 방식이 JSON 문자열 안에서 의도치 않은 이스케이프나 포맷 변환을 일으키기 쉽다.

예를 들어 강의 제목에 &, <, > 같은 특수문자가 들어가면 Thymeleaf가 HTML 엔터티로 인코딩해버린다. JSON-LD는 HTML 문서 안에 있지만 결국 JSON 파서가 읽는 텍스트이기 때문에, &amp; 같은 엔터티가 그대로 들어가면 파싱 결과가 달라지거나 Google의 구조화 데이터 테스트 도구에서 오류로 잡힌다.

<!-- 문제가 됐던 패턴 (th:inline 방식) -->
<script type="application/ld+json" th:inline="javascript">
{
  "name": [[${course.title}]],
  "description": [[${course.description}]]
}
</script>

이 상태에서 course.title"Java & Spring 완성"이면 렌더링 결과가 "Java &amp; Spring 완성"으로 나올 수 있다. JSON으로는 유효하지 않거나 의미가 달라지는 값이 심어지는 것.


변경 내용: th:utext로 교체

해결 방법은 간단했다. th:inline을 제거하고, JSON-LD 전체를 백엔드에서 미리 직렬화한 JSON 문자열로 만들어 th:utext로 주입하는 방식으로 바꿨다.

<!-- 변경 후 (th:utext 방식) -->
<script type="application/ld+json" th:utext="${courseJsonLd}"></script>

th:utext는 "unescaped text"다. HTML 이스케이프를 하지 않고 값을 그대로 출력한다. 백엔드에서 ObjectMapperGson으로 직렬화한 JSON 문자열을 모델에 담아서 넘기면, 템플릿 레이어에서 이스케이프 걱정 없이 그대로 박힌다.

방식 이스케이프 동작 JSON-LD 적합성 유지보수
th:inline="javascript" 자동 이스케이프 (HTML 엔터티 변환 가능) 특수문자 포함 시 깨질 수 있음 템플릿 안에서 JSON 구조 직접 관리
th:utext (JSON 문자열 주입) 이스케이프 없음 직렬화된 JSON 그대로 출력 백엔드에서 구조 관리, 테스트 용이

th:utext를 쓸 때 주의할 점은 XSS다. 사용자 입력이 직접 들어가는 필드라면 반드시 서버에서 sanitize를 거쳐야 한다. 이번 경우는 DB에서 가져온 강의 데이터를 백엔드에서 직렬화하는 구조라 입력 경로가 통제되고 있었지만, 팀 리뷰에서도 이 부분은 짚고 넘어갔다. th:utext는 "믿을 수 있는 소스에서 온 데이터"에만 쓴다는 원칙을 명시적으로 코드 리뷰 코멘트로 남겨뒀다.


회고

course-detail.html 하나짜리 변경이었지만, 이 수정이 의미 있는 이유는 SEO 영향 범위 때문이다. JSON-LD가 깨진 상태로 크롤링되면 구조화 데이터 자체가 무효화되고, 검색 결과에서 리치 결과 노출 기회를 날리게 된다. 사용자에게 당장 에러가 보이는 버그가 아니라서 놓치기 쉬운 종류의 문제다.

이런 류의 이슈는 보통 "SEO 관련이라 QA에서 잡기 어렵다"는 특성이 있다. 구글 서치 콘솔이나 Rich Results Test 같은 외부 도구를 개발 프로세스 안에 어느 정도 넣어두지 않으면 배포 후에야 발견하게 된다. 앞으로 구조화 데이터 관련 템플릿 변경이 있을 때는 해당 도구 검증을 체크리스트에 포함시키는 쪽으로 팀 컨벤션을 잡을 생각이다.

Thymeleaf에서 JSON을 다뤄야 할 때는 th:inline 방식보다 백엔드에서 직렬화 → th:utext 주입 패턴이 훨씬 예측 가능하고 안전하다. 이 패턴을 한번 팀 내 정리 문서로 남겨두면 비슷한 실수가 반복되는 걸 줄일 수 있을 것 같다.

끝.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.