우리의 SEO 여정: SPA에서 Next.js까지 (완전 플레이북)

발행: (2025년 12월 16일 오후 07:59 GMT+9)
15 min read
원문: Dev.to

Source: Dev.to

Remember that moment?
당신은 방금 반짝이는 새로운 싱글 페이지 애플리케이션(SPA)을 런칭했습니다. 사용자 경험은 부드럽고, 클라이언트‑사이드 마법은 부인할 수 없으며, 기분도 좋습니다. 그런데 분석 도구를 확인해 보니요. 유기적 트래픽? 정적입니다. 아름답게 애니메이션된 데이터‑풍부한 콘텐츠가 검색 엔진에 거의 보이지 않는다는 사실을 깨달았습니다. 저도 그때 머리를 쥐어뜯으며 구글봇이 내 걸작을 차갑게 무시하는 이유를 고민했던 적이 있습니다.

현대 웹 개발에서 오래된 이야기입니다: React, Vue, Angular 같은 프레임워크로 만든 SPA는 인터랙티비티에 뛰어나지만, 기본적으로는 클라이언트‑사이드에서 JavaScript로 콘텐츠를 렌더링합니다. 최신 검색 엔진 크롤러가 JavaScript를 실행할 수는 있지만, 이것이 그들의 최애 활동은 아닙니다. 크롤링 예산을 더 많이 소모하게 하고, 렌더링 문제를 일으키며, 인덱싱을 크게 지연시킬 수 있습니다. 이 때문에 중요한 콘텐츠가 보이지 않아 SEO 병목 현상이 발생합니다.

우리 여정에서 이 사실을 깨달은 순간은 충격적이었습니다: 우리는 JavaScript가 실행되기 전에 크롤러에게 완전한 HTML을 제공해야 했습니다. 여기서 React 기반의 Next.js가 등장했습니다—단순한 프레임워크가 아니라 전략적인 SEO 무기였습니다. Next.js 덕분에 우리는 React의 장점을 활용하면서도 콘텐츠가 검색 엔진에 잘 발견되도록 할 수 있었습니다. 이것은 단순한 전환이 아니라 웹 존재 방식을 근본적으로 바꾸는 변화였습니다.

Next.js 렌더링 전략으로 SPA SEO 문제 해결

StrategyWhen to Use
Server‑Side Rendering (SSR)각 요청마다 서버에서 페이지를 렌더링합니다.
Static Site Generation (SSG)빌드 시에 페이지를 렌더링합니다.
Incremental Static Regeneration (ISR)SSG와 동일하지만 배포 후 백그라운드에서 페이지를 재생성할 수 있습니다.

실제로 어떻게 구현했는지와 우리가 얻은 인사이트를 살펴보겠습니다.

1️⃣ Server‑Side Rendering with getServerSideProps

동적으로 자주 변하거나 실시간 데이터가 필요한 경우 getServerSideProps를 주로 사용했습니다. 예를 들어 사용자 대시보드, 고도로 개인화된 콘텐츠, 뉴스 피드 등이 있습니다.

// pages/post/[id].tsx
import { GetServerSideProps } from 'next';

interface PostProps {
  post: {
    id: string;
    title: string;
    content: string;
  };
}

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { id } = context.params!; // Get ID from dynamic route
  const res = await fetch(`https://api.example.com/posts/${id}`);
  const post = await res.json();

  if (!post) {
    return { notFound: true };
  }

  return {
    props: { post },
  };
};

function PostPage({ post }: PostProps) {
  return (
    <>
      <h2>{post.title}</h2>
      <p>{post.content}</p>
    </>
  );
}

export default PostPage;

Insight: 강력하지만 SSR을 남용하지 마세요. 각 요청마다 서버‑사이드 렌더링이 발생해 지연 시간이 늘어날 수 있습니다. 진정으로 동적이고 자주 인증이 필요한 콘텐츠에만 사용하고, 공개되고 자주 접근되는 페이지는 다른 방식을 고려했습니다.

2️⃣ getStaticPropsgetStaticPaths를 이용한 정적 생성

이 방법은 블로그 포스트, 문서, 제품 페이지와 같이 콘텐츠가 많고 정적인 성격이 강한 페이지에서 진정한 SEO 혁신을 가져왔습니다. getStaticProps는 빌드 시 데이터를 가져와 HTML 파일을 생성하므로 CDN을 통해 초고속으로 제공할 수 있습니다. getStaticPaths는 동적 라우트에 필수적인데, Next.js에 미리 렌더링할 경로들을 알려줍니다.

// pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next';

interface BlogPostProps {
  post: {
    slug: string;
    title: string;
    body: string;
  };
}

export const getStaticPaths: GetStaticPaths = async () => {
  const res = await fetch('https://api.example.com/blog-posts-slugs'); // Get all slugs
  const slugs: string[] = await res.json();

  const paths = slugs.map((slug) => ({
    params: { slug },
  }));

  return { paths, fallback: 'blocking' }; // 'blocking' shows loading state, then renders
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const { slug } = params!;
  const res = await fetch(`https://api.example.com/blog/${slug}`);
  const post = await res.json();

  if (!post) {
    return { notFound: true };
  }

  return {
    props: { post },
    revalidate: 60, // ISR: regenerate no more than once every 60 seconds
  };
};

function BlogPostPage({ post }: BlogPostProps) {
  return (
    <>
      <h2>{post.title}</h2>
      <article>{post.body}</article>
    </>
  );
}

export default BlogPostPage;

Insight: getStaticPropsrevalidate 속성은 정말 금과 같습니다. 이 속성을 통해 **Incremental Static Regeneration (ISR)**을 사용할 수 있어, 전체 재배포 없이도 콘텐츠를 업데이트하면서 SSG의 성능 이점을 그대로 누릴 수 있습니다. 우리는 블로그 포스트와 제품 페이지에서 이 기능을 많이 활용했으며, 콘텐츠가 자주 업데이트되지만 실시간일 필요는 없는 경우에 특히 유용했습니다.

3️⃣ Next.js에 내장된 SEO 향상 도구

next/image를 사용한 최적화된 이미지

Core Web Vitals에서 가장 손쉽게 개선할 수 있는 항목 중 하나입니다. 이 컴포넌트는 이미지 크기와 포맷(예: WebP)을 자동으로 최적화하고, 반응형 이미지를 제공하며 기본적으로 lazy‑loading을 적용합니다. 페이지 속도 향상에 큰 도움이 되며, 이는 중요한 순위 요소입니다.

import Image from 'next/image';

function MyComponent() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={800}
      priority
    />
  );
}

Pitfall:

  • 외부 CDN에서 이미지를 가져올 때 next.config.js에 이미지 도메인을 설정하는 것을 잊는 경우.
  • widthheight를 설정하지 않으면 레이아웃 이동이 발생할 수 있습니다.

next/head를 사용한 헤드 관리

메타 태그, 제목, 설명, 정규화 URL, Open Graph 태그 등을 설정하는 데 필수적이며, 이는 페이지가 검색 결과 및 소셜 미리보기에서 어떻게 표시되는지에 중요한 역할을 합니다.

import Head from 'next/head';

function BlogPostPage({ post }: BlogPostProps) {
  return (
    <>
      <Head>
        <title>{post.title} – My Awesome Blog</title>
        <meta name="description" content={post.body.slice(0, 150)} />
        {/* Add more OG/Twitter tags as needed */}
      </Head>

      <h2>{post.title}</h2>
      <article>{post.body}</article>
    </>
  );
}

Tip: 메타 데이터를 DRY하게 유지하려면 title, description, image 등을 받아 적절한 태그를 삽입하는 재사용 가능한 컴포넌트를 추출하세요.

4️⃣ 추가적인 Next.js SEO 이점

기능SEO 이점
자동 sitemap.xml 생성 (플러그인 next-sitemap 등 사용)크롤러가 모든 페이지를 발견하도록 돕습니다.
내장 Link 컴포넌트와 프리패칭인지된 로드 시간을 줄여 사용자 경험 신호를 향상시킵니다.
라우트 기반 코드 스플리팅작은 JavaScript 번들 → 더 빠른 페이지 로드 → 향상된 Core Web Vitals.
robots.txt 지원 ( next-sitemap 또는 사용자 정의 파일 사용)크롤러에게 인덱싱할 내용을 안내합니다.

🎉 Takeaways

  1. 크롤러가 필요로 하는 시점에 필요한 것을 렌더링하세요.

    • 진정으로 동적이고 개인화된 콘텐츠에는 SSR을 사용합니다.
    • 가끔 업데이트가 필요한 정적에 가까운 페이지에는 SSG/ISR을 사용합니다.
  2. Next.js의 내장 최적화를 활용하세요.

    • next/image는 성능을 최우선으로 하는 이미지 처리에 사용합니다.
    • next/head는 견고한 메타 태그 관리를 위해 사용합니다.
    • 플러그인(next-sitemap, next-seo)을 이용해 SEO 기본 작업을 자동화합니다.
  3. 모니터링하고 반복 개선하세요.

    • Google Search Console에서 크롤링 오류를 확인합니다.
    • Lighthouse 또는 PageSpeed Insights로 Core Web Vitals를 검증합니다.
    • 콘텐츠 신선도 요구에 따라 revalidate 간격을 조정합니다.

SPA를 Next.js 기반 사이트로 교체하고 위 전략을 신중히 적용함으로써, 우리는 “정적 상태”를 지속적인 유기 트래픽 흐름으로 바꾸었습니다. SEO 병목 현상이 사라지고, 사이트는 탁월한 성능 검색 엔진 가시성을 동시에 누리게 되었습니다.


SPA에 Next.js 변신을 시도해 볼 준비가 되셨나요? 플레이북은 여러분 손에 있습니다—검색 가능한 무언가를 만들어 보세요!

# SEO Tips for Next.js

When your pages appear in search results and are shared on social media, proper meta tags are essential.

```tsx
import Head from 'next/head';

function MyPage({ title, description, imageUrl, url }) {
  return (
    <Head>
      <title>{title}</title>
      <meta name="description" content={description} />
      {/* Open Graph Tags for social media sharing */}
      <meta property="og:title" content={title} />
      <meta property="og:description" content={description} />
      <meta property="og:image" content={imageUrl} />
      <meta property="og:url" content={url} />
      {/* ... other meta tags */}
    </Head>
  );
}

## 일반적인 함정

- **`<Head>` 콘텐츠를 동적으로 만들지 않음**  
  하드코딩된 메타 설명이나 제목은 모든 페이지를 크롤러에게 동일하게 보이게 합니다. 각 페이지마다 고유하고 관련성 있는 메타데이터가 필요합니다.

- **클라이언트 측 데이터 가져오기에 과도하게 의존**  
  Next.js를 사용하더라도 데이터 가져오기에 `useEffect`에 의존하기 쉽습니다. SEO에 중요한 콘텐츠는 `getServerSideProps` 또는 `getStaticProps`를 사용해 컴포넌트가 렌더링되기 전에 데이터를 가져와야 합니다.

- **잘못된 `robots.txt` 및 사이트맵**  
  Next.js가 렌더링을 처리하지만, 크롤러를 안내할 견고한 `robots.txt`와 중요한 페이지를 나열하는 정확한 `sitemap.xml`이 필요합니다. `getStaticPaths` 출력으로 동적으로 사이트맵을 생성하면 효과적입니다.

- **Lighthouse / PageSpeed Insights 무시**  
  Next.js가 도구를 제공하지만, 적극적으로 최적화해야 합니다. Core Web Vitals를 정기적으로 확인하고, 큰 번들, 최적화되지 않은 폰트, 렌더링을 차단하는 CSS 등을 피해 서버 렌더링의 이점을 손상시키지 않도록 합니다.

- **`getStaticPaths`의 `fallback` 오해**  
  - `fallback: false` → 생성되지 않은 경로는 404 반환.  
  - `fallback: true` → 대체 버전이 제공된 뒤 페이지가 생성됨.  
  - `fallback: 'blocking'` → 페이지가 생성될 때까지 요청이 대기함.  
  각 모드마다 사용 사례가 있으며, 잘못 적용하면 UX와 SEO에 악영향을 줄 수 있습니다.

## 우리의 경험

순수 SPA에서 Next.js 기반 애플리케이션으로 전환한 것은 단순한 기술 리팩터링이 아니라 제품 전략의 근본적인 변화였습니다. 우리는 사용자뿐만 아니라 크롤러의 관점에서도 콘텐츠 전달을 고민하게 되었습니다. Next.js의 유연성 덕분에 각 콘텐츠에 맞는 적절한 렌더링 전략을 선택할 수 있었고, 그 결과:

- 유기적 검색 가시성의 큰 향상  
- 페이지 로드 속도 향상  
- 훨씬 더 만족스러운 엔지니어링 팀  

자신만의 SEO 여정을 시작한다면, **마라톤이지 스프린트가 아니라는 점**을 기억하세요. Next.js는 강력한 엔진을 제공하지만, 전략적인 구현, 지속적인 모니터링, 그리고 콘텐츠 요구에 대한 깊은 이해가 진정한 성공을 이끌어냅니다.

> 🚀 내 블로그에서 읽기
Back to Blog

관련 글

더 보기 »