Logo
Published on
·14 min read

Next.js - 블로그 다국어(i18n)로 만들기 Part 4

개요

Next.js - 블로그 다국어(i18n)로 만들기 Part 3 포스팅에서 rss, sitemap 을 수정했습니다.

내용이 길어져 Part 1 ~ 4 로 나누어서 작성합니다.

Part 4에서는 html lang 설정과, 추가된 언어에 대한 alternate 설정을 추가합니다.


gitHub, i18n-tailwind-nextjs-starter-blog를 참고하여 만들었습니다.

Tailwind CSS Blog Starter 템플릿 사용을 결정하셨고, 프로젝트를 처음 시작한다면 위 프로젝트로 시작하는 것도 좋을 것 같습니다.

제 경우 이미 프로젝트가 진행 중이라서, 위 프로젝트를 참고하여 진행했습니다.

html lang

페이지를 소스보기로 확인해보면 설정된 언어와 상관없이 아래와 같이 고정되어있습니다.

<html lang="en" class="scroll-smooth">

이 부분을 언어에 따라 자동으로 변경되도록 하겠습니다.

pages/_document.js

  • 파일을 열어보면 <Html lang="en">로 코드가 고정되어있습니다. lang=".." 부분을 제거합니다.
class MyDocument extends Document {
  render() {
    return (
      <Html className="scroll-smooth"> // lang="en" 삭제
        // ...
      </Html>
    )
  }
}

export default MyDocument

제거 후 다시 소스보기로 확인해보면 변경된 언어에 맞게 lang=".."가 자동으로 추가되는 것을 확인할 수 있습니다.

이것이 가능한 이유는 아래와 같습니다.

Search Engine Optimization

Since Next.js knows what language the user is visiting it will automatically add the lang attribute to the <html> tag.

해석: Next.js는 사용자가 어떤 언어로 방문했는지 알고 있으므로 자동으로 <html> 태그에 lang 속성을 추가합니다.

이에 대한 자세한 내용은 Next.js- Internationalization (i18n) Routing: Search Engine Optimization 부분을 참고하시면 됩니다.

siteMetadata

data/siteMetadata.js

  • siteMetadata에 설정된 text를 언어별로 다르게 표시할 경우 아래 코드의 description 항목처럼 언어별로 구분하여 설정합니다.
const siteMetadata = {
  title: 'HLog',
  author: 'hlog',
  headerTitle: 'HLog',
  description: {
    ko: '생활 정보, 개발 정보 등 여러가지 관심사를 다루는 블로그입니다.',
    en: 'This is a blog that covers various interests such as lifestyle information, development updates, and more.',
  },
  // ...
}
  • siteMetadata.description 항목을 사용하는 모든 곳에 locale을 전달받아 표시하도록 수정합니다.

  • pages/index.js 페이지 코드를 예로 들어보겠습니다.

export async function getStaticProps({ locale, defaultLocale, locales }) {
  const posts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales)

  return { props: { posts, locale } } // locale 추가
}

export default function Home({ posts, locale }) { // locale 추가
  return (
    <>
      <PageSEO title={siteMetadata.title} description={siteMetadata.description[locale]} /> // locale 추가
      <div className="...">
        <div className="...">
          <p className="...">
            {siteMetadata.description[locale]} // locale 추가
          </p>
        </div>
      </div>
      // ...
    </>
  )
}

alternate

여러 언어를 사용하는 페이지가 있을 경우 alternate를 사용하여 사용자에게 알려줄 수 있습니다.

pages/_document.js

아래와 같이 alternate 코드를 제거합니다. (여기서 제거하고 CommonSEO 컴포넌트에서 언어에 맞게 추가되도록 위치를 변경합니다.)

{/*<link rel="alternate" type="application/rss+xml" href="/feed.xml" />*/}

components/SEO.js

  • 사용 가능한 언어 목록을 availableLocales 파라미터로 받도록 공통으로 사용하는 SEO 컴포넌트를 수정합니다.

  • CommonSEO 수정

const CommonSEO = ({
  // ...
  availableLocales, // availableLocales 추가
}) => {
  const router = useRouter()
  return (
    <Head>
      // ...
      // 사용 가능한 언어 목록에 대한 `alternate` 태그를 추가
      {availableLocales &&
        availableLocales.length > 1 &&
        availableLocales.map((locale) => (
          <link
            rel="alternate"
            hrefLang={locale}
            href={`${siteMetadata.siteUrl}${locale === router.defaultLocale ? '' : `/${locale}`}${
              router.asPath
            }`}
            key={locale}
          />
        ))}
      <link
        rel="alternate"
        type="application/rss+xml"
        href={`/feed${router.locale === router.defaultLocale ? '' : `.${router.locale}`}.xml`}
      />
    </Head>
  )
}
  • PageSEO 수정
export const PageSEO = ({ title, description, availableLocales }) => { // availableLocales 추가
  // ...
  return (
    <CommonSEO
      // ...
      availableLocales={availableLocales} // availableLocales 추가
    />
  )
}
  • TagSEO 수정
export const TagSEO = ({ title, description, availableLocales }) => { // availableLocales 추가
  // ...
  return (
    <>
      <CommonSEO
        // ...
        availableLocales={availableLocales} // availableLocales 추가
      />
      <Head>
        <link
          // ...
          // 현재 언어에 맞는 feed 주소를 설정
          href={`${siteMetadata.siteUrl}${router.asPath}/feed${
            router.locale === router.defaultLocale ? '' : `.${router.locale}`
          }.xml`}
        />
      </Head>
    </>
  )
}
  • BlogSEO 수정
export const BlogSEO = ({
  // ...
  availableLocales, // availableLocales 추가
}) => {
  // ...

  return (
    <>
      <CommonSEO
        // ...
        availableLocales={availableLocales} // availableLocales 추가
      />
      // ...
    </>
  )
}

PageSEO 사용 페이지들 수정

  • PageSEO 컴포넌트를 사용하는 페이지들에 availableLocales 파라미터를 전달합니다.

  • pages/blog.js를 예로 들면 아래와 같습니다.

// ...

export async function getStaticProps({ locale, defaultLocale, locales }) {
  // ...

  return {
    props: {
      // ...
      availableLocales: locales, // availableLocales 추가
    },
  }
}

export default function Blog({ posts, initialDisplayPosts, pagination, locale, availableLocales }) { // availableLocales 추가

  return (
    <>
      <PageSEO
        // ...
        availableLocales={availableLocales} // availableLocales 추가
      />
      // ...
    </>
  )
}

pages/tags/[tag].js

  • tag의 경우 동적으로 생성되는 페이지이므로, 해당 태그에 대해 사용 가능한 언어 목록을 찾아야합니다.
// ...

export async function getStaticProps({ params, locale, defaultLocale, locales }) {
  // ...

  // 사용 가능한 언어 목록을 알아냅니다.
  const availableLocales = []
  await Promise.all(
    locales.map(async (local) => {
      const tags = await getAllTags('blog', local, defaultLocale, locales)
      if (tags[params.tag] !== undefined) availableLocales.push(local)
    })
  )

  // ...

  return { props: { posts: filteredPosts, tag: params.tag, availableLocales } } // availableLocales 추가
}

export default function Tag({ posts, tag, availableLocales }) { // availableLocales 추가
  // ...
  return (
    <>
      <TagSEO
        // ...
        availableLocales={availableLocales} // availableLocales 추가
      />
      <ListLayout posts={posts} title={title} />
    </>
  )
}

pages/blog/[...slug].js

  • 블로그 글의 경우 동적으로 생성되는 페이지이므로 해당 글에 대한 사용 가능한 언어 목록을 찾아야합니다.

사용 가능한 언어를 필터링하는 함수를 아래와 같이 추가했습니다.

  • lib/mdx.js 수정 : getAvailableLocalesBySlug 함수 추가
export async function getAvailableLocalesBySlug(
  type,
  slug,
  locale = null,
  defaultLocale = null,
  locales = null
) {
  return locales
    .map((locale) => {
      const localeExtention = locale === defaultLocale ? '' : `.${locale}`
      const mdxPath = path.join(root, 'data', type, `${slug}${localeExtention}.mdx`)
      const mdPath = path.join(root, 'data', type, `${slug}${localeExtention}.md`)
      if (fs.existsSync(mdxPath) || fs.existsSync(mdPath)) {
        return locale
      }
    })
    .filter((locale) => locale !== undefined)
}
  • pages/blog/[...slug].js 수정
import {
  getAvailableLocalesBySlug, // getAvailableLocalesBySlug 추가
  // ...
} from '@/lib/mdx'
// ...

export async function getStaticProps({ params, locale, defaultLocale, locales }) {

  // ...

  // 사용 가능한 언어 필터링 함수 호출
  const availableLocales = await getAvailableLocalesBySlug(
    'blog',
    params.slug.join('/'),
    locale,
    defaultLocale,
    locales
  )
  // ...

  return {
    props: {
      // ...
      availableLocales, // availableLocales 추가
    },
  }
}

export default function Blog({ post, authorDetails, prev, next, relatedPosts, availableLocales }) { // availableLocales 추가
  // ...

  return (
    <>
      {frontMatter.draft !== true ? (
        <MDXLayoutRenderer
          // ...
          availableLocales={availableLocales} // availableLocales 추가
        />
      ) : (
        <div className="mt-24 text-center">
          // ...
        </div>
      )}
    </>
  )
}
  • MDXLayoutRenderer에서 사용하는 layout 컴포넌트들을 수정합니다.
    • layouts/PostLayout.js 를 예로 들면 아래와 같습니다.
// ...

export default function PostLayout({
  // ...
  availableLocales, // availableLocales 추가
}) {
  // ...

  return (
    <SectionContainer>
      <BlogSEO
        // ...
        availableLocales={availableLocales} // availableLocales 추가
      />

      // ...
    </SectionContainer>
  )
}

날짜 형식 변경

  • toLocaleDateString 호출 시 siteMetadata.locale 대신 locale을 사용하도록 수정합니다.

  • lib/utils/formatDate.js 를 예로 들겠습니다.

// ...
const formatDate = (date, locale) => { // locale 추가
  const options = {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }
  const now = new Date(date).toLocaleDateString(locale ? locale : siteMetadata.locale, options) // locale로 변경

  return now
}

export default formatDate
  • layouts/PostLayout.js 도 추가로 예로 들겠습니다.
// ...
import { useRouter } from 'next/router' // useRouter 추가

// ...

export default function PostLayout({ // ...
}) {
  const { slug, fileName, date, title, images, tags } = frontMatter
  const { locale } = useRouter() // locale 추가

  return (
    <SectionContainer>
      // ...
      <ScrollTopAndComment />
      <article>
        <div className="...">
          <header className="...">
            <div className="...">
              <dl className="...">
                <div>
                  <dt className="...">Published on</dt>
                  <dd className="...">
                    <time dateTime={date}>
                      {new Date(date).toLocaleDateString(locale, postDateTemplate)} // locale 로 변경
                    </time>
                  </dd>
                </div>
              </dl>
              // ...
            </div>
          </header>
          // ...
        </div>
      </article>
    </SectionContainer>
  )
}

정리

이제 모든(?) 작업이 끝났습니다.

Next.js를 Tailwind CSS Blog Starter 템플릿으로 처음 접하고 어떻게 동작하는 지도 모르고 쓰고 있었는데 다국어 처리를 하며 어떻게 동작하고 있었는 지 조금은 파악할 수 있게 되었습니다.

꼭 Tailwind CSS Blog Starter 템플릿이 아니더라도 다국어 처리를 위해 어떤 방식으로 필터링하고, path를 생성하는 지 파악하면 다른 프로젝트에도 적용할 수 있을 것 같습니다.

코드

코드는 multilingual-i18n-blog 브랜치에서 확인하실 수 있습니다.

위 GitHub 소스는 현재 블로그와 완전히 동일하지는 않지만, 글에 대한 이해를 돕기 위해 따로 작성한 Git 소스로 참고하시면 될 것 같습니다.

모두의 구글 애널리틱스4:GA4로 하는 디지털 마케팅 데이터 분석, 길벗  아무나 쉽게 따라하는 블로그 마케팅:검색 상위노출을 위한 블로그 마케팅의 모든 것, 페이스메이커  트래픽을 쓸어 담는 검색엔진 최적화:검색엔진이 가장 좋아하는 사이트 만들기, e비즈북스  실전에서 바로 쓰는 Next.js:SSR부터 SEO 배포까지 확장성 높은 풀스택 서비스 구축 가이드, 한빛미디어
(위 링크는 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.)