mogee.
<< BACK
#SEO#React#OG태그#JSON-LD#react-helmet-async#mogee

mogee.org SEO 최적화 작업기 — react-helmet-async, OG 태그, JSON-LD

DATE: 2026년 3월 23일TIME: 7분 읽기VIEWS: 7

mogee.org SEO 최적화 작업기

React SPA로 만든 블로그/포트폴리오 사이트를 운영하면서 SEO를 제대로 챙긴 적이 없었다. 어느 날 문득 생각이 들었다.

"내 블로그 글을 카카오톡에 공유하면 미리보기가 어떻게 보이지?"

링크를 붙여넣는 순간, 아무것도 없었다. 제목도, 설명도, 이미지도. 그냥 URL 하나만 덩그러니.

그래서 이번에 SEO를 제대로 정비했다.


진단 먼저

작업 전에 현황을 파악했다.

항목상태
기본 title/description✅ 있음
robots.txt✅ 있음
OG / Twitter Card 태그❌ 없음
sitemap.xml❌ 없음
페이지별 동적 meta 태그❌ 없음
JSON-LD 구조화 데이터❌ 없음
canonical 태그❌ 없음

가장 큰 문제는 모든 페이지가 동일한 title과 description을 공유한다는 점이었다. 블로그 글마다 고유한 제목이 있는데, 구글 검색 결과에는 항상 "Mogee Development - Flutter App Developer"만 나오는 것이다.


Next.js로 마이그레이션해야 할까?

SPA SEO 얘기가 나오면 항상 "SSR로 전환해야 한다"는 말이 따라온다.

하지만 2024년 기준 Googlebot은 최신 Chrome 엔진으로 JS를 실행한다. 순수 React CSR도 크롤링이 된다. 마이그레이션 비용 대비 효과가 개인 포트폴리오/블로그에는 크지 않다고 판단했다.

대신 빠르게 체감 효과가 큰 것들을 먼저 처리하기로 했다.


react-helmet-async 적용

npm install react-helmet-async

App.tsxHelmetProvider를 감싸고, 재사용 가능한 SEOHead 컴포넌트를 만들었다.

// src/components/SEOHead.tsx
import { Helmet } from 'react-helmet-async';

interface SEOHeadProps {
  title?: string;
  description?: string;
  ogType?: 'website' | 'article';
  canonicalPath?: string;
  publishedAt?: string;
  tags?: string[];
  jsonLd?: object;
}

const SEOHead: React.FC<SEOHeadProps> = ({ title, description, ... }) => {
  const pageTitle = title
    ? `${title} | Mogee Development`
    : `Mogee Development - Flutter App Developer`;

  return (
    <Helmet>
      <title>{pageTitle}</title>
      <meta name="description" content={description} />
      <link rel="canonical" href={canonicalUrl} />
      <meta property="og:title" content={pageTitle} />
      <meta property="og:description" content={description} />
      <meta property="og:image" content={ogImage} />
      <meta name="twitter:card" content="summary_large_image" />
      {/* ... */}
    </Helmet>
  );
};

블로그 포스트 페이지가 핵심

가장 중요한 변경은 PostDetail.tsx다. Firestore에서 불러온 포스트 데이터로 동적으로 SEO 태그를 생성한다.

// 마크다운 기호 제거 후 160자 description 생성
const seoDescription = (() => {
  const raw = displayContent.replace(/[#*`>-_[\]()]/g, '').trim();
  return raw.length > 160 ? raw.slice(0, 157) + '...' : raw;
})();

// JSON-LD BlogPosting 스키마
const postJsonLd = {
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: displayTitle,
  description: seoDescription,
  url: `https://mogee.org/post/${post.id}`,
  datePublished: publishedIso,
  author: { '@type': 'Person', name: 'Mogee Development' },
  keywords: displayTags?.join(', '),
};

return (
  <>
    <SEOHead
      title={displayTitle}
      description={seoDescription}
      ogType="article"
      canonicalPath={`/post/${post.id}`}
      publishedAt={publishedIso}
      tags={displayTags}
      jsonLd={postJsonLd}
    />
    {/* 포스트 본문 */}
  </>
);

이제 각 블로그 글마다 고유한 제목, 설명, 아티클 메타 태그, JSON-LD가 자동으로 설정된다.


sitemap.xml 추가

public/sitemap.xml에 정적 라우트를 추가했다. 블로그 포스트 URL은 Firestore 기반이라 빌드 시점에 알 수 없어서 정적 페이지만 포함했다.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://mogee.org/</loc>
    <changefreq>daily</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://mogee.org/portfolio</loc>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

결과

항목
OG / Twitter Card
페이지별 동적 title
페이지별 description
JSON-LD 구조화 데이터
sitemap.xml
canonical 태그
이미지 lazy loading

카카오톡에 블로그 포스트 링크를 붙여넣으면 이제 제목과 설명이 미리보기로 나온다. 작은 작업이지만 체감 차이가 크다.


한 줄 정리

React SPA도 react-helmet-async 하나면 OG 태그, JSON-LD, 동적 title을 모두 해결할 수 있다. Next.js가 필요한 건 그다음 이야기다.

이 글 공유하기

[X] X에 공유

// SPONSORED

[>]댓글

아직 댓글이 없어요. 첫 댓글을 남겨보세요!