Next.js Link 태그의 locale 동작 방식 파헤쳐보기

dongwon·2023년 3월 26일
1

파헤치게된 계기

현재 저는 KR, JP, US 세 나라를 대상으로 각각 서비스를 개발하고 있고 그중 US를 Global 서비스로 전환하는 작업을 진행하고 있습니다. Global 서비스를 하면서 자연스럽게 해당 나라의 언어 노출에 대해 고민하게 됐는데요, 저희 팀은 도메인 뒤 [lang] 슬러그를 이용해 언어 토큰을 판별(MDN 참고), 자체 i18n dictionary를 구축해 노출하는 전략을 세웠습니다.

이와 같은 전략에서, 페이지 이동 간 [lang] 슬러그가 따라붙어야 해당 지역의 언어를 유지할 수 있는 구조가 됩니다. 이 기능을 페이지 이동 간 사용하는 태그의
locale props를 이용해 자동으로 설정을 할 수 있는데, (href props의 url path를 조작하거나) 어째서인지 이 기능이 정상 작동을 하지 않아 파헤쳐 보며 원인을 파악한 과정을 공유해 보려 합니다.

현재 사용하는 Next.js의 버전은 13.2.4, Beta 버전인 App directory를 사용하고 있습니다.

파악하기 전 저는 버그의 원인을 Beta 버전인 App directory로 의심을 하고 들어갔습니다. 기존 page directory를 사용하는 KR 서비스에서는 잘 작동하기에..

간단한 Next.js 실행, 테스트 방법

서버 실행

  1. repo clone
    gh repo clone vercel/next.js -- --depth=3000 --branch canary --single-branch
  2. dependency install
    pnpm install
  3. server start
    pnpm dev

contribute참고

예제 실행, 테스트

코드들이 술술 읽히지만.. 개인적으로 결과값을 직접 봐야 하는 성격이라 테스트할 모듈을 사용하는 서버가 필요했습니다. npm link를 이용해 개발하고 있는 서버에 연결시킬 수도 있지만,
Link만 보면 되기 때문에 Link를 사용하는 간단한 Example을 이용했습니다.

실행 방법은 run example 참고.

Link.js

Link가 실제로 구현되어 있는 파일에 들어가 봅니다. 코드 위치

directory config 체크

가장 먼저 현재 directory config를 체크했습니다. 변수명과 주석을 바탕으로 isAppRouter를 이용해 판별하는 걸 확인했습니다.

// packages/next/dist/client/link
const Link = _react.default.forwardRef(function LinkComponent(props, forwardedRef) {
  // 중략...
  const pagesRouter = _react.default.useContext(_routerContext.RouterContext);
  const appRouter = _react.default.useContext(_appRouterContext.AppRouterContext);
  const router = pagesRouter != null ? pagesRouter : appRouter;
  // We're in the app directory if there is no pages router.
  const isAppRouter = !pagesRouter;

  // 중략...
});

Link와 관련된 이벤트를 childProps로 제공하고 있습니다. click 했을 때 linkClicked 함수에서 처리하고 있습니다.
(다른 주제지만 hover 시 prefetch 되는 내부 로직이 굉장히 궁금하네요.)

const childProps = {
  onClick(e) {
    // 중략...
    linkClicked(e, router, href, as, replace, shallow, scroll, locale, isAppRouter, prefetchEnabled);
  },
  onMouseEnter(e) { prefetch(...) },
  onTouchStart(e) { prefetch(...) },
  // 중략...
};

linkClicked

Link 태그의 페이지 이동은 흔히 사용하는 useRouter를 이용하고 있었군요. Link 태그에서 props를 받아 조합 후, replace나 push 하는 걸 확인할 수 있었습니다. (때문에 router.push에 locale 속성을 추가해 똑같이 제어할 수 있습니다.)
여기까지 ₩isAppRouter₩를 이용해 분기로 처리하는 로직이 보이지만, 같은 navigate 함수를 사용하는 거 보니 아직까지 큰 차이점은 없는 것 같습니다. router에서 버그의 원인을 찾을 수 있을 것 같네요!

function linkClicked(e, router, href, locale, isAppRouter ...) {
  // 중략..
  const navigate = () => {
    // If the router is an NextRouter instance it will have `beforePopState`
    if ('beforePopState' in router) {
      router[replace ? 'replace' : 'push'](href, as, {
        shallow,
        locale,
        scroll,
      });
    } else {
      router[replace ? 'replace' : 'push'](as || href, {
        forceOptimisticNavigation: !prefetchEnabled,
      });
    }
  };

  if (isAppRouter) {
    // @ts-expect-error startTransition exists.
    _react.default.startTransition(navigate);
  } else {
    navigate();
  }
}

router.js

코드 위치

Router 클래스의 push 메서드에서 전달받은 url을 체크해 봅니다. 예상대로라면 Link 태그의 href와, locale이 조합된 url을 예상합니다.

class Router {
  push(url, as, options = {}) {
    console.log(url);
    // 예상 url: localhost:3000/en/mypage
    // 실제 url: localhost:3000/mypage
    return this.change('pushState', url, as, options);
  }
}

전달받은 url이 왜 다를까요? constructor를 확인해 봤습니다. 보다 보면 좀 서늘한 env 변수가 보이는데..
process.env.__NEXT_I18N_SUPPORT 유무에 따라 설정값undefined로 설정하는 것을 확인할 수 있습니다.

class Router {
  constructor(...) {
    // 중략..
    this.state = {
      locale: process.env.__NEXT_I18N_SUPPORT ? locale : undefined,
    };
  }
}

그럼 process.env.__NEXT_I18N_SUPPORT는 뭘까.. webpack을 뒤져보면 next.config.js에 설정하는 i18n config 임을 확인할 수 있습니다.

// webpack-config.js
export function getDefineEnv(
    // 중략..
    'process.env.__NEXT_I18N_SUPPORT': JSON.stringify(!!config.i18n),
    // 중략..
)

Example next.config.js에 i18n 설정 후 url 정보를 다시 확인해보면 정상 작동하는 것을 확인할 수 있습니다.

class Router {
  push(url, as, options = {}) {
    console.log(url);
    // url: localhost:3000/en/mypage
    return this.change('pushState', url, as, options);
  }
}

삽질 회고

사실 locale props가 궁금해서 파헤쳐 보기 보기보단 버그로 판단, 당장 고칠 생각으로 파헤쳐 봤습니다. (당장 하반기에 배포를 해야 하니..)

버그로 판단한 이유는 다음과 같습니다.

  1. Next.js의 13 버전의 App directory는 아직 Beta 버전
  2. github repo에 수많은 Link, locale, i18n 관련 issue, PR
  3. Page Directory를 사용하는 KR 서비스에서 작동하는 locale props
  4. 문서에 i18n 설정하라는 얘기는 없었잖아..

여기서 결정적으로 3번을 보고 파헤쳐 보게 되었는데요, KR 서비스엔 i18n config에 defaultLocale 속성에 ko를 추가해둔 상황이라 정상 작동을 했지, 결과적으로 App directory와는 관련이 없었습니다.

그럼 왜 Global 서비스엔 i18n config 설정을 안 했을까? 이전에는 next.config.js에서 redirect를 시키기 위해 i18n 설정을 했습니다. 하지만 지금 프로젝트에선 middleware로 redicrect를 제어했기 때문에 굳이 설정을 안한게 이슈의 핵심이었습니다.

비교 대상을 잘못 선정해 삽질을 하긴했지만 나름의 수확은 얻었다고 생각합니다. 나중에 prefetch 하는 로직을 꼭 파헤쳐 보기로..

참고

  1. Next.js repository https://github.com/vercel/next.js
profile
데이원컴퍼니 프론트엔드 개발자입니다.

0개의 댓글