Frontend

Next.js 프로젝트의 SEO 최적화 완벽 가이드

실제 프로젝트에 적용한 Next.js SEO 최적화 전략과 구현 방법을 상세히 설명합니다. 메타데이터, 구조화된 데이터, 다국어 지원 등 핵심 요소들을 코드와 함께 알아보세요.

Next.js 프로젝트의 SEO 최적화 완벽 가이드

소개

검색 엔진 최적화(SEO)는 웹사이트의 가시성과 도달성을 높이는 핵심 요소입니다. 우리 팀이 itemSCV 사이트를 Next.js로 구축하면서 적용한 SEO 전략과, 메타데이터·구조화 데이터·다국어 대응을 어떻게 구현했는지 정리해 두었습니다. 아래에서 코드와 함께 단계별로 살펴봅니다.

1. 서버 사이드 렌더링 (SSR) 최적화

서버 컴포넌트 활용

Next.js 13부터 도입된 React Server Components를 활용하여 초기 페이지 로딩 속도를 개선하고 검색 엔진 크롤링을 최적화했습니다.

tsx
1// app/[lang]/blog/[slug]/page.tsx
2export default async function BlogPostPage({ params }: BlogPostPageProps) {
3  const post = await getPostBySlug(params.slug);
4  
5  if (!post) {
6    notFound();
7  }
8
9  return (
10    <article className="prose prose-invert max-w-none">
11      <h1>{post.title}</h1>
12      <MarkdownContent content={post.content} />
13    </article>
14  );
15}
16
17// 정적 경로 생성
18export async function generateStaticParams() {
19  return blogPosts.map((post) => ({
20    slug: post.slug,
21  }));
22}
23
24// 정적 생성 강제
25export const dynamic = 'force-static';
26export const dynamicParams = false;

2. 메타데이터 최적화

동적 메타데이터 생성

각 페이지별로 최적화된 메타데이터를 생성하여 검색 결과와 소셜 미디어 공유 시 풍부한 정보를 제공합니다.

tsx
1// app/[lang]/blog/[slug]/page.tsx
2export async function generateMetadata({ params }: BlogPostPageProps): Promise<Metadata> {
3  const post = await getPostBySlug(params.slug);
4  if (!post) return {};
5
6  const url = `https://itemscv.com/${params.lang}/blog/${post.slug}`;
7  const ogImage = post.coverImage?.url || '/images/og-image.png';
8
9  return {
10    title: post.title,
11    description: post.description,
12    openGraph: {
13      title: post.title,
14      description: post.description,
15      url,
16      siteName: 'itemSCV Blog',
17      images: [
18        {
19          url: ogImage,
20          width: post.coverImage?.width || 1200,
21          height: post.coverImage?.height || 630,
22          alt: post.coverImage?.alt || post.title,
23        },
24      ],
25      locale: params.lang === 'ko' ? 'ko_KR' : 'en_US',
26      type: 'article',
27    },
28    twitter: {
29      card: 'summary_large_image',
30      title: post.title,
31      description: post.description,
32      images: [ogImage],
33    },
34    alternates: {
35      canonical: url,
36      languages: {
37        ko: `https://itemscv.com/ko/blog/${post.slug}`,
38        en: `https://itemscv.com/en/blog/${post.slug}`,
39      },
40    },
41  };
42}

3. 구조화된 데이터 구현

Schema.org 마크업

검색 결과에서 더 풍부한 정보를 제공하기 위해 구조화된 데이터를 구현했습니다.

tsx
1// 블로그 포스트 스키마
2const structuredData = {
3  '@context': 'https://schema.org',
4  '@type': 'BlogPosting',
5  headline: post.title,
6  description: post.description,
7  datePublished: post.date,
8  dateModified: post.lastModified || post.date,
9  image: post.coverImage ? {
10    '@type': 'ImageObject',
11    url: post.coverImage.url,
12    width: post.coverImage.width,
13    height: post.coverImage.height,
14    caption: post.coverImage.alt,
15  } : undefined,
16  author: {
17    '@type': 'Person',
18    name: post.author.name,
19    jobTitle: post.author.role,
20    image: post.author.image,
21  },
22  publisher: {
23    '@type': 'Organization',
24    name: 'itemSCV',
25    url: 'https://itemscv.com',
26    logo: {
27      '@type': 'ImageObject',
28      url: 'https://itemscv.com/images/logo.png',
29    },
30  },
31  mainEntityOfPage: {
32    '@type': 'WebPage',
33    '@id': `https://itemscv.com/${params.lang}/blog/${post.slug}`,
34  },
35};
36
37// JSX에서 구조화된 데이터 적용
38<script
39  type="application/ld+json"
40  dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
41/>

4. 다국어 지원 최적화

미들웨어를 통한 언어 감지

사용자의 선호 언어를 감지하고 적절한 언어 버전으로 리다이렉트하는 미들웨어를 구현했습니다.

tsx
1// middleware.ts
2export function middleware(request: NextRequest) {
3  const pathname = request.nextUrl.pathname;
4  
5  // 언어 코드가 없는 경우 처리
6  if (pathnameIsMissingLocale) {
7    const locale = getLocale(request);
8    return NextResponse.redirect(
9      new URL(`/${locale}${pathname}`, request.url)
10    );
11  }
12
13  const response = NextResponse.next();
14
15  // 보안 헤더 추가
16  response.headers.set('X-Frame-Options', 'DENY');
17  response.headers.set('X-Content-Type-Options', 'nosniff');
18  
19  // 캐시 전략 설정
20  if (pathname.includes('/blog/')) {
21    response.headers.set(
22      'Cache-Control',
23      `public, max-age=${day}, stale-while-revalidate=${day * 7}`
24    );
25  }
26
27  return response;
28}

5. 성능 최적화

이미지 최적화

Next.js의 Image 컴포넌트를 활용하여 이미지 최적화를 구현했습니다.

tsx
1// components/PostCard.tsx
2import Image from 'next/image';
3
4export default function PostCard({ post }: PostCardProps) {
5  return (
6    <article className="relative">
7      {post.coverImage && (
8        <Image
9          src={post.coverImage.url}
10          alt={post.coverImage.alt}
11          width={post.coverImage.width}
12          height={post.coverImage.height}
13          className="rounded-lg"
14          priority={true}
15          sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
16        />
17      )}
18      {/* ... */}
19    </article>
20  );
21}

6. 시맨틱 HTML과 웹 접근성

ARIA 레이블과 랜드마크

시맨틱 HTML과 ARIA 속성을 활용하여 웹 접근성을 개선했습니다.

tsx
1<div className="blog-layout">
2  <nav aria-label="breadcrumb">
3    <ol className="flex items-center gap-2">
4      <li>
5        <Link href={'/blog'}>Blog</Link>
6      </li>
7      <li aria-hidden="true"></li>
8      <li>{post.category}</li>
9    </ol>
10  </nav>
11
12  <main id="main-content" tabIndex={-1}>
13    <article 
14      className="blog-post"
15      itemScope 
16      itemType="http://schema.org/BlogPosting"
17    >
18      <header>
19        <h1 id="post-title" itemProp="headline">
20          {post.title}
21        </h1>
22      </header>
23      
24      <div 
25        className="author-info"
26        itemProp="author" 
27        itemScope 
28        itemType="http://schema.org/Person"
29      >
30        {/* ... */}
31      </div>
32    </article>
33  </main>
34</div>

7. 기술적 SEO 요소

사이트맵 생성

동적으로 사이트맵을 생성하여 검색 엔진의 크롤링을 돕습니다.

tsx
1// app/sitemap.ts
2import { MetadataRoute } from 'next';
3import { blogPosts } from '@/lib/data/blog-posts';
4
5export default function sitemap(): MetadataRoute.Sitemap {
6  const baseUrl = 'https://itemscv.com';
7  
8  const blogPostsUrls = blogPosts
9    .filter(post => post.status === 'published')
10    .map(post => ({
11      url: `${baseUrl}/blog/${post.slug}`,
12      lastModified: post.lastModified || post.date,
13      changeFrequency: 'monthly',
14      priority: 0.7,
15    }));
16
17  return [
18    {
19      url: baseUrl,
20      lastModified: new Date(),
21      changeFrequency: 'weekly',
22      priority: 1,
23    },
24    ...blogPostsUrls,
25  ];
26}

RSS 피드 제공

블로그 포스트의 RSS 피드를 제공하여 구독자들에게 업데이트 정보를 전달합니다.

tsx
1// app/feed.xml/route.ts
2export async function GET() {
3  const publishedPosts = blogPosts
4    .filter(post => post.status === 'published')
5    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
6
7  const rss = `<?xml version="1.0" encoding="UTF-8" ?>
8    <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
9      <channel>
10        <title>itemSCV Tech Blog</title>
11        <link>https://itemscv.com/blog</link>
12        <description>itemSCV 엔지니어들이 공유하는 기술 경험과 인사이트</description>
13        <language>ko</language>
14        <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
15        ${publishedPosts.map(post => `
16          <item>
17            <title><![CDATA[${post.title}]]></title>
18            <link>https://itemscv.com/blog/${post.slug}</link>
19            <description><![CDATA[${post.description}]]></description>
20            <pubDate>${new Date(post.date).toUTCString()}</pubDate>
21            <author>${post.author.name}</author>
22          </item>
23        `).join('')}
24      </channel>
25    </rss>`;
26
27  return new Response(rss, {
28    headers: {
29      'Content-Type': 'application/xml',
30      'Cache-Control': 'public, max-age=3600',
31    },
32  });
33}

구현 효과

1. 검색 엔진 최적화

  • 검색 결과에서 더 풍부한 정보 제공
  • 구조화된 데이터를 통한 리치 스니펫 표시
  • 빠른 페이지 로드로 인한 검색 순위 개선

2. 사용자 경험 향상

  • 빠른 초기 페이지 로드
  • 부드러운 페이지 전환
  • 접근성 개선으로 모든 사용자가 콘텐츠에 쉽게 접근

3. 국제화 지원

  • 다국어 콘텐츠 자동 감지 및 제공
  • 언어별 최적화된 메타데이터
  • 검색 엔진의 언어별 색인 최적화

결론

SEO는 기술적 구현을 넘어 사용자 경험의 핵심 요소입니다. Next.js의 강력한 기능들을 활용하여 검색 엔진 최적화와 사용자 경험을 모두 만족시키는 솔루션을 구현할 수 있었습니다. 이러한 최적화는 지속적인 모니터링과 개선을 통해 더욱 발전시켜 나갈 예정입니다.

공유하기

관련 포스트