SEO 엔지니어링 및 트러블슈팅

티스토리 시절보다 못한 유입량의 원인을 분석하다 발견한 치명적인 문제와, 이를 해결하기 위한 기술적 SEO 적용기.

Jun Noh

이 블로그와 내 서비스를 소개할 홈페이지를 만든 지 이제 한 달 하고도 열흘이 지났다.

공식 소개 페이지는 뭐… 컨텐츠가 없으니까 그렇다 쳐도… 블로그에는 꽤나 글을 많이 쓰고 있다고 생각했는데, 유입이 내가 티스토리로 신입 시절 한참 수준 떨어지는 javascript 글을 쓴 것보다 적다.

(사실상 거의 제로에 가깝다.)

왜일까?

분명 처음 블로그를 셋팅할 때 SEO 설정은 빠진 거 없이 한 거 같은데…

도대체 뭐가 문제여서 구글이 상위 노출을 안 시켜주는 지 모르겠어서, 오늘 주말이기도 하고 시간이 조금 남길래 분석을 좀 해봤다.

결론은 뭐 늘 그렇듯, 내가 만든 구멍이 있어서 그랬던거지만 시작한 김에 오늘은 이 SEO 에 대해서 좀 다뤄볼까 한다.

SEO의 본질: 도서관 사서와 프로토콜

로그를 까보기 전에, SEO(Search Engine Optimization)가 정확히 기술적으로 무엇을 요구하는지 정리를 한 번 해보자.

쉽게 말해 SEO는 내 사이트를 검색 엔진이라는 깐깐한 도서관 사서에게 소개하는 프로토콜이다.

구글이라는 사서는 매일 수억 권의 책(웹사이트)을 분류한다.

내 책표지(Title)가 백지이거나 목차(Sitemap)가 없으면, 사서는 내용을 읽어보지도 않고 구석진 창고에 처박아 둔다.

엔지니어 관점에서 SEO는 다음 세 가지 단계를 통과해야 한다.

  1. Crawlability (수집 가능성): 봇이 내 서버에 들어와서 링크를 타고 돌아다닐 수 있는가? (robots.txt, sitemap.xml)
  2. Indexability (색인 가능성): 수집한 페이지가 빈 껍데기가 아니라 유의미한 HTML 텍스트를 담고 있는가? (SPA의 취약점)
  3. 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)도 스키마에 맞춰 들어가 있다.

그런데 왜 유입이 없는가? 여기서 치명적인 실수를 발견했다.

가장 큰 문제는 태그는 있는데 페이지가 없다는 것이었다.

[현상] 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 링크)이 있다면 밭을 아무리 갈아도 싹이 트지 않는다.

이번 트러블슈팅으로 기본적인 배관 공사는 끝났다.

앞으로의 할 일은 다음과 같다.

  1. Search Console 확인: 사이트맵 제출 후 실제 색인 생성 추이를 모니터링한다.

  2. 태그 전략: 글 작성 시 연관 태그를 3~5개 필수로 입력하여 태그 페이지(/tags/[tag])가 활성화되도록 한다.

  3. 콘텐츠: 이제 봇이 길을 잃지 않으니, 양질의 글을 쌓는 데 집중한다.

마침.

다른 글 보기