블로그 SEO 태그 완성과 Thymeleaf 경고 정리
목차
블로그 글 SEO 메타태그 풀세트 적용하면서 Thymeleaf 3.1 deprecated 경고도 같이 정리한 작업이다.
배경 — article:* 왜 이제야
솔직히 말하면 og:type = article만 박아두고 article:* 계열 태그는 미뤄뒀었다. 작동은 되니까. 근데 Facebook 디버거나 LinkedIn 미리보기에서 날짜 정보가 안 뜨는 문제가 계속 신경 쓰였고, 검색 엔진이 콘텐츠의 시간 맥락을 얼마나 신뢰하느냐도 결국 이 태그들과 연결된다는 걸 다시 인지하게 됐다.
article:* 풀세트라는 게 별거 없어 보여도, 막상 챙겨야 할 항목이 꽤 된다.
| 태그 | 역할 | 비고 |
|---|---|---|
article:published_time |
최초 발행 시각 | ISO 8601 형식 권장 |
article:modified_time |
최종 수정 시각 | 업데이트 시 갱신 필수 |
article:author |
저자 프로필 URL | 프로필 페이지 링크 |
article:section |
카테고리/섹션명 | 단순 문자열 |
article:tag |
태그 (복수 가능) | 반복 메타태그로 처리 |
이 중에서 article:published_time과 article:modified_time은 특히 구글 리치 결과에서도 영향을 주기 때문에, 아예 빠져 있으면 크롤러가 "이 글이 언제 작성됐는지 모르겠다"는 상태가 된다. 콘텐츠 신선도를 평가할 수 없으니 불리할 수밖에 없다.
작업 구조 — Controller Advice + Fragment
이번에 건드린 파일은 두 개다. AdsControllerAdvice.java와 seo.html 프래그먼트.
구조 자체는 이미 잡혀 있었다. Controller Advice에서 SEO용 모델 어트리뷰트를 전역으로 내리고, Thymeleaf fragment에서 그걸 받아서 <meta> 태그로 렌더링하는 패턴. 이 방식의 장점은 각 컨트롤러에서 일일이 model.addAttribute()를 반복하지 않아도 된다는 거다. 공통 SEO 맥락은 Advice에서 한 번에 처리하고, 페이지별 오버라이드가 필요하면 개별 컨트롤러에서 덮어쓰는 구조.
article:tag는 값이 복수일 수 있어서 이 부분만 약간 신경 썼다. Thymeleaf에서 리스트를 반복하면서 동일한 property를 가진 <meta> 태그를 여러 개 찍는 방식이 OGP 스펙상 맞는 방법이다.
<!-- seo.html fragment 패턴 -->
<th:block th:each="tag : ${article.tags}">
<meta property="article:tag" th:content="${tag}" />
</th:block>
단순해 보이지만 th:each 루프 안에서 null-safe 처리를 안 하면 태그 목록이 없는 글에서 에러가 난다. ${article.tags} 자체가 null인 케이스와 빈 리스트인 케이스 둘 다 방어해야 한다.
Thymeleaf 3.1 deprecated 정리
이게 사실 작업하다가 발견한 곁가지였는데, 그냥 넘기기엔 찜찜해서 같이 처리했다.
Thymeleaf 3.1에서 expression utility object 일부가 deprecated 처리됐다. 대표적으로 #request, #response, #session, #servletContext 같은 web context 오브젝트들을 Thymeleaf expression 안에서 직접 접근하는 방식이 제한되기 시작했다. 보안 이슈와 관련된 결정이라 당연히 따라가야 한다.
기존 코드가 이런 식으로 되어 있었다면:
<!-- deprecated — 3.1 이후 제한됨 -->
<meta th:content="${#request.requestURL}" ... />
이걸 Controller Advice에서 미리 값을 계산해서 모델에 담아주는 방식으로 바꾸는 게 정석이다.
// AdsControllerAdvice.java
@ModelAttribute("canonicalUrl")
public String canonicalUrl(HttpServletRequest request) {
return request.getRequestURL().toString();
}
<!-- seo.html — 모델에서 가져오기 -->
<meta property="og:url" th:content="${canonicalUrl}" />
이 패턴으로 바꾸면 Thymeleaf expression에서 직접 request 오브젝트를 건드리지 않아도 되고, 테스트할 때도 훨씬 편하다. 모델 어트리뷰트만 mocking하면 되니까.
회고
AdsControllerAdvice라는 이름이 좀 의아할 수 있는데, 광고 관련 공통 처리를 담당하던 Advice가 SEO 관련 공통 모델도 함께 담당하게 된 케이스다. 팀 상황에서 이런 일이 자주 생긴다 — "일단 여기 넣자"가 쌓이면서 Advice 하나가 비대해지는 것. 이번 작업 자체는 잘 마무리됐지만, 언젠가 SeoControllerAdvice를 별도로 분리하는 게 맞겠다는 숙제는 남겼다.
SEO 작업은 "당장 안 해도 서비스는 돌아간다"는 이유로 우선순위에서 밀리기 쉬운데, 그래서 더 의식적으로 챙겨야 한다.
다음은 structured data(JSON-LD) 쪽 정리 예정.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.