React

[NEXTjs] 다국어 페이지 설정하기 (next-i18next)

zubetcha 2022. 5. 6. 14:25

 

 

Locale이란?

 

로케일은 사용자 인터페이스에 사용하기 위해 언어, 지역, 출력 방식 등을 정의한 문자열을 말한다. 로케일에 대한 자세한 설명은 이 포스팅에서 볼 수 있다!

 

 

로케일(Locale)이란? 국제화(internationalization)와 지역화(localization)

 

로케일(Locale)이란? 국제화(internationalization)와 지역화(localization)

로케일(Locale)이란? 로케일이란 사용자 인터페이스(UI)에서 사용되는 언어(ISO 639-1 codes 표준 형식), 지역 설정(ISO 3166-1 표준 형식), 출력 방식(Character Set 또는 ISO 8859-1, UTF-8 등의 인코딩 식별자..

zubetcha.tistory.com

 

Nextjs의 Internationalized Routing

 

Nextjs는 v10.0.0부터 i18n(internationlization,국제화)를 위한 라우팅을 지원하고 있다. config 설정 방법은 간단하다. next.config.js에 설정하고 싶은 로케일과 도메인 관련 정보들을 설정하면 원하게끔 라우팅이 동작하도록 할 수 있다.

 

// next.config.js
module.exports = {
  i18n: {
  	// 프로젝트에 지원하고 싶은 모든 로케일
    locales: ['ko', 'en-US', 'ja'],
    
    // 도메인에 로케일 prefix가 붙지 않는 기본 로케일
    defaultLocale: 'ko',
    
    // 도메인을 prefix가 아닌 서브도메인으로 핸들링하고 싶은 경우 아래와 같이
    // 각각의 로케일을 defaultLocale과 도메인을 설정한다.
    domains: [
      {
        domain: 'example.com',
        defaultLocale: 'en-US',
      },
      {
        domain: 'example.nl',
        defaultLocale: 'nl-NL',
      },
      {
        domain: 'example.fr',
        defaultLocale: 'fr',
      },
    ],
  },
}

 

라우팅 처리 방법

 

NEXTjs에서 지원하는 라우팅 처리 방법은 Sub-path 라우팅, Domain 라우팅 두 가지가 있다.

 

Sub-path 라우팅

Sub-path 라우팅은 URL에 로케일이 prefix로 붙는 방식이다. 아래와 같이 domains 프로퍼티를 제외하고 next.config.js를 설정하면 defaultLocale을 제외한 나머지 로케일은 pages 뎁스 전에 prefix로 붙은 형태의 URL로 라우팅시킬 수 있다.

 

// next.config.js
module.exports = {
  i18n: {
    locales: ['ko', 'en-US', 'ja'],
    defaultLocale: 'ko',
  },
}

 

▪︎ ko: `/pages `

▪︎ en-US: `/en-US/pages`

▪︎ ja: `/ja/pages`

 

Domain 라우팅

Domain 라우팅은 locale에 따라 라우팅을 처리할 때 완전히 다른 도메인으로 라우팅시키는 방식이다. 도메인 라우팅 방식을 취하고 싶다면 next.config.js에 아래와 같이 domains 프로퍼티까지 추가로 설정해주면 된다.

 

다만, 주의할 점은 로케일 라우팅에 사용할 서브도메인은 반드시 기본 도메인과 일치하는 도메인 값을 가지고 있어야 한다는 것이다. 예를 들어, defaultLocale에 사용할 도메인이 example.com 이라면, 로케일 라우팅에 사용할 서브도메인도 반드시 example을 포함하고 있어야 한다.

 

// next.config.js
module.exports = {
  i18n: {
    locales: ['ko', 'en-US', 'ja'],
    defaultLocale: 'ko',

    domains: [
      {
        domain: 'example.com',
        defaultLocale: 'ko',
      },
      {
        domain: 'en-US.example.com',
        defaultLocale: 'en-US',
      },
      {
        domain: 'ja.example.com',
        defaultLocale: 'ja',
      },
    ],
  },
}

 

▪︎ ko: `example.com/pages`, `www.example.com/pages`

▪︎ en-US: `en-US.example.com/pages`

▪︎ ja: `ja.example.com/pages`

 

accept-language 무시하기

 

만약 헤더에 Accept-Language가 설정되어 있다면 Nextjs는 자동으로 이를 감지하여 해당 로케일과 일치하는 도메인 또는 sub path로 리다이렉트 시킨다. 만약 리다이렉트 시키지 않고 무조건 우리가 원하는 로케일의 화면으로 보여주기 위해 이 Accept-Language에 설정되어 있는 로케일을 무시하고 싶다면, 아래와 같이 localeDetection 값을 false로 설정해주면 된다.

 

// next.config.js
module.exports = {
  i18n: {
    localeDetection: false,
  },
}

 

로케일 정보 접근하기

 

useRouter()를 사용하는 경우

useRouter() 훅이 반환하는 프로퍼티 중 locale, locales, defaultLocale을 통해 로케일 정보에 접근할 수 있다.

 

- locale: 현재 활성화되어 있는 로케일 정보

- locales: config에 설정한 모든 로케일 정보

- defaultLocale: config에 설정한 defaultLocale 정보

 

getStaticProps 또는 getServerSideProps를 사용하는 경우

getStaticProps 또는 getServerSideProps를 이용해 프리렌더링을 하고 있는 경우 해당 함수가 제공하는 컨텍스트에서 로케일 정보를 얻을 수 있다.

 

로케일 전환하기

 

Nextjs에서 로케일 간 전환하는 방법은 next/link를 사용하는 방법과 next/router를 사용하는 방법이 있다. 

 

next/link

만약 페이지 라우팅에 next/link의 Link 태그를 사용하고 있다면, 현재 로케일에서 다른 로케일로 전환하고 싶을 때 Link 태그에 locale 프로퍼티를 추가로 설정해주면 된다. 만약 locale 프로퍼티를 설정하지 않으면 현재 활성화되어 있는 로케일이 계속 유지된다.

 

import Link from 'next/link'

export default function IndexPage(props) {
  return (
    <Link href="/another" locale="ja">
      <a>To /ja/another</a>
    </Link>
  )
}

 

next/router

next/router를 사용하는 경우 라우팅 옵션에 locale을 설정함으로써 로케일을 전환시킬 수 있다. 

 

import { useRouter } from 'next/router'

export default function IndexPage(props) {
  const router = useRouter()

  return (
    <div
      onClick={() => {
        router.push('/another', '/another', { locale: 'ja' })
      }}
    >
      to /ja/another
    </div>
  )
}

 

만약 기존의 라우팅 정보를 모두 보존하면서 locale만 바꾸고 싶다면, 아래와 같이 useRouter()가 반환하는 pathname, asPath, query 정보를 이용해 href 파라미터를 제공하면 된다.

 

import { useRouter } from 'next/router'
const router = useRouter()
const { pathname, asPath, query } = router
router.push({ pathname, query }, asPath, { locale: nextLocale })

 

라이브러리: next-i18next

 

Nextjs는 로케일 간의 라우팅을 편하게 설정할 수 있도록 지원하지만, 로케일에 맞는 번역 기능을 따로 지원하고 있지는 않다. 번역 파일을 관리하고 각 로케일에 해당하는 번역 페이지를 보여주기 위해서는 별도의 라이브러리 설치가 필요하다. 

 

next-i18next는 React용 번역 라이브러리로 유명한 react-i18next를 기반으로 만든 Nextjs 애플리케이션 용 번역 라이브러리이다. 

 

0. next-i18next 설치

yarn add next-i18next
// or
npm install next-i18next

 

1. 환경 설정

 

우선 루트 디렉토리에 next-i18next.config.js 파일을 생성한 후 아래와 같이 locales와 defaultLocale을 설정한다.

 

module.exports = {
  i18n: {
    defaultLocale: 'ko',
    locales: ['ko', 'en-US', 'ja'],
  },
};

 

next-i18next.config.js에서 export하고 있는 i18n 모듈을 next.config.js에서 불러와서 아래와 같이 추가한다.

 

const { i18n } = require('./next-i18next.config');

module.exports = {
  i18n,
};

 

2. 디렉토리 구조

 

다국어 페이지를 지원하기 위해서는 우선 각 locale에 해당하는 json 형태의 번역 파일이 필요하다. 디렉토리 구조는 반드시 아래와 같은 형태를 따라야 한다. locales 하위 폴더는 next-i18next.config.js에 설정한 로케일 이름으로 만든다.

 

.
└── public
    └── locales
        ├── ko
        |   └── common.json
        └── en-US
        |   └── common.json
        └── ja
            └── common.json

 

3. 번역에 필요한 세 가지 훅

 

위의 디렉토리 구조를 보면 locales 하위 디렉토리에는 우리가 설정한 locale 이름의 디렉토리가 있고 각 locale 디렉토리 안에 동일한 이름의 json 파일이 존재한다. next-i18next에서 제공하는 몇 가지 훅을 사용하면 번역이 필요한 페이지에서 현재 활성화되어 있는 로케일에 맞는 번역 파일에 접근해 해당 콘텐츠를 불러올 수 있다.

 

appWithTranslation

appWithTranslation은 context provider같은 역할을 한다. 밑에서 살펴볼 실제로 페이지를 번역하는 데 필요한 함수인 useTranslation을 전역에서 사용할 수 있도록 해준다. 그렇기 때문에 이 훅은 최상위 컴포넌트인 _app.tsx에서 전체 앱을 감싸는 형태로 사용된다.

 

// _app.tsx

import { appWithTranslation } from 'next-i18next';

const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />;

export default appWithTranslation(MyApp);

 

serverSideTranslations

serverSideTranslations는 페이지 레벨의 컴포넌트에서 사용되는 비동기 훅이다. public > locales 디렉토리에 있는 번역 컨텐츠와 next-i18next.config.js에 있는 로케일 관련 환경설정 옵션들을 페이지에 내려주는 역할을 한다. 아래와 같이 getStaticProps(또는 getServerSideProps) 안에서 로케일 정보와 번역 컨텐츠를 비동기로 호출하여 해당 페이지에 props로 넘겨준다.

 

// page level components

import { serverSideTranslations } from 'next-i18next/serverSideTranslations';

export async function getStaticProps({ locale }) {
  return {
    props: {
      ...(await serverSideTranslations(locale, ['common', 'footer'])),
    },
  };
}

 

serverSideTranslations를 사용하기에 앞서 몇 가지 주의사항이 있으니 참고하도록 하자.

 

- serverSideTranslations는 반드시 next-i18next/serverSideTranslations에서 import 해야 한다.

- serverSideTranslations는 getInitialProps와는 함께 사용할 수 없다. serverSideTranslations는 서버 환경에서 실행되지만 getInitialProps는 클라이언트 사이드에서 호출되기 때문이다.

 

useTranslation

useTranslation은 실제로 페이지를 번역하기 위해 번역 컨텐츠를 불러와주는 훅이다. useTranslation 파라미터에는 번역에 사용하고자 하는 json 파일의 이름을 string으로 넘겨주고, t함수의 파라미터에는 해당 json 파일의 각각의 프로퍼티를 넘겨줘서 값을 불러온다. 이렇게 하면 자동으로 현재 로케일에 맞춰 해당 로케일 디렉토리 하위의 json 파일에 접근하여 실제로 다국어 페이지를 유저에게 제공할 수 있게 된다.

 

import { useTranslation } from 'next-i18next';

export const Footer = () => {
  const { t } = useTranslation('footer');

  return (
    <footer>
      <p>{t('description')}</p>
    </footer>
  );
};

 

사용해보기!

 

나는 defaultLocale로 한국어를 설정하고, 그 외의 locale에 영어와 일본어를 설정했다. (일본의 locale은 jp가 아닌 ja이다. 내가 잘못 씀..config에 설정한 locale과 일치하면 동작하는 데 문제는 없지만 웬만하면 맞추는 게 좋을 것 같다.)

 

 

각 locale 디렉토리 하위에 위치하고 있는 common.json에는 같은 내용이 각각의 언어로 번역되어 있다.

 

// ko/common.json

{
  "title": "안녕하세요 👋🏻",
  "question": "어떤 걸 사용해 볼까요?",
  "button": {
    "link": "Link ⚡️",
    "router": "Router ✨"
  }
}
// en/common.json

{
  "title": "Hello 👋🏻",
  "question": "Click each button!",
  "button": {
    "link": "Link ⚡️",
    "router": "Router ✨"
  }
}
// jp/common.json

{
  "title": "こんにちは 👋🏻",
  "question": "どれを使ってみましょうか",
  "button": {
    "link": "リンク ⚡️",
    "router": "ルーター ✨"
  }
}

 

그리고 next/link와 next/router를 이용해 버튼을 클릭하면 각각 영어, 일본어로 번역된 페이지로 라우팅되도록 만들어 보았다.

import { useTranslation } from "next-i18next"
import Link from "next/link"
import { useRouter } from "next/router"

export const LocalBox = () => {
    const { i18n, t } = useTranslation("common");
    const router = useRouter();
    const { pathname, asPath, query } = router;

    return (
        <>
            <LocalBoxWrapper>
                <h1>{t("title")}</h1>
                <div className="navigate">
                    <p>{t("question")}</p>
                    <div className="button-wrapper">
                        <Link href={"/local"} locale="en">
                            <a  className="navigate-button">
                            {t("button.link")}
                            </a>
                            </Link>
                        <button className="navigate-button" onClick={() => {
                            router.push({ pathname, query }, asPath, { locale: "jp" })
                        }}>{t("button.router")}</button>
                    </div>
                </div>
            </LocalBoxWrapper>
        </>
    )
}

 

defaultLocale을 ko로 설정했기 때문에 url에는 아무런 sub-path나 locale 정보가 보이지 않는다. 그리고 자동으로 ko/common.json의 컨텐츠를 보여주고 있다.

 

 

en 로케일로 라우팅되도록 처리한 Link버튼을 클릭하면 url에 en이 루트 도메인 prefiex로 붙는 sub-path가 되는 걸 확인할 수 있다. 또한 마찬가지로 자동으로 t함수가 en/common.json의 컨텐츠를 불러와서 보여준다.

 

jp 로케일로 라우팅되도록 처리한 Router버튼을 클릭하면 마찬가지로 루트 도메인에 jp가 prefix로 붙으며, jp/common.json의 각 프로퍼티의 값을 보여준다.

 

 

 


마치며

처음에 방법을 모를 때는 다국어 페이지를 만드는 게 어려울 거라고 생각했는데, 막상 해보니 다국어를 지원하도록 환경을 설정하는 것 자체는 라이브러리 덕분에 굉장히 간편하다는 생각이 들었다. 다만 각 언어에 해당하는 번역 파일을 만들고, 번역이 필요한 모든 컴포넌트마다 t함수를 넣어주는 데 공수가 많이 들 것 같다..!