웹 개발

Next.js SEO 최적화 실전 테크닉

Daniel Kim|프론트엔드 리드 개발자
2025-06-02
12분 소요
#Next.js#SEO#메타데이터#구조화 데이터#성능 최적화
Next.js SEO 최적화 실전 테크닉

Next.js는 React 기반 프레임워크 중 가장 SEO 친화적인 선택입니다. 하지만 프레임워크가 SEO를 '가능하게' 해줄 뿐, 실제 최적화는 개발자의 몫입니다.

이 가이드에서는 Next.js 15 App Router 기준으로 실전에서 바로 적용 가능한 SEO 최적화 테크닉을 다룹니다.

Next.js SEO의 3가지 핵심 축

SEO Optimization Pyramid Authority Backlinks & Trust Content Strategy Keywords, Quality, UX Technical SEO Backlinks, Reviews, Brand Mentions Keyword Research, Blog Posts, Landing Pages Core Web Vitals, Schema, Sitemap
SEO is built layer by layer from technical foundation to authority

1. 기술적 SEO (Technical SEO)

  • 서버 사이드 렌더링 (SSR/SSG)
  • Core Web Vitals 최적화
  • 구조화 데이터 (JSON-LD)
  • Sitemap/Robots.txt 자동화

2. On-Page SEO

  • 메타데이터 최적화
  • 시맨틱 HTML 구조
  • 내부 링크 전략
  • 이미지 최적화

3. 콘텐츠 SEO

  • 키워드 전략
  • 사용자 의도 반영
  • E-E-A-T (Experience, Expertise, Authoritativeness, Trustworthiness)
💡 2025년 SEO 핵심 변화

Google은 이제 사용자 경험(UX)콘텐츠 품질을 동등하게 평가합니다. 빠르지만 내용 없는 사이트나, 좋은 콘텐츠지만 느린 사이트 모두 상위 노출이 어렵습니다.


1. 메타데이터 최적화: App Router 방식

Next.js 15의 App Router는 파일 기반 메타데이터 시스템을 제공합니다.

Next.js SEO Metadata Flow generateMetadata Server Function Runs at request/build HTML <head> title, meta, OG tags Injected into response Search Crawler Google, Bing, etc. Reads & indexes Search Results Rich snippets, OG cards Shown to users
How metadata flows from code to search results

정적 메타데이터 (기본)

typescript
// app/about/page.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'About POEMA | Digital Agency',
  description: 'POEMA는 웹 개발, 이커머스, 퍼포먼스 마케팅을 제공하는 디지털 에이전시입니다.',
  keywords: ['웹 개발', '이커머스', '디지털 마케팅', 'POEMA'],
  authors: [{ name: 'POEMA Team' }],
  openGraph: {
    title: 'About POEMA',
    description: 'Digital transformation partner for startups',
    images: ['/og-image.jpg'],
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: 'About POEMA',
    description: 'Digital transformation partner',
    images: ['/twitter-image.jpg'],
  },
};

export default function AboutPage() {
  return <div>...</div>;
}

동적 메타데이터 (추천)

케이스 스터디, 블로그 포스트 등 동적 페이지에 필수입니다.

typescript
// app/portfolio/[slug]/page.tsx
import { Metadata } from 'next';
import { getCaseStudy } from '@/data/marketing/case-studies';

export async function generateMetadata({
  params
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params;
  const caseStudy = await getCaseStudy(slug);

  if (!caseStudy) {
    return {
      title: 'Portfolio Not Found',
    };
  }

  return {
    title: `${caseStudy.title.ko} | POEMA Portfolio`,
    description: caseStudy.overview.ko,
    keywords: caseStudy.tags.ko,
    openGraph: {
      title: caseStudy.title.ko,
      description: caseStudy.overview.ko,
      images: [
        {
          url: caseStudy.thumbnail,
          width: 1200,
          height: 630,
          alt: caseStudy.title.ko,
        },
      ],
      type: 'article',
      publishedTime: caseStudy.publishedAt,
    },
  };
}
✅ 메타데이터 체크리스트
  • [ ] 모든 페이지에 고유한 title (50-60자 이내)
  • [ ] description 120-160자, 액션 지향적 문구 포함
  • [ ] OG 이미지 1200×630px, 파일 크기 300KB 이하
  • [ ] Twitter Card 설정
  • [ ] 다국어 사이트의 경우 alternates 설정

  • 2. 구조화 데이터: JSON-LD로 리치 스니펫 획득

    구조화 데이터는 검색 엔진이 콘텐츠를 이해하도록 돕고, 리치 스니펫을 통해 CTR을 높입니다.

    Organization Schema (전역)

    typescript
    // app/layout.tsx
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      const organizationSchema = {
        '@context': 'https://schema.org',
        '@type': 'Organization',
        name: 'POEMA',
        url: 'https://poema.agency',
        logo: 'https://poema.agency/logo.png',
        description: 'Digital agency specializing in web development and e-commerce',
        address: {
          '@type': 'PostalAddress',
          addressCountry: 'KR',
          addressLocality: 'Seoul',
        },
        contactPoint: {
          '@type': 'ContactPoint',
          telephone: '+82-2-1234-5678',
          contactType: 'Customer Service',
          email: 'hello@poema.agency',
        },
        sameAs: [
          'https://www.instagram.com/poema.agency',
          'https://www.linkedin.com/company/poema',
        ],
      };
    
      return (
        <html>
          <head>
            <script
              type="application/ld+json"
              dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }}
            />
          </head>
          <body>{children}</body>
        </html>
      );
    }

    Article Schema (블로그 포스트)

    typescript
    // components/seo/BlogPostJsonLd.tsx
    interface BlogPostJsonLdProps {
      title: string;
      description: string;
      publishedAt: string;
      author: string;
      image: string;
      url: string;
    }
    
    export function BlogPostJsonLd({ title, description, publishedAt, author, image, url }: BlogPostJsonLdProps) {
      const schema = {
        '@context': 'https://schema.org',
        '@type': 'BlogPosting',
        headline: title,
        description: description,
        image: image,
        datePublished: publishedAt,
        author: {
          '@type': 'Person',
          name: author,
        },
        publisher: {
          '@type': 'Organization',
          name: 'POEMA',
          logo: {
            '@type': 'ImageObject',
            url: 'https://poema.agency/logo.png',
          },
        },
        mainEntityOfPage: {
          '@type': 'WebPage',
          '@id': url,
        },
      };
    
      return (
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
        />
      );
    }

    FAQPage Schema

    typescript
    export function FAQJsonLd({ faqs }: { faqs: { question: string; answer: string }[] }) {
      const schema = {
        '@context': 'https://schema.org',
        '@type': 'FAQPage',
        mainEntity: faqs.map((faq) => ({
          '@type': 'Question',
          name: faq.question,
          acceptedAnswer: {
            '@type': 'Answer',
            text: faq.answer,
          },
        })),
      };
    
      return (
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
        />
      );
    }
    ⚠️ 구조화 데이터 주의사항
  • Google Rich Results Test로 반드시 검증
  • 실제 페이지에 표시된 내용과 일치해야 함
  • 과도한 키워드 스터핑 금지
  • 업데이트 시 스키마도 함께 수정

  • 3. Sitemap & Robots.txt 자동화

    Next.js 15는 파일 기반 sitemap/robots 생성을 지원합니다.

    동적 Sitemap 생성

    typescript
    // app/sitemap.ts
    import { MetadataRoute } from 'next';
    import { caseStudies } from '@/data/marketing/case-studies';
    import { blogPosts } from '@/data/marketing/blog';
    
    export default function sitemap(): MetadataRoute.Sitemap {
      const baseUrl = 'https://poema.agency';
    
      // Static pages
      const staticPages: MetadataRoute.Sitemap = [
        {
          url: baseUrl,
          lastModified: new Date(),
          changeFrequency: 'weekly',
          priority: 1.0,
        },
        {
          url: `${baseUrl}/services`,
          lastModified: new Date(),
          changeFrequency: 'monthly',
          priority: 0.8,
        },
        {
          url: `${baseUrl}/portfolio`,
          lastModified: new Date(),
          changeFrequency: 'weekly',
          priority: 0.9,
        },
      ];
    
      // Dynamic case studies
      const caseStudyPages: MetadataRoute.Sitemap = caseStudies.map((study) => ({
        url: `${baseUrl}/portfolio/${study.slug}`,
        lastModified: new Date(study.publishedAt),
        changeFrequency: 'monthly' as const,
        priority: 0.7,
      }));
    
      // Dynamic blog posts
      const blogPages: MetadataRoute.Sitemap = blogPosts.map((post) => ({
        url: `${baseUrl}/blog/${post.slug}`,
        lastModified: new Date(post.publishedAt),
        changeFrequency: 'monthly' as const,
        priority: 0.6,
      }));
    
      return [...staticPages, ...caseStudyPages, ...blogPages];
    }

    Robots.txt 설정

    typescript
    // app/robots.ts
    import { MetadataRoute } from 'next';
    
    export default function robots(): MetadataRoute.Robots {
      const baseUrl = 'https://poema.agency';
    
      return {
        rules: [
          {
            userAgent: '*',
            allow: '/',
            disallow: ['/api/', '/admin/', '/_next/'],
          },
          {
            userAgent: 'Googlebot',
            allow: '/',
            disallow: ['/api/', '/admin/'],
          },
        ],
        sitemap: `${baseUrl}/sitemap.xml`,
      };
    }

    4. Core Web Vitals 최적화

    Core Web Vitals Targets LCP Largest Contentful Paint ≤ 2.5s Loading Speed INP Interaction to Next Paint ≤ 200ms Responsiveness CLS Cumulative Layout Shift ≤ 0.1 Visual Stability
    Google uses these metrics as ranking signals

    Google은 LCP, FID, CLS를 페이지 경험 지표로 평가합니다.

    LCP (Largest Contentful Paint) 최적화

    목표: 2.5초 이하

    typescript
    // 이미지 최적화
    import Image from 'next/image';
    
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1920}
      height={1080}
      priority // LCP 이미지에 필수
      quality={90}
      placeholder="blur"
      blurDataURL="data:image/..."
    />
    
    // 폰트 최적화
    import { Inter } from 'next/font/google';
    
    const inter = Inter({
      subsets: ['latin'],
      display: 'swap', // FOUT 방지
      preload: true,
    });

    CLS (Cumulative Layout Shift) 최적화

    목표: 0.1 이하

    tsx
    // 이미지/동영상에 명시적 크기 지정
    <Image src="..." width={600} height={400} alt="..." />
    
    // 동적 콘텐츠에 최소 높이 설정
    <div className="min-h-[400px]">
      {isLoading ? <Skeleton /> : <Content />}
    </div>
    
    // 폰트 preload로 FOUT 방지
    <link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />

    FID/INP 최적화

    목표: FID 100ms 이하, INP 200ms 이하

    • Dynamic import로 JS 번들 분할
    • 무거운 컴포넌트는 below-the-fold에서만 로드
    • debounce/throttle로 이벤트 핸들러 최적화
    ✅ Core Web Vitals 체크리스트
  • [ ] PageSpeed Insights로 실제 사용자 데이터 확인
  • [ ] Lighthouse CI로 빌드마다 자동 테스트
  • [ ] Vercel Analytics/Google Search Console로 지속 모니터링
  • [ ] 모바일 우선 최적화 (트래픽의 70%+)

  • 5. 다국어 SEO (next-intl 활용)

    typescript
    // app/[locale]/layout.tsx
    import { Metadata } from 'next';
    
    export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
      const { locale } = await params;
    
      return {
        alternates: {
          canonical: `https://poema.agency/${locale}`,
          languages: {
            'ko-KR': 'https://poema.agency/ko',
            'en-US': 'https://poema.agency/en',
          },
        },
      };
    }

    Hreflang 태그는 Next.js가 자동 생성하지만, Sitemap에는 수동 추가 필요:

    typescript
    export default function sitemap(): MetadataRoute.Sitemap {
      return [
        {
          url: 'https://poema.agency/ko',
          lastModified: new Date(),
          alternates: {
            languages: {
              en: 'https://poema.agency/en',
            },
          },
        },
      ];
    }

    6. SEO 체크리스트: 론칭 전 필수 확인

    기술적 체크

    항목도구목표
    Core Web VitalsPageSpeed InsightsLCP <2.5s, FID <100ms, CLS <0.1
    모바일 친화성Mobile-Friendly TestPass
    구조화 데이터Rich Results Test0 Errors
    보안SSL LabsA+
    SitemapGoogle Search ConsoleIndexed

    On-Page 체크

    ✅ SEO 론칭 체크리스트
  • [ ] 모든 페이지에 고유한 </code>, <code><meta description></code></li><li style="margin-left:16px;list-style:disc;color:#166534;">[ ] H1 태그 페이지당 1개, 의미 있는 계층 구조</li><li style="margin-left:16px;list-style:disc;color:#166534;">[ ] 이미지 alt 텍스트 100% 작성</li><li style="margin-left:16px;list-style:disc;color:#166534;">[ ] 내부 링크 전략 (주요 페이지 간 연결)</li><li style="margin-left:16px;list-style:disc;color:#166534;">[ ] 404 페이지 커스터마이징</li><li style="margin-left:16px;list-style:disc;color:#166534;">[ ] OG 이미지 모든 페이지 설정</li><li style="margin-left:16px;list-style:disc;color:#166534;">[ ] Canonical URL 설정</li><li style="margin-left:16px;list-style:disc;color:#166534;">[ ] 구조화 데이터 (최소 Organization + Breadcrumb)</li></div> </div> <hr style="border:none;border-top:1px solid #e2e8f0;margin:32px 0;"/> <h2>실전 팁: 빠른 SEO 성과 내기</h2> <h3>1. 롱테일 키워드 공략</h3> <blockquote style="border-left:3px solid #cbd5e1;padding:12px 20px;margin:24px 0;background:#f8fafc;border-radius:0 8px 8px 0;font-style:italic;color:#475569;"><p style="margin:4px 0;">"이커머스 개발"보다 "Shopify 한글화 개발 에이전시"가 경쟁률 낮고 전환율 높습니다.</p></blockquote> <ul><li>Google Keyword Planner, Ahrefs로 키워드 리서치</li><li>블로그 콘텐츠로 롱테일 키워드 대량 타겟팅</li><li>지역 키워드 활용 ("서울 웹 개발", "강남 쇼핑몰 제작")</li></ul> <h3>2. E-E-A-T 강화</h3> <ul><li>팀 소개 페이지에 실명, 사진, 경력 공개</li><li>케이스 스터디에 클라이언트 로고/추천사</li><li>블로그에 저자 프로필 명시</li><li>업계 미디어 기고, 외부 사이트 백링크</li></ul> <h3>3. 기술 블로그로 자연 유입 확보</h3> <p>"Next.js SEO 가이드", "포트폴리오 사이트 제작 방법" 같은 How-to 콘텐츠는 장기적으로 꾸준한 트래픽을 가져옵니다.</p> <ul><li>주 1-2회 발행</li><li>2,000자 이상 심도 있는 콘텐츠</li><li>코드 예시, 스크린샷 포함</li><li>내부 링크로 서비스 페이지 연결</li></ul> <hr style="border:none;border-top:1px solid #e2e8f0;margin:32px 0;"/> <h2>마무리: SEO는 마라톤</h2> <p>SEO 성과는 보통 3-6개월 후 나타납니다. 단기 트래픽이 필요하다면 광고를, 장기 자산을 쌓고 싶다면 SEO를 선택하세요.</p> <p>Next.js는 SEO를 위한 모든 도구를 제공합니다. 이제 실행만 남았습니다.</p>
  • 관련 글