시맨틱 마크업으로 SEO를 다시 잡았다
Article
이 블로그의 검색 노출이 영 시원치 않아서 원인을 파고들었다. 콘텐츠 문제이기 전에 마크업 문제였다. 퍼블리셔 출신이 이런 걸 방치했다는 게 좀 부끄러워서, 며칠에 걸쳐 싹 정리했다. 그 기록이다.
div 나열은 검색봇에게 백지다
처음 블로그를 빠르게 만들 때 화면만 보고 마크업을 짰다. 결과는 흔한 div 나열이었다.
<div class="header">
<div class="logo">DCODELAB</div>
<div class="menu">...</div>
</div>
<div class="list">
<div class="item">
<div class="title">포스트 제목</div>
<div class="date">2026-04-09</div>
</div>
</div>
사람 눈에는 멀쩡하다. CSS가 다 그려주니까. 하지만 검색봇 입장에서 이 문서는 위계가 없는 텍스트 덩어리다. 어디가 제목이고 어디가 날짜인지, 어떤 게 독립된 글인지 알 수가 없다. 이렇게 바꿨다.
<header>
<h1><a href="/" title="디코드랩">DCODELAB</a></h1>
<nav>...</nav>
</header>
<section>
<article>
<h3>포스트 제목</h3>
<time datetime="2026-04-09">2026. 04. 09</time>
</article>
</section>
교체 기준은 단순하다. 페이지 머리는 header, 메뉴는 nav, 독립 콘텐츠는 article, 이미지 래퍼는 figure, 날짜는 반드시 time에 datetime 속성. 그리드 목록은 ul > li를 유지했다. 태그 이름 자체가 문서의 목차 역할을 한다.
작업하면서 잡은 부수 버그도 있다. 로고가 그냥 div였던 것을 h1으로 감싸고, nav 안에 nav가 중첩된 구조를 폈다. 시맨틱 정리는 하다 보면 구조 자체의 어색함이 같이 드러난다.
JSON-LD로 문서의 정체를 선언한다
태그가 구조를 알려준다면, JSON-LD는 문서의 정체를 알려준다. 포스트 페이지에 이런 구조화 데이터를 넣었다.
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.description,
datePublished: post.createdAt,
dateModified: post.updatedAt,
author: { "@type": "Person", name: "DCODELAB" },
};
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>;
점검해 보니 포스트 페이지에만 넣어두고 나머지 페이지 다섯 곳이 비어 있었다. 목록 페이지는 CollectionPage, 소개는 AboutPage로 전부 채웠다. 리치 결과에 바로 반영되는 건 아니지만, 검색엔진이 페이지 성격을 추측하지 않게 만드는 게 핵심이다.
마지막으로 내부 링크 전부에 title 속성을 달았다. 앵커 텍스트가 로고 이미지거나 "더 보기"처럼 맥락 없는 경우, title이 그 링크의 의미를 보충한다.
퍼블리셔의 직업병이 무기가 된다
이번 작업을 하면서 새삼 느꼈다. 시맨틱 마크업은 퍼블리셔 시절엔 "지켜야 하는 규칙"이었는데, SEO 관점에서 보니 성능이자 기능이다. 검색봇은 JS를 실행하지 않고도 문서 구조를 읽어야 하고, 그때 남는 건 태그의 의미뿐이다.
React를 쓰면서 마크업 감각이 무뎌지는 개발자를 많이 봤다. div에 onClick 달면 버튼이 되는 세계니까. 하지만 렌더링 결과물은 결국 HTML 문서다. 문서를 문서답게 쓰는 것, 그게 이번 SEO 작업의 전부였다.