SEO 엔지니어링 및 트러블슈팅
티스토리 시절보다 못한 유입량의 원인을 분석하다 발견한 치명적인 문제와, 이를 해결하기 위한 기술적 SEO 적용기.
이 블로그와 내 서비스를 소개할 홈페이지를 만든 지 이제 한 달 하고도 열흘이 지났다.
공식 소개 페이지는 뭐… 컨텐츠가 없으니까 그렇다 쳐도… 블로그에는 꽤나 글을 많이 쓰고 있다고 생각했는데, 유입이 내가 티스토리로 신입 시절 한참 수준 떨어지는 javascript 글을 쓴 것보다 적다.
(사실상 거의 제로에 가깝다.)
왜일까?
분명 처음 블로그를 셋팅할 때 SEO 설정은 빠진 거 없이 한 거 같은데…
도대체 뭐가 문제여서 구글이 상위 노출을 안 시켜주는 지 모르겠어서, 오늘 주말이기도 하고 시간이 조금 남길래 분석을 좀 해봤다.
결론은 뭐 늘 그렇듯, 내가 만든 구멍이 있어서 그랬던거지만 시작한 김에 오늘은 이 SEO 에 대해서 좀 다뤄볼까 한다.
SEO의 본질: 도서관 사서와 프로토콜
로그를 까보기 전에, SEO(Search Engine Optimization)가 정확히 기술적으로 무엇을 요구하는지 정리를 한 번 해보자.
쉽게 말해 SEO는 내 사이트를 검색 엔진이라는 깐깐한 도서관 사서에게 소개하는 프로토콜이다.
구글이라는 사서는 매일 수억 권의 책(웹사이트)을 분류한다.
내 책표지(Title)가 백지이거나 목차(Sitemap)가 없으면, 사서는 내용을 읽어보지도 않고 구석진 창고에 처박아 둔다.
엔지니어 관점에서 SEO는 다음 세 가지 단계를 통과해야 한다.
- Crawlability (수집 가능성): 봇이 내 서버에 들어와서 링크를 타고 돌아다닐 수 있는가? (
robots.txt,sitemap.xml) - Indexability (색인 가능성): 수집한 페이지가 빈 껍데기가 아니라 유의미한 HTML 텍스트를 담고 있는가? (SPA의 취약점)
- Rankability (순위 요소): 사용자가 던진 쿼리(Query)와 내 데이터가 일치하는가? (Title, Keywords, JSON-LD)
단순히 서버가 200 OK를 뱉는다고 끝이 아니다. 사서가 이해할 수 있는 “분류 기호(메타 데이터)“를 명확히 달아줘야 한다.
아키텍처 분석: SPA vs SSG
현재 프로젝트는 두 가지 구조가 혼재되어 있다.
메인 랜딩 페이지는 React 기반이고, 블로그는 Astro 기반이다.
SPA (Main Page)의 한계
메인 페이지(web/index.html)는 React다.
naver-site-verification 태그도 넣었고 공유 시 썸네일(og:image)도 잘 나오지만, 근본적으로 SPA는 초기 HTML이 비어있다.
크롤러가 JS를 실행해주지 않으면 빈 화면만 본다.
react-helmet-async로 동적 제어를 한다 해도, 순수 HTML을 선호하는 봇에게는 불리하다.
/blog는 SSG인데?
반면 블로그는 Astro를 사용해 빌드 타임에 HTML을 생성하는 SSG 방식이다. 이론상 SEO 점수가 높아야 정상이다.
BlogPost.astro에서 메타 태그도 동적으로 주입하고 있고, 구조화된 데이터(JSON-LD)도 스키마에 맞춰 들어가 있다.
그런데 왜 유입이 없는가? 여기서 치명적인 실수를 발견했다.
결정적 원인: 데드 링크(Dead Link) 대참사
가장 큰 문제는 태그는 있는데 페이지가 없다는 것이었다.
[현상] 404 Not Found의 향연
블로그 글 하단에 #Sentry, #Essay 같은 태그를 열심히 달았다. 렌더링된 HTML 소스를 보면 앵커 태그도 정상적으로 박혀있다.
<a href="/blog/tags/Sentry">#Sentry</a>
하지만 이 링크를 클릭하면 404 Not Found가 뜨면서 다시 메인 목차 페이지로 리다이렉트가 된다.
/blog/tags/Sentry라는 페이지를 생성하는 파일 자체가 없었기 때문이다.
사실 이 문제를 알고는 있었지만, 태그를 클릭했을 때 태그별로 게시글을 보여주는 걸 구현하기 귀찮아서 미뤄두고 있었다.
하지만 생각해보니, 크롤러가 이 태그를 타고 이동했을 때 404를 마주했다면 위에서 언급한 Rankability이나 색인 가능성에 큰 문제가 생기는 것이었다.
풀어보면 이렇다.
[분석] 봇(Bot)의 관점
구글 봇 입장에서 내 사이트는 이런 상태였다.
“이 사이트는 들어오자마자 링크가 죄다 깨져있네. 관리가 안 되는 사이트군.”
롱테일 키워드(Long-tail Keyword)를 노리고 태그를 달았지만, 그 키워드를 눌렀을 때 보여줄 목록 페이지가 없으니 검색 엔진은 해당 태그를 ‘없는 셈’ 치거나, 사이트 전체의 신뢰도(Rankability)를 깎아먹고 있었던 것이다.
[해결] getStaticPaths 구현
부랴부랴 web/blog/src/pages/tags/[tag].astro 파일을 생성했다.
Astro의 getStaticPaths를 사용해 빌드 타임에 모든 태그별 페이지를 미리 만들어두도록 수정했다.
// web/blog/src/pages/tags/[tag].astro
export async function getStaticPaths() {
const allPosts = await getCollection('blog');
// 중복 제거된 유니크한 태그 목록 생성
const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
return uniqueTags.map((tag) => {
// 해당 태그를 가진 글만 필터링
const filteredPosts = allPosts.filter((post) => post.data.tags.includes(tag));
return {
params: { tag },
props: { posts: filteredPosts },
};
});
}
이제 태그 링크가 실제로 살아있는 페이지로 연결된다.
검색 엔진이 내부 링크를 타고 사이트 구석구석을 돌아다닐 수 있게 길이 뚫렸다.
SEO 기술적 보완 (Code Level)
구멍을 메웠으니, 이제 검색 엔진이 좋아할 만한 데이터 구조를 확실하게 잡아둔다.
4-1. 동적 메타 태그 주입
BlogPost.astro에서 Frontmatter로 받은 데이터를 그대로 메타 태그에 꽂아 넣는다.
const { title, description, tags } = Astro.props;
const keywords = tags && tags.length > 0 ? tags.join(', ') : '개발, 블로그, 기술';
<meta name="keywords" content={keywords} />
<meta name="description" content={description} />
4-2. JSON-LD (구조화된 데이터)
단순 텍스트 파싱에 의존하지 않고, 기계가 읽기 쉬운 JSON 포맷으로 데이터를 떠먹여 준다. 구글이 리치 스니펫(Rich Snippet)을 띄워주길 기대하며 schema.org 표준을 맞췄다.
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": title,
"description": description,
"author": {
"@type": "Person",
"name": author
},
// ...
};
4-3. Robots.txt 및 Sitemap
public/robots.txt를 통해 크롤러의 경로를 제어한다.
특히 /blog 경로는 적극적으로 허용하고, 사이트맵 위치를 명시하여 색인 생성을 유도했다.
User-agent: *
Allow: /
Allow: /blog
Sitemap: https://slowflowsoft.com/sitemap-index.xml
결론 및 모니터링 계획
SEO는 “설정하고 끝”이 아니라 “농사”와 같다.
하지만 기술적 결함(404 링크)이 있다면 밭을 아무리 갈아도 싹이 트지 않는다.
이번 트러블슈팅으로 기본적인 배관 공사는 끝났다.
앞으로의 할 일은 다음과 같다.
-
Search Console 확인: 사이트맵 제출 후 실제 색인 생성 추이를 모니터링한다.
-
태그 전략: 글 작성 시 연관 태그를 3~5개 필수로 입력하여 태그 페이지(
/tags/[tag])가 활성화되도록 한다. -
콘텐츠: 이제 봇이 길을 잃지 않으니, 양질의 글을 쌓는 데 집중한다.
마침.
다른 글 보기
RHCSA 4일차: AI 강사를 해고하고, 다시 AI를 고용했다
할루시네이션에 지쳐 원서를 샀다. 그리고 Gemini를 나만의 1타 강사로 개조했다.
RHCSA 3일차
내용이 적은건 기분탓..
Prisma, 넌 누구니?
MyBatis의 노가다와 TypeORM의 배신을 넘어, Prisma는 얼마나 합리적인가
RHCSA 2일차
유연한 스토리지 관리의 미학.