테마 전환

Next.js 다국어 SEO 최적화 가이드: 검색 엔진이 언어별 페이지를 올바르게 색인하게 만드는 법

Easton editorial illustration: island architecture model

정성 들여 다국어 웹사이트를 만들었지만 검색 엔진에는 계속 잘못된 언어 페이지가 표시될 수 있습니다. 사용자가 한국어나 중국어 콘텐츠를 검색했는데 클릭하면 영어 페이지로 이동하기도 합니다. 언어별 페이지가 검색 결과에서 서로 경쟁해 전체 순위가 떨어지는 경우도 있습니다.

이런 현상은 다국어 웹사이트의 SEO 설정이 잘못되었을 때 흔히 발생합니다. Google 통계에 따르면 다국어 웹사이트의 60% 이상에 hreflang 설정 오류가 있으며, 이는 국제화 성과와 사용자 경험에 큰 영향을 줍니다.

60%+
다국어 웹사이트 설정 오류율
다국어 웹사이트의 60% 이상에 hreflang 설정 오류가 있음

이 글에서는 Next.js에서 다국어 SEO를 올바르게 구현하는 방법을 다음 항목을 중심으로 설명합니다.

  • hreflang 태그를 올바르게 설정하는 방법 - 언어 페이지 혼선을 방지합니다.
  • 다국어 Sitemap 생성 전략 - 검색 엔진의 색인을 빠르게 합니다.
  • URL 구조 모범 사례 - 프로젝트에 맞는 국제화 방식을 선택합니다.
  • 흔한 오류 점검과 수정 - 문제를 빠르게 찾아 해결합니다.

Pages Router와 App Router 중 어느 쪽을 사용하더라도 해당하는 해결 방법을 찾을 수 있습니다.

1. 다국어 SEO의 핵심 개념 이해하기

1.1 hreflang이란

hreflang은 페이지의 대상 언어와 지역을 검색 엔진에 알리는 HTML 속성입니다. 주요 역할은 다음과 같습니다.

  1. 중복 콘텐츠 문제 방지 - 언어별 페이지가 중복이 아니라 같은 콘텐츠의 번역임을 검색 엔진에 알립니다.
  2. 사용자와 정확히 연결 - 사용자의 언어 및 지역 설정에 맞는 페이지를 표시합니다.
  3. 사용자 경험 개선 - 사용자가 잘못된 언어의 콘텐츠를 보지 않도록 합니다.

1.2 Google이 다국어 콘텐츠를 처리하는 방식

Google 크롤러가 다국어 웹사이트에 접근하면 다음 단계를 거칩니다.

  1. 페이지의 언어를 감지합니다(HTML lang 속성, hreflang 태그, 페이지 콘텐츠 분석).
  2. hreflang 태그를 찾아 언어별 페이지 사이의 관계를 파악합니다.
  3. 사용자의 언어 선호도에 맞는 페이지를 검색 결과에 표시합니다.
  4. 언어별 페이지가 서로 경쟁하게 두지 않고 SEO 신호를 통합합니다.

1.3 흔한 SEO 오류 사례

오류 1: hreflang 태그 누락

<!-- ❌ 오류: hreflang 태그가 없음 -->
<head>
  <title>My Website</title>
  <link rel="canonical" href="https://example.com/en/about" />
</head>

결과: 검색 엔진이 언어별 페이지의 관계를 파악하지 못해 검색 결과에 잘못된 언어 페이지가 나타날 수 있습니다.

오류 2: 비대칭 hreflang 설정

<!-- 영어 페이지 -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />

<!-- ❌ 중국어 페이지 - 오류: hreflang 태그가 없음 -->
<!-- 모든 언어 페이지에 완전한 hreflang을 설정해야 함 -->

결과: Google은 hreflang이 양방향으로 설정되기를 요구하므로 단방향 설정은 무시됩니다.

오류 3: 잘못된 언어 코드 사용

<!-- ❌ 오류: 표준에 맞지 않는 언어 코드 사용 -->
<link rel="alternate" hreflang="cn" href="..." /> <!-- zh를 사용해야 함 -->
<link rel="alternate" hreflang="en-us" href="..." /> <!-- 대소문자를 지켜 en-US를 사용해야 함 -->

결과: 검색 엔진이 언어 코드를 인식하지 못해 hreflang 설정이 작동하지 않습니다.

2. URL 전략 선택

다국어 웹사이트를 구현하기 전에 적절한 URL 전략을 먼저 선택해야 합니다. 이 결정은 SEO, 사용자 경험, 기술 구현 전반에 영향을 줍니다.

2.1 세 가지 주요 URL 전략 비교

전략예시SEO 영향구현 난이도추천도
하위 디렉터리example.com/en/
example.com/zh/
⭐⭐⭐⭐⭐ 매우 좋음⭐⭐⭐ 보통⭐⭐⭐⭐⭐
하위 도메인en.example.com
zh.example.com
⭐⭐⭐ 보통⭐⭐⭐⭐ 어려움⭐⭐⭐
URL 매개변수example.com?lang=en⭐⭐ 좋지 않음⭐⭐⭐⭐⭐ 쉬움⭐⭐

2.2 전략별 상세 분석

방안 1: 하위 디렉터리 전략(권장)

장점:

  • SEO 신호가 기본 도메인에 집중되어 전체 순위 향상에 유리합니다.
  • 설정이 간단하고 별도의 도메인 관리나 SSL 인증서가 필요 없습니다.
  • 한 코드베이스로 배포하므로 유지보수와 확장이 쉽습니다.
  • Next.js에서 기본 지원하므로 구현이 간단합니다.

단점:

  • 모든 언어가 같은 도메인을 사용하므로 특정 시장별 DNS 최적화가 어렵습니다.

Next.js 구현:

// next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'zh', 'ja', 'de'],
    defaultLocale: 'en',
    localeDetection: true // 사용자 언어 자동 감지
  }
}

방안 2: 하위 도메인 전략

장점:

  • 시장별로 다른 서버에 배포할 수 있습니다(예: 중국 지역 별도 배포).
  • 기술 스택을 독립적으로 운영할 수 있어 유연성이 높습니다.
  • CDN과 지리적 위치 최적화에 유리합니다.

단점:

  • SEO 신호가 분산되므로 하위 도메인마다 권위를 별도로 쌓아야 합니다.
  • 추가 도메인 관리와 SSL 인증서가 필요합니다.
  • 구현 및 유지보수 비용이 더 높습니다.

방안 3: URL 매개변수 전략(권장하지 않음)

장점:

  • 구현이 가장 간단합니다.

단점:

  • 검색 엔진이 매개변수를 무시할 수 있어 SEO 효과가 가장 낮습니다.
  • URL이 사용자 친화적이지 않습니다.
  • CDN 캐시를 최적화하기 어렵습니다.
  • 검색 결과에서 언어별 페이지를 구분하기 어렵습니다.

결론:

대부분의 프로젝트에는 하위 디렉터리 전략을 적극 권장합니다. SEO 효과, 구현 난이도, 유지보수 비용 사이의 균형이 가장 좋습니다.

3. hreflang 설정 자세히 알아보기

3.1 hreflang 태그의 역할

hreflang 태그는 검색 엔진에 다음 정보를 전달합니다.

  1. 이 페이지에 어떤 언어 페이지가 있는지
  2. 각 페이지의 전체 URL이 무엇인지
  3. 각 페이지가 어떤 언어와 지역을 대상으로 하는지

3.2 Next.js App Router에서 hreflang 설정하기

방법 1: Metadata API 사용(권장)

Next.js 13+의 App Router는 더 간결한 Metadata API를 제공합니다.

// app/[lang]/about/page.tsx
import { Metadata } from 'next'

type Props = {
  params: { lang: string }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { lang } = params

  // 지원 언어 목록 정의
  const languages = ['en', 'zh', 'ja', 'de']

  // 모든 언어의 alternate 링크 생성
  const alternates = {
    canonical: `https://example.com/${lang}/about`,
    languages: languages.reduce((acc, locale) => {
      acc[locale] = `https://example.com/${locale}/about`
      return acc
    }, {} as Record<string, string>)
  }

  return {
    title: 'About Us',
    alternates,
    // 일치하는 언어가 없을 때 사용할 x-default 추가
    other: {
      'x-default': 'https://example.com/en/about'
    }
  }
}

export default function AboutPage({ params }: Props) {
  return <div>About page in {params.lang}</div>
}

방법 2: 사용자 정의 Head 컴포넌트 사용

더 세밀하게 제어해야 하는 경우에 적합합니다.

// components/I18nHead.tsx
import Head from 'next/head'

interface I18nHeadProps {
  currentLang: string
  pathname: string
  languages?: string[]
}

export default function I18nHead({
  currentLang,
  pathname,
  languages = ['en', 'zh', 'ja', 'de']
}: I18nHeadProps) {
  const baseUrl = 'https://example.com'

  return (
    <Head>
      {/* 현재 페이지의 canonical URL */}
      <link rel="canonical" href={`${baseUrl}/${currentLang}${pathname}`} />

      {/* 모든 언어 페이지의 hreflang */}
      {languages.map(lang => (
        <link
          key={lang}
          rel="alternate"
          hrefLang={lang}
          href={`${baseUrl}/${lang}${pathname}`}
        />
      ))}

      {/* x-default는 기본 언어를 가리킴 */}
      <link
        rel="alternate"
        hrefLang="x-default"
        href={`${baseUrl}/en${pathname}`}
      />
    </Head>
  )
}

사용 방법:

// app/[lang]/about/page.tsx
import I18nHead from '@/components/I18nHead'

export default function AboutPage({ params }: { params: { lang: string } }) {
  return (
    <>
      <I18nHead
        currentLang={params.lang}
        pathname="/about"
      />
      <div>About page content</div>
    </>
  )
}

3.3 Next.js Pages Router에서 hreflang 설정하기

Pages Router에서는 다른 API를 사용합니다.

// pages/about.tsx
import { GetStaticProps } from 'next'
import Head from 'next/head'
import { useRouter } from 'next/router'

export default function AboutPage() {
  const router = useRouter()
  const { locale, locales, asPath } = router
  const baseUrl = 'https://example.com'

  return (
    <>
      <Head>
        {/* 현재 페이지의 canonical URL */}
        <link rel="canonical" href={`${baseUrl}/${locale}${asPath}`} />

        {/* 모든 언어 페이지의 hreflang */}
        {locales?.map(loc => (
          <link
            key={loc}
            rel="alternate"
            hrefLang={loc}
            href={`${baseUrl}/${loc}${asPath}`}
          />
        ))}

        {/* x-default 기본 언어 */}
        <link
          rel="alternate"
          hrefLang="x-default"
          href={`${baseUrl}/en${asPath}`}
        />
      </Head>

      <div>About page content</div>
    </>
  )
}

export const getStaticProps: GetStaticProps = async ({ locale }) => {
  return {
    props: {
      messages: (await import(`../locales/${locale}.json`)).default
    }
  }
}

3.4 지역 코드를 사용하는 고급 설정

특정 국가나 지역에 맞춤 콘텐츠를 제공해야 한다면 language-REGION 형식을 사용할 수 있습니다.

// 지역별 영어 및 중국어 사용자 대상 설정
const hreflangConfig = {
  'en-US': 'https://example.com/en-us/about', // 미국 영어
  'en-GB': 'https://example.com/en-gb/about', // 영국 영어
  'en-AU': 'https://example.com/en-au/about', // 호주 영어
  'zh-CN': 'https://example.com/zh-cn/about', // 중국 본토 간체 중국어
  'zh-TW': 'https://example.com/zh-tw/about', // 대만 번체 중국어
  'zh-HK': 'https://example.com/zh-hk/about', // 홍콩 번체 중국어
}

Next.js에서 지역 단위 라우팅을 구현하는 방법은 다음과 같습니다.

// next.config.js
module.exports = {
  i18n: {
    locales: ['en-US', 'en-GB', 'en-AU', 'zh-CN', 'zh-TW', 'zh-HK'],
    defaultLocale: 'en-US',
  }
}

3.5 흔한 설정 오류와 해결 방법

오류 1: 자기 참조 누락

<!-- ❌ 오류: 현재 페이지가 자기 자신을 참조하지 않음 -->
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />

<!-- ✅ 올바름: 현재 페이지의 자기 참조를 포함해야 함 -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />

왜 자기 참조가 필요한가요?
Google은 hreflang이 양방향이어야 한다고 규정합니다. 따라서 모든 언어 페이지는 자기 자신을 포함해 다른 모든 페이지를 참조해야 합니다.

오류 2: x-default 누락

<!-- ✅ 권장: 기본 언어를 나타내는 x-default 추가 -->
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />

x-default는 지원 언어와 일치하지 않는 사용자에게 기본 페이지를 제공합니다. 예를 들면 다음과 같습니다.

  • 사용자의 브라우저 언어가 아랍어지만 웹사이트가 아랍어를 지원하지 않는 경우
  • 검색 엔진은 x-default가 지정한 페이지를 반환합니다.

오류 3: hreflang과 canonical 충돌

<!-- ❌ 오류: canonical이 다른 언어 페이지를 가리킴 -->
<link rel="canonical" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />

<!-- ✅ 올바름: canonical은 현재 언어 페이지를 가리켜야 함 -->
<link rel="canonical" href="https://example.com/zh/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />

핵심 원칙: canonical 태그는 다른 언어 페이지가 아니라 현재 페이지 자체의 URL을 가리켜야 합니다.

4. 다국어 Sitemap 구현

Sitemap은 검색 엔진이 페이지를 발견하고 색인하도록 돕는 중요한 도구입니다. 다국어 웹사이트에서는 Sitemap을 올바르게 설정하는 것이 특히 중요합니다.

4.1 다국어 Sitemap이 필요한 이유

다국어 Sitemap에는 세 가지 주요 이점이 있습니다.

  1. 색인 속도 향상 - 크롤러가 우연히 찾기를 기다리지 않고 검색 엔진에 모든 언어 페이지를 직접 알립니다.
  2. 누락 방지 - 특히 링크 깊이가 깊은 페이지를 비롯해 일부 언어 페이지가 빠지는 일을 방지합니다.
  3. hreflang 정보 전달 - Sitemap에도 hreflang을 설정해 언어 페이지 사이의 관계를 강화할 수 있습니다.

4.2 Sitemap 전략 선택

웹사이트 규모에 맞는 방식을 선택하세요.

방안 1: 단일 Sitemap(소규모 웹사이트에 권장)

모든 언어 URL을 하나의 sitemap.xml에 넣는 방식으로, 페이지 수가 5,000개 이하인 웹사이트에 적합합니다.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <!-- 영어 페이지 -->
  <url>
    <loc>https://example.com/en/about</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
    <xhtml:link rel="alternate" hreflang="zh" href="https://example.com/zh/about"/>
    <xhtml:link rel="alternate" hreflang="ja" href="https://example.com/ja/about"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
  </url>
  <!-- 중국어 페이지 -->
  <url>
    <loc>https://example.com/zh/about</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
    <xhtml:link rel="alternate" hreflang="zh" href="https://example.com/zh/about"/>
    <xhtml:link rel="alternate" hreflang="ja" href="https://example.com/ja/about"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
  </url>
</urlset>

방안 2: 언어별 Sitemap(대형 웹사이트에 권장)

언어별로 독립된 Sitemap을 만들고 sitemap index로 묶는 방식입니다. 페이지가 5,000개를 넘거나 언어 수가 많은 웹사이트에 적합합니다.

<!-- sitemap-index.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://example.com/sitemap-en.xml</loc>
    <lastmod>2025-01-01</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-zh.xml</loc>
    <lastmod>2025-01-01</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-ja.xml</loc>
    <lastmod>2025-01-01</lastmod>
  </sitemap>
</sitemapindex>

4.3 Next.js App Router에서 Sitemap 생성하기

Next.js 13+는 편리한 Sitemap 생성 기능을 기본으로 제공합니다.

// app/sitemap.ts
import { MetadataRoute } from 'next'

// 지원 언어 목록 정의
const languages = ['en', 'zh', 'ja', 'de']

// 언어 접두사를 제외한 모든 사이트 경로 정의
const routes = ['', '/about', '/blog', '/contact']

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = 'https://example.com'
  const sitemap: MetadataRoute.Sitemap = []

  // 각 경로에 대해 모든 언어 페이지 생성
  routes.forEach(route => {
    // 언어마다 항목 하나씩 생성
    languages.forEach(lang => {
      const url = `${baseUrl}/${lang}${route}`

      sitemap.push({
        url,
        lastModified: new Date(),
        changeFrequency: 'weekly',
        priority: route === '' ? 1 : 0.8,
        // Next.js가 alternateRefs를 자동으로 처리함
        alternates: {
          languages: languages.reduce((acc, l) => {
            acc[l] = `${baseUrl}/${l}${route}`
            return acc
          }, {} as Record<string, string>)
        }
      })
    })
  })

  return sitemap
}

4.4 동적 콘텐츠의 Sitemap 생성

블로그 글이나 제품 페이지 같은 동적 콘텐츠가 있다면 데이터베이스 또는 CMS에서 데이터를 가져와야 합니다.

// app/sitemap.ts
import { MetadataRoute } from 'next'

const languages = ['en', 'zh', 'ja']
const baseUrl = 'https://example.com'

// 데이터베이스 또는 CMS에서 글 목록 가져오기
async function getArticles() {
  // 실제 프로젝트에서는 데이터베이스나 CMS API에서 가져와야 함
  // 예: const articles = await prisma.article.findMany()
  return [
    { slug: 'getting-started', lastModified: '2025-01-01' },
    { slug: 'advanced-guide', lastModified: '2025-01-15' },
  ]
}

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const sitemap: MetadataRoute.Sitemap = []

  // 1. 정적 페이지 추가
  const staticPages = ['', '/about', '/contact']
  staticPages.forEach(page => {
    languages.forEach(lang => {
      sitemap.push({
        url: `${baseUrl}/${lang}${page}`,
        lastModified: new Date(),
        changeFrequency: 'monthly',
        priority: page === '' ? 1 : 0.8,
        alternates: {
          languages: languages.reduce((acc, l) => {
            acc[l] = `${baseUrl}/${l}${page}`
            return acc
          }, {} as Record<string, string>)
        }
      })
    })
  })

  // 2. 동적 콘텐츠(블로그 글) 추가
  const articles = await getArticles()
  articles.forEach(article => {
    languages.forEach(lang => {
      sitemap.push({
        url: `${baseUrl}/${lang}/blog/${article.slug}`,
        lastModified: new Date(article.lastModified),
        changeFrequency: 'weekly',
        priority: 0.6,
        alternates: {
          languages: languages.reduce((acc, l) => {
            acc[l] = `${baseUrl}/${l}/blog/${article.slug}`
            return acc
          }, {} as Record<string, string>)
        }
      })
    })
  })

  return sitemap
}

4.5 Pages Router에서 Sitemap 생성하기

Pages Router에서는 API 경로를 직접 만들어야 합니다.

// pages/api/sitemap.xml.ts
import { NextApiRequest, NextApiResponse } from 'next'

const baseUrl = 'https://example.com'
const languages = ['en', 'zh', 'ja']

function generateSiteMap(pages: string[]) {
  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
${pages.map(page => {
  return languages.map(lang => {
    const url = `${baseUrl}/${lang}${page}`
    const alternates = languages.map(l =>
      `    <xhtml:link rel="alternate" hreflang="${l}" href="${baseUrl}/${l}${page}"/>`
    ).join('\n')

    return `  <url>
    <loc>${url}</loc>
    <lastmod>${new Date().toISOString()}</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
${alternates}
    <xhtml:link rel="alternate" hreflang="x-default" href="${baseUrl}/en${page}"/>
  </url>`
  }).join('\n')
}).join('\n')}
</urlset>`
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // 모든 페이지 경로 정의
  const pages = ['', '/about', '/blog', '/contact']

  const sitemap = generateSiteMap(pages)

  res.setHeader('Content-Type', 'text/xml')
  res.write(sitemap)
  res.end()
}

4.6 검색 엔진에 Sitemap 제출하기

Sitemap을 생성한 뒤에는 검색 엔진에 직접 제출해 색인 속도를 높여야 합니다.

방법 1: robots.txt에 선언

가장 간단한 방법이며 검색 엔진이 자동으로 읽습니다.

# public/robots.txt
User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml

방법 2: Google Search Console에 제출

직접 제출하면 색인을 바로 요청할 수 있습니다.

  1. Google Search Console에 접속합니다.
  2. 웹사이트 속성을 선택합니다.
  3. 왼쪽 메뉴에서 ‘Sitemaps’를 선택합니다.
  4. sitemap.xml URL을 입력합니다.
  5. ‘제출’을 클릭합니다.

방법 3: Bing Webmaster Tools에 제출

Bing도 일정한 시장 점유율이 있으므로 빼놓지 마세요.

  1. Bing Webmaster Tools에 접속합니다.
  2. 웹사이트를 추가합니다.
  3. ‘Sitemaps’ 영역에서 sitemap.xml을 제출합니다.

4.7 Sitemap 검증

다음 도구로 Sitemap 형식이 올바른지 확인할 수 있습니다.

  1. XML Sitemap Validator: https://www.xml-sitemaps.com/validate-xml-sitemap.html
  2. Google Search Console: 제출 후 색인 상태와 오류 메시지를 확인합니다.
  3. 온라인 XML 검증기: XML 형식이 표준에 완전히 맞는지 확인합니다.

5. 모범 사례와 주의 사항

5.1 콘텐츠 번역 품질의 중요성

검색 엔진, 특히 Google은 품질이 낮은 번역을 감지할 수 있으며 이는 순위에 직접 영향을 줍니다.

피해야 할 일:

  • ❌ Google Translate 같은 자동 번역 도구로 생성한 콘텐츠를 그대로 사용
  • ❌ 내비게이션과 제목만 번역하고 본문은 다른 언어로 유지
  • ❌ 언어별 페이지의 콘텐츠 구조와 정보량에 큰 차이를 둠

해야 할 일:

  • ✅ 전문 번역가나 원어민에게 번역 의뢰
  • ✅ 단순 번역이 아닌 현지화 수행(문화적 차이와 표현 습관 고려)
  • ✅ 언어별 페이지의 콘텐츠 일관성과 품질 기준 유지

5.2 자동 번역의 SEO 위험 피하기

클라이언트 자동 번역은 SEO에 전혀 도움이 되지 않습니다. 검색 엔진 크롤러에는 원문만 보이기 때문입니다.

// ❌ 권장하지 않음: 클라이언트 자동 번역(검색 엔진이 색인할 수 없음)
import GoogleTranslate from 'google-translate-api'

export default function Page() {
  const [content, setContent] = useState('')

  useEffect(() => {
    // 이 방식은 SEO에 도움이 되지 않음
    GoogleTranslate(originalText, { to: 'zh' })
      .then(res => setContent(res.text))
  }, [])

  return <div>{content}</div>
}
// ✅ 권장: 서버에서 실제 번역 콘텐츠 렌더링
export default function Page({ params }: { params: { lang: string } }) {
  // 데이터베이스나 파일 시스템에서 실제 번역 콘텐츠 가져오기
  const content = await getTranslatedContent(params.lang)

  return <div>{content}</div>
}

5.3 성능 최적화 권장 사항

다국어 웹사이트는 전 세계 사용자를 대상으로 하는 경우가 많으므로 성능 최적화가 특히 중요합니다.

1. CDN으로 여러 지역의 접속 속도 개선

// next.config.js
module.exports = {
  images: {
    domains: ['cdn.example.com'],
  },
  // 자동 압축 활성화
  compress: true,
}

2. 언어 팩 필요할 때 불러오기

모든 언어 번역 파일을 한 번에 불러오지 마세요.

// 현재 언어의 번역 파일을 동적으로 가져오기
const messages = await import(`@/locales/${lang}.json`)

3. 캐시 전략

페이지 캐시 시간을 적절하게 설정합니다.

// app/[lang]/layout.tsx
export const revalidate = 3600 // 1시간마다 재검증

5.4 모니터링과 유지보수

다국어 SEO는 한 번으로 끝나는 작업이 아니라 지속적인 모니터링과 최적화가 필요합니다.

1. hreflang 오류 정기 확인

Google Search Console의 ‘국제 타겟팅’ 보고서를 사용합니다.

  • hreflang 태그 오류와 경고 확인
  • 언어별 페이지의 색인 상태 확인
  • 언어별 검색 성과와 클릭률 모니터링

2. 권장 모니터링 도구

3. 모니터링 스크립트 만들기

hreflang 설정 검사를 자동화할 수 있습니다.

// scripts/check-hreflang.ts
import { JSDOM } from 'jsdom'

async function checkHreflang(url: string) {
  const response = await fetch(url)
  const html = await response.text()
  const dom = new JSDOM(html)
  const document = dom.window.document

  const hreflangLinks = document.querySelectorAll('link[rel="alternate"][hreflang]')

  console.log(`Found ${hreflangLinks.length} hreflang links on ${url}`)

  hreflangLinks.forEach(link => {
    const hreflang = link.getAttribute('hreflang')
    const href = link.getAttribute('href')
    console.log(`  ${hreflang}: ${href}`)
  })

  // 자기 참조가 있는지 확인
  const currentUrl = new URL(url).href
  const hasSelfReference = Array.from(hreflangLinks).some(
    link => link.getAttribute('href') === currentUrl
  )

  if (!hasSelfReference) {
    console.warn('⚠️ Warning: Missing self-reference hreflang tag')
  }

  // x-default가 있는지 확인
  const hasXDefault = Array.from(hreflangLinks).some(
    link => link.getAttribute('hreflang') === 'x-default'
  )

  if (!hasXDefault) {
    console.warn('⚠️ Warning: Missing x-default hreflang tag')
  }
}

// 사용 예
checkHreflang('https://example.com/en/about')
checkHreflang('https://example.com/zh/about')

6. 실전 사례: 전체 프로젝트 예제

Next.js App Router 프로젝트에서 다국어 SEO를 구현하는 전체 예제를 살펴보겠습니다.

6.1 프로젝트 구조

my-i18n-site/
├── app/
│   ├── [lang]/                    # 동적 언어 경로
│   │   ├── layout.tsx             # 언어 단위 레이아웃
│   │   ├── page.tsx               # 홈페이지
│   │   ├── about/
│   │   │   └── page.tsx           # 소개 페이지
│   │   └── blog/
│   │       ├── page.tsx           # 블로그 목록
│   │       └── [slug]/
│   │           └── page.tsx       # 블로그 글 상세 페이지
│   ├── sitemap.ts                 # Sitemap 생성기
│   └── robots.ts                  # Robots.txt 생성기
├── components/
│   └── I18nMetadata.tsx           # 다국어 메타데이터 컴포넌트
├── lib/
│   ├── i18n.ts                    # 국제화 설정
│   └── articles.ts                # 글 데이터 가져오기
├── locales/                       # 번역 파일
│   ├── en.json
│   ├── zh.json
│   └── ja.json
└── next.config.js                 # Next.js 설정

6.2 설정 파일

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 주의: App Router는 i18n 설정을 사용하지 않음
  // 언어 경로를 직접 구현해야 함
}

module.exports = nextConfig
// lib/i18n.ts
export const languages = ['en', 'zh', 'ja'] as const
export type Language = (typeof languages)[number]

export const defaultLanguage: Language = 'en'

export const languageNames: Record<Language, string> = {
  en: 'English',
  zh: '中文',
  ja: '日本語',
}

export function isValidLanguage(lang: string): lang is Language {
  return languages.includes(lang as Language)
}

6.3 Layout 컴포넌트

// app/[lang]/layout.tsx
import { languages, isValidLanguage } from '@/lib/i18n'
import { notFound } from 'next/navigation'

export async function generateStaticParams() {
  return languages.map(lang => ({ lang }))
}

export default function LangLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: { lang: string }
}) {
  // 언어 코드 유효성 검사
  if (!isValidLanguage(params.lang)) {
    notFound()
  }

  return (
    <html lang={params.lang}>
      <body>{children}</body>
    </html>
  )
}

6.4 Metadata를 포함한 페이지 컴포넌트

// app/[lang]/about/page.tsx
import { Metadata } from 'next'
import { languages, Language } from '@/lib/i18n'

type Props = {
  params: { lang: Language }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { lang } = params
  const baseUrl = 'https://example.com'
  const pathname = '/about'

  // alternates 설정 생성
  const alternates = {
    canonical: `${baseUrl}/${lang}${pathname}`,
    languages: languages.reduce((acc, locale) => {
      acc[locale] = `${baseUrl}/${locale}${pathname}`
      return acc
    }, {} as Record<string, string>)
  }

  // 언어별 제목과 설명 반환
  const titles: Record<Language, string> = {
    en: 'About Us - Learn More About Our Company',
    zh: '关于我们 - 了解更多关于我们公司的信息',
    ja: '私たちについて - 当社についてもっと知る',
  }

  const descriptions: Record<Language, string> = {
    en: 'Learn about our mission, values, and the team behind our success.',
    zh: '了解我们的使命、价值观以及我们成功背后的团队。',
    ja: '私たちの使命、価値観、そして成功を支えるチームについて学びます。',
  }

  return {
    title: titles[lang],
    description: descriptions[lang],
    alternates,
    openGraph: {
      title: titles[lang],
      description: descriptions[lang],
      url: `${baseUrl}/${lang}${pathname}`,
      siteName: 'Example Site',
      locale: lang,
      type: 'website',
    },
  }
}

export default function AboutPage({ params }: Props) {
  const content = {
    en: 'About us content in English...',
    zh: '关于我们的中文内容...',
    ja: '私たちについての日本語コンテンツ...',
  }

  return (
    <div>
      <h1>About Us</h1>
      <p>{content[params.lang]}</p>
    </div>
  )
}

6.5 hreflang을 포함한 동적 경로

// app/[lang]/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { languages, Language } from '@/lib/i18n'
import { getArticle, getAllArticles } from '@/lib/articles'
import { notFound } from 'next/navigation'

type Props = {
  params: { lang: Language; slug: string }
}

// 모든 글 페이지 정적 생성
export async function generateStaticParams() {
  const articles = await getAllArticles()

  return languages.flatMap(lang =>
    articles.map(article => ({
      lang,
      slug: article.slug,
    }))
  )
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { lang, slug } = params
  const article = await getArticle(slug, lang)

  if (!article) {
    return {}
  }

  const baseUrl = 'https://example.com'
  const pathname = `/blog/${slug}`

  const alternates = {
    canonical: `${baseUrl}/${lang}${pathname}`,
    languages: languages.reduce((acc, locale) => {
      acc[locale] = `${baseUrl}/${locale}${pathname}`
      return acc
    }, {} as Record<string, string>)
  }

  return {
    title: article.title,
    description: article.excerpt,
    alternates,
    openGraph: {
      title: article.title,
      description: article.excerpt,
      url: `${baseUrl}/${lang}${pathname}`,
      type: 'article',
      publishedTime: article.publishedAt,
      authors: [article.author],
    },
  }
}

export default async function BlogArticle({ params }: Props) {
  const { lang, slug } = params
  const article = await getArticle(slug, lang)

  if (!article) {
    notFound()
  }

  return (
    <article>
      <h1>{article.title}</h1>
      <p>{article.content}</p>
    </article>
  )
}

6.6 Sitemap 생성

// app/sitemap.ts
import { MetadataRoute } from 'next'
import { languages } from '@/lib/i18n'
import { getAllArticles } from '@/lib/articles'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://example.com'
  const sitemap: MetadataRoute.Sitemap = []

  // 1. 정적 페이지 추가
  const staticPages = ['', '/about', '/contact']
  staticPages.forEach(page => {
    languages.forEach(lang => {
      sitemap.push({
        url: `${baseUrl}/${lang}${page}`,
        lastModified: new Date(),
        changeFrequency: 'monthly',
        priority: page === '' ? 1 : 0.8,
        alternates: {
          languages: languages.reduce((acc, l) => {
            acc[l] = `${baseUrl}/${l}${page}`
            return acc
          }, {} as Record<string, string>)
        }
      })
    })
  })

  // 2. 동적 콘텐츠(블로그 글) 추가
  const articles = await getAllArticles()
  articles.forEach(article => {
    languages.forEach(lang => {
      sitemap.push({
        url: `${baseUrl}/${lang}/blog/${article.slug}`,
        lastModified: new Date(article.updatedAt),
        changeFrequency: 'weekly',
        priority: 0.6,
        alternates: {
          languages: languages.reduce((acc, l) => {
            acc[l] = `${baseUrl}/${l}/blog/${article.slug}`
            return acc
          }, {} as Record<string, string>)
        }
      })
    })
  })

  return sitemap
}

6.7 Robots.txt

// app/robots.ts
import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
    },
    sitemap: 'https://example.com/sitemap.xml',
  }
}

7. 검증과 테스트

7.1 로컬 테스트 체크리스트

프로덕션 환경에 배포하기 전에 다음 항목을 확인하세요.

  • 모든 페이지의 <html> 태그에 올바른 lang 속성이 있음
  • 모든 페이지에 전체 hreflang 태그가 있음(모든 언어 페이지 포함)
  • hreflang 태그에 자기 참조가 있음(현재 페이지가 자기 자신을 참조)
  • 기본 언어를 가리키는 x-default 태그가 있음
  • canonical 태그가 올바른 URL(현재 언어 페이지)을 가리킴
  • Sitemap에 모든 언어 페이지 URL이 있음
  • robots.txtsitemap.xml을 올바르게 가리킴
  • 언어별 콘텐츠의 품질이 일관되고 번역이 정확함

7.2 Google Rich Results Test 사용

Google Rich Results Test에 접속해 페이지를 테스트합니다.

  1. 페이지 URL을 입력합니다.
  2. Google이 페이지를 가져와 분석할 때까지 기다립니다.
  3. 오류나 경고가 있는지 확인합니다.
  4. hreflang 태그가 올바르게 인식되는지 확인합니다.

7.3 hreflang 검사 도구 사용

hreflang 전용 검증 도구는 다음과 같습니다.

이 도구로 할 수 있는 작업은 다음과 같습니다.

  • 여러 페이지의 hreflang 설정 일괄 검사
  • 비대칭 문제(단방향 참조) 발견
  • 잘못된 언어 코드 감지

7.4 Google Search Console 검증

프로덕션 환경에 배포한 뒤 다음 절차를 진행합니다.

  1. Google Search Console에 Sitemap을 제출합니다.
  2. Google이 초기 색인을 마칠 때까지 1~2주 기다립니다.
  3. ‘국제 타겟팅’ > ‘언어’ 보고서를 확인합니다.
  4. hreflang 오류와 경고가 있는지 확인합니다.
  5. 언어별 페이지의 검색 성과를 모니터링합니다.

8. 자주 묻는 질문

Q1: hreflang과 canonical은 어떻게 다른가요?

  • canonical - 중복 콘텐츠 문제를 처리하기 위해 페이지의 표준 URL을 검색 엔진에 알립니다.
  • hreflang - 언어 타겟팅을 위해 페이지에 어떤 언어 페이지가 있는지 검색 엔진에 알립니다.

두 태그는 서로 충돌하지 않으며 함께 사용할 수 있습니다. 각 언어 페이지의 canonical은 자기 자신을 가리키고, hreflang은 모든 언어 페이지를 가리켜야 합니다.

Q2: 모든 페이지에 hreflang을 설정해야 하나요?

네. hreflang 태그는 모든 언어 페이지에 있어야 하며 양방향으로 서로 참조해야 합니다. 영어 페이지에만 설정하고 중국어 페이지에는 설정하지 않으면 Google이 해당 설정을 무시합니다.

Q3: x-default는 어떤 언어를 가리켜야 하나요?

일반적으로 기본 언어나 가장 범용적인 페이지를 가리킵니다. 권장 전략은 다음과 같습니다.

  • 주요 독자가 영어 사용자라면 영어 페이지를 가리킵니다.
  • 전 세계를 대상으로 하는 웹사이트라면 국제 영어 페이지(en-US)를 가리킵니다.
  • 특정 지역을 대상으로 한다면 해당 지역의 주요 언어를 가리킵니다.

Q4: 하위 디렉터리와 하위 도메인 중 어느 쪽이 더 좋은가요?

하위 디렉터리(권장):

  • SEO 신호가 기본 도메인에 집중됩니다.
  • 구현과 유지보수가 간단합니다.
  • 대부분의 프로젝트에 적합합니다.

하위 도메인:

  • 여러 서버에 독립적으로 배포할 수 있습니다.
  • 시장별 독립 운영이 필요한 대형 국제 웹사이트에 적합합니다.
  • 추가 도메인 관리와 비용이 필요합니다.

결론: 특별한 요구가 없다면 하위 디렉터리 전략을 선택하세요.

Q5: 기계 번역 콘텐츠는 어떻게 처리해야 하나요?

SEO용 콘텐츠에 기계 번역 결과를 그대로 사용하는 것은 권장하지 않습니다.

  • 검색 엔진이 품질이 낮은 번역을 감지해 순위에 영향을 줄 수 있습니다.
  • 가치가 낮은 콘텐츠로 판단될 수 있습니다.
  • 사용자 경험이 나빠지고 이탈률이 높아집니다.

예산이 제한적이라면 다음과 같이 대응하세요.

  1. 핵심 페이지(홈페이지, 주요 제품 페이지, 트래픽이 많은 페이지)를 우선 번역합니다.
  2. 기계 번역을 사용했다면 반드시 사람이 검수하고 다듬습니다.
  3. 번역 품질을 단계적으로 개선하고 콘텐츠를 정기적으로 업데이트합니다.

Q6: 다국어 웹사이트가 색인되는 데 얼마나 걸리나요?

일반적인 일정은 다음과 같습니다.

  • Sitemap 제출 후 1~2주 안에 색인이 시작됩니다.
  • 전체 색인에는 1~2개월이 걸릴 수 있습니다.
  • SEO 신호가 쌓이는 데는 3~6개월이 필요합니다.

색인을 빠르게 하는 방법은 다음과 같습니다.

  • Sitemap 형식이 올바른지 확인하고 제때 제출합니다.
  • 콘텐츠 품질과 업데이트 빈도를 높입니다.
  • 품질이 높은 외부 링크를 확보합니다.
  • 중요한 페이지는 Google Search Console에서 색인을 요청합니다.

9. 마무리

다국어 SEO 최적화는 국제화 웹사이트의 성공을 좌우하는 핵심 요소입니다. 주요 내용을 정리해 보겠습니다.

9.1 핵심 사항

  1. URL 전략

    • 하위 디렉터리 전략(example.com/en/, example.com/zh/)을 권장합니다.
    • URL 구조를 명확하고 일관되며 이해하기 쉽게 유지합니다.
  2. hreflang 설정

    • 모든 페이지에 전체 hreflang 태그를 설정합니다(모든 언어 페이지 포함).
    • 자기 참조(현재 페이지가 자기 자신을 참조)를 반드시 포함합니다.
    • 기본 언어를 가리키는 x-default를 추가합니다.
    • ISO 639-1 표준에 맞는 올바른 언어 코드를 사용합니다.
  3. Sitemap

    • 모든 언어 페이지의 URL을 포함합니다.
    • Sitemap에도 hreflang 정보를 추가합니다(선택 사항이지만 권장).
    • 정기적으로 업데이트하고 검색 엔진에 제출합니다.
  4. 콘텐츠 품질

    • 기계 번역 결과를 그대로 게시하지 않습니다.
    • 언어별 콘텐츠의 일관성과 전문성을 유지합니다.
    • 단순 번역을 넘어 문화와 표현 습관을 고려해 현지화합니다.
  5. 모니터링과 유지보수

    • Google Search Console로 지속적으로 모니터링합니다.
    • hreflang 오류와 경고를 정기적으로 확인합니다.
    • 언어별 검색 성과와 전환율을 추적합니다.

9.2 실행 체크리스트

다음 단계를 완료해 다국어 SEO가 올바르게 설정되었는지 확인하세요.

  • URL 전략 선택 및 구현(하위 디렉터리 권장)
  • 모든 페이지에 전체 hreflang 태그 추가
  • 올바른 canonical 태그 설정
  • 모든 언어 페이지가 포함된 Sitemap 생성
  • robots.txt를 설정하고 sitemap.xml 연결
  • Google Search Console과 Bing Webmaster Tools에 Sitemap 제출
  • 검증 도구로 hreflang 설정 확인
  • 콘텐츠 번역 품질 확인 및 개선
  • 모니터링 절차 수립 및 정기 점검

9.3 더 읽어보기

다국어 SEO를 올바르게 구현하려면 시간과 노력이 들지만 더 나은 검색 순위, 더 정확한 사용자 연결, 더 높은 전환율이라는 큰 보상을 얻을 수 있습니다. 이 글의 모범 사례를 따르면 다국어 웹사이트가 검색 엔진에서 더 좋은 성과를 낼 수 있습니다.

Next.js 다국어 SEO 전체 설정 절차

hreflang 태그 설정부터 다국어 Sitemap 생성과 URL 전략 선택까지의 전체 단계

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: hreflang 태그 설정

    metadata에서 설정합니다.
    ```tsx
    // app/[locale]/about/page.tsx
    export async function generateMetadata({ params }): Promise<Metadata> {
    const { locale } = params

    return {
    title: 'About Us',
    alternates: {
    languages: {
    'zh': '/zh/about',
    'en': '/en/about',
    'x-default': '/en/about', // 기본 언어
    },
    },
    }
    }
    ```

    핵심 사항:
    • 모든 언어 페이지 포함
    • x-default는 기본 언어를 가리킴
    • 모든 페이지에 설정

    HTML 출력:
    ```html
    <link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
    <link rel="alternate" hreflang="en" href="https://example.com/en/about" />
    <link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
    ```

    역할:
    • 페이지의 대상 언어를 검색 엔진에 알림
    • 중복 콘텐츠 문제 방지
    • 사용자에게 적합한 언어 페이지 연결
  2. 2

    Step 2: 다국어 Sitemap 생성

    방법 1: 언어별 Sitemap 생성
    ```tsx
    // app/[locale]/sitemap.ts
    export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
    const baseUrl = 'https://example.com'
    const locale = params.locale

    return [
    {
    url: `${baseUrl}/${locale}`,
    lastModified: new Date(),
    changeFrequency: 'daily',
    priority: 1,
    },
    // ...
    ]
    }
    ```

    방법 2: Sitemap 인덱스 사용
    ```tsx
    // app/sitemap.ts
    export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
    const locales = ['zh', 'en']
    const baseUrl = 'https://example.com'

    return locales.flatMap(locale => [
    {
    url: `${baseUrl}/${locale}`,
    lastModified: new Date(),
    changeFrequency: 'daily',
    priority: 1,
    },
    // ...
    ])
    }
    ```

    핵심 사항:
    • 모든 언어 페이지 포함
    • 올바른 URL 형식 사용
    • Google Search Console에 제출
  3. 3

    Step 3: URL 전략 선택

    방안 1: 하위 경로(권장)
    • URL 형식: /zh/about, /en/about
    • 설정이 간단함
    • SEO에 유리함
    • 대부분의 프로젝트에 적합함

    방안 2: 하위 도메인
    • URL 형식: zh.example.com, en.example.com
    • 여러 도메인 설정 필요
    • 시장별 운영에 적합함
    • 대형 프로젝트에 적합함

    방안 3: Cookie
    • Cookie로 언어 전환
    • URL에 언어 접두사가 없음
    • SEO에 불리함
    • 권장하지 않음

    선택 기준:
    • 대부분의 프로젝트 → 하위 경로
    • 대형 프로젝트 → 하위 도메인
    • 피해야 할 방식 → Cookie

    핵심: 하위 경로 방식이 SEO에 가장 유리하므로 권장합니다.
  4. 4

    Step 4: 검증 및 테스트

    검증 도구:

    1. Google Search Console:
    • 다국어 Sitemap 제출
    • hreflang 태그 확인
    • 색인 상태 확인

    2. hreflang 테스트 도구:
    • https://www.aleydasolis.com/en/english-tools/international-seo-tools/hreflang-tags-validator/
    • hreflang 설정이 올바른지 확인

    3. 다국어 Sitemap 검증:
    • Sitemap 형식 확인
    • 모든 언어 페이지가 포함되었는지 확인
    • URL 정확성 검증

    흔한 오류 확인:
    • hreflang 태그 누락
    • 잘못된 x-default 설정
    • 일부 언어 페이지가 Sitemap에 누락됨
    • 일관되지 않은 URL 형식

    권장 사항: 문제가 생길 때까지 기다리지 말고 설정 직후 검증하세요.

FAQ

hreflang 태그란 무엇이며 왜 필요한가요?
hreflang은 페이지의 대상 언어와 지역을 검색 엔진에 알리는 HTML 속성입니다.

주요 역할:
1. 중복 콘텐츠 문제 방지 - 언어별 페이지가 중복이 아니라 같은 콘텐츠의 번역임을 검색 엔진에 알립니다.
2. 사용자와 정확히 연결 - 사용자의 언어 및 지역 설정에 맞는 페이지를 표시합니다.
3. 사용자 경험 개선 - 사용자가 잘못된 언어의 콘텐츠를 보지 않도록 합니다.

설정 방법:
```tsx
export async function generateMetadata({ params }): Promise<Metadata> {
return {
alternates: {
languages: {
'zh': '/zh/about',
'en': '/en/about',
'x-default': '/en/about',
},
},
}
}
```

핵심 사항:
• 모든 언어 페이지 포함
• x-default는 기본 언어를 가리킴
• 모든 페이지에 설정

Google 통계에 따르면 다국어 웹사이트의 60% 이상에 hreflang 설정 오류가 있습니다.
hreflang 태그는 어떻게 설정하나요?
metadata에서 설정합니다.
```tsx
// app/[locale]/about/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const { locale } = params

return {
title: 'About Us',
alternates: {
languages: {
'zh': '/zh/about',
'en': '/en/about',
'x-default': '/en/about', // 기본 언어
},
},
}
}
```

HTML 출력:
```html
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
```

핵심 사항:
• 모든 언어 페이지 포함
• x-default는 기본 언어를 가리킴
• 모든 페이지에 설정
• URL은 절대 경로여야 함

주의: hreflang 태그에는 현재 페이지를 포함한 모든 언어 페이지가 들어가야 합니다.
다국어 Sitemap은 어떻게 생성하나요?
방법 1: 언어별 Sitemap 생성
```tsx
// app/[locale]/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://example.com'
const locale = params.locale

return [
{
url: `${baseUrl}/${locale}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
]
}
```

방법 2: Sitemap 인덱스 사용
```tsx
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const locales = ['zh', 'en']
const baseUrl = 'https://example.com'

return locales.flatMap(locale => [
{
url: `${baseUrl}/${locale}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
])
}
```

핵심 사항:
• 모든 언어 페이지 포함
• 올바른 URL 형식 사용
• Google Search Console에 제출

권장 사항: 더 유연한 Sitemap 인덱스 방식을 사용하세요.
다국어 웹사이트의 URL 전략은 어떻게 선택하나요?
세 가지 URL 전략이 있습니다.

방안 1: 하위 경로(권장)
• URL 형식: /zh/about, /en/about
• 설정이 간단함
• SEO에 유리함
• 대부분의 프로젝트에 적합함

방안 2: 하위 도메인
• URL 형식: zh.example.com, en.example.com
• 여러 도메인 설정 필요
• 시장별 운영에 적합함
• 대형 프로젝트에 적합함

방안 3: Cookie
• Cookie로 언어 전환
• URL에 언어 접두사가 없음
• SEO에 불리함
• 권장하지 않음

선택 기준:
• 대부분의 프로젝트 → 하위 경로
• 대형 프로젝트 → 하위 도메인
• 피해야 할 방식 → Cookie

핵심: 하위 경로 방식이 SEO에 가장 유리하므로 권장합니다.

주의: URL 전략을 선택한 뒤에는 hreflang 태그의 URL 형식도 일관되게 유지해야 합니다.
다국어 SEO 설정은 어떻게 검증하나요?
검증 도구:

1. Google Search Console:
• 다국어 Sitemap 제출
• hreflang 태그 확인
• 색인 상태 확인

2. hreflang 테스트 도구:
• https://www.aleydasolis.com/en/english-tools/international-seo-tools/hreflang-tags-validator/
• hreflang 설정이 올바른지 확인

3. 다국어 Sitemap 검증:
• Sitemap 형식 확인
• 모든 언어 페이지가 포함되었는지 확인
• URL 정확성 검증

흔한 오류 확인:
• hreflang 태그 누락
• 잘못된 x-default 설정
• 일부 언어 페이지가 Sitemap에 누락됨
• 일관되지 않은 URL 형식

권장 사항:
• 설정 직후 검증
• 색인 상태 정기 확인
• 문제를 빠르게 수정

검증은 SEO 최적화의 중요한 단계이므로 생략하지 마세요.
다국어 SEO에서 흔히 발생하는 오류는 무엇인가요?
흔한 오류:

1. hreflang 태그 누락
• 검색 엔진이 페이지 언어를 알 수 없음
• 잘못된 언어 페이지가 표시될 수 있음

2. 잘못된 x-default 설정
• x-default를 설정하지 않음
• 또는 x-default가 잘못된 언어를 가리킴

3. Sitemap에 일부 언어 페이지가 없음
• 일부 언어 페이지만 제출함
• 검색 엔진이 모든 페이지를 찾을 수 없음

4. 일관되지 않은 URL 형식
• hreflang 태그의 URL 형식이 서로 다름
• 설정 오류로 이어짐

5. 중복 콘텐츠 문제
• hreflang을 올바르게 설정하지 않음
• 검색 엔진이 언어별 페이지를 중복 콘텐츠로 판단함

해결 방법:
• hreflang 태그 설정
• 모든 언어 페이지 포함
• 올바른 URL 형식 사용
• 완전한 Sitemap 제출

이 글의 모범 사례를 따라 이러한 흔한 오류를 피하세요.

10분 읽기 · 게시일: 2025년 12월 25일 · 수정일: 2026년 9월 8일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog