Sub-path Routing 다국어 사이트에서의 뒤로가기 핸들링

4 min read
Cover Image for Sub-path Routing 다국어 사이트에서의 뒤로가기 핸들링

개발 환경

  • React / Next.js / React Query

  • Sub-path Routing 방식 Locale 관리: en/cart 또는 ko/cart

Sub-path Routing

Artue 사이트는 URL에 Locale을 포함하는 Sub-path Routing을 사용합니다. 사용자는 두 가지 방법으로 언어를 전환할 수 있습니다.

  • GNB > 언어 전환 버튼 클릭

  • 주소창 > URL 직접 수정

모두 결과적으로는 URL 변경을 트리거 합니다.

언어 전환 시 화면 갱신 흐름

  1. Router 객체의 Locale 변경 감지

    • Router에서 바뀐 Locale감지 > 쿠키 업데이트

    • 쿼리 무효화 처리, 서버에 바뀐 쿠키로 데이터 재요청

    • 예: ko에서 en으로 바뀔때

        // useSetLanguage.ts
        if (prevLocale !== currLocale) {  
          // 데이터 요청에 사용되는 'Language' 쿠키를 세팅합니다. (예: 'en')  
          setCookie('Language', currLocale, { path: '/' });  
      
          // 모든 쿼리 invalidate → 재요청 → 영어 컨텐츠로 응답  
          queryClient.invalidateQueries(undefined);  
        }
      
  2. 새 데이터 렌더링

    • 언어에 영향받지 않는 컨텐츠(이미지, 영상 등)는 재렌더링되지 않고, 변경된 항목만 다시 그려집니다.

    • key가 바뀌면 다시 렌더링 되기 때문에, key에 번역대상 텍스트가 포함되지 않도록 주의해야 합니다.

      • NG: user-image-영희

        • 영어로 바뀌면 user-image-younghui로 바뀌므로 다시 렌더링 됨
      • OK: user-image-12

        • 언어에 관계없이 일관된 id등의 식별자 활용

문제(As-is)

  • 마이페이지에서 장바구니 페이지로 이동 > 언어를 한국어(ko)로 전환 > 뒤로가기 시

    • 언어 전환이 무시되고, 마이페이지가 영어(en)로 보입니다.

  • 원인

    • 뒤로가기 시 Locale을 포함한 이전 URL로 복원하여 언어 변경이 유지되지 않는 것처럼 보입니다.

목표(To-be)

  • 뒤로가기 시에도 언어 전환이 유지되어야 합니다.

  • 해결 방법

    • 기존 관리 방식에서는 URL(router)에 의존하는 Locale 관리방식을 사용했으나,

      • 예: Locale값 우선 > URL의 Locale값이 바뀌면 쿠키 값도 자동으로 바뀜
    • 뒤로가기 시에는 예외적으로 쿠키값을 더 우선해야함

      • Cookie와 URL의 Locale이 불일치하면 URL을 업데이트

정리

  • Cookie Language !== URL Locale

    • 일반적으로: Cookie를 수정

    • 뒤로가기 이벤트: URL을 수정

해결 방법 코드(BeforePopState)

뒤로가기 이벤트 시에만 추가 처리하여 기존 로직을 유지합니다.

1) 뒤로가기 이벤트 이해

  • History Stack: pushState/replaceState로 entry가 생성되며, 상단이 최신입니다.

  • popstate 이벤트: 세션 기록 탐색으로 활성화된 기록 항목이 바뀔 때 발생. 새 entry가 current가 된 후 호출됩니다.

  • Next.js의 router.beforePopState(cb)를 사용해 popState 이전에 개입할 수 있습니다.

    • props: url, as, options(scroll, shallow, locale 등)

    • cb 반환이 false면 router의 기본 동작을 차단합니다.

2) 불일치 감지 로그 확인

// History Back 핸들링 Hook의 일부  
// router: Next.js router 객체  
// currentPageLocale: cookie 'Language' 값  
useEffect(() => {  
  router.beforePopState(({ url, as, options }) => {  
    const { locale: prevPageLocale } = options;  

    if (prevPageLocale !== currentPageLocale) {  
      // TODO: Locale 불일치 시 추가 처리  
      console.log('Locale does not match.');  
    }  
    return true;  
  });  

  return () => {  
    router.beforePopState(() => true);  
  };  
}, [router, currentPageLocale]);

3) 기본 동작 차단

router.beforePopState(({ url, as, options }) => {  
  const { locale: prevPageLocale } = options;  

  if (prevPageLocale !== currentPageLocale) {  
    // History 조작 직접 처리 위해 false 리턴  
    return false;  
  }  
  return true;  
});  

return () => {  
  router.beforePopState(() => true);  
};
// Next.js 내부(onPopState) 일부  
// this._bps: beforePopState 콜백  
// false면 this.change(...)를 실행하지 않음 → router 상태를 직접 업데이트해야 함  
if (this._bps && !this._bps(state)) { return }  

this.change(  
  'replaceState',  
  url,  
  as,  
  Object.assign<{}, TransitionOptions, TransitionOptions>({}, options, {  
    shallow: options.shallow && this._shallow,  
    locale: options.locale || this.defaultLocale,  
    // @ts-ignore internal value not exposed on types  
    _h: 0,  
  }),  
  forcedScroll  
)

4) History Stack 상단 entry를 올바른 Locale로 교체

router.beforePopState(({ url, as, options }) => {  
  const { locale: prevPageLocale } = options;  

  if (prevPageLocale !== currentPageLocale) {  
    // 쿠키의 Locale로 새로운 path, options 생성  
    const newPath = getNewPath(currentPageLocale, as);  
    const newOptions = getNewOption(currentPageLocale, options);  

    // 최근 History 변경은 replace 사용  
    router.replace(newPath, undefined, newOptions);  
    return false;  
  }  
  return true;  
});  

return () => {  
  router.beforePopState(() => true);  
};

5) 주소창 깜빡임 제거

router.beforePopState(({ url, as, options }) => {  
  const { locale: prevPageLocale } = options;  

  if (prevPageLocale !== currentPageLocale) {  
    const newPath = getNewPath(currentPageLocale, as);  
    const newOptions = getNewOption(currentPageLocale, options);  

    router.replace(newPath, undefined, newOptions);  
    // 주소창 깜빡임 방지  
    window.history.replaceState(newOptions, '', newPath);  

    return false;  
  }  
  return true;  
});  

return () => {  
  router.beforePopState(() => true);  
};

최종 Hook 코드

/* imports 생략 */  

export const useLocalizedBackNavigation = () => {  
  const router = useRouter();  
  const [cookies] = useCookies(['Language']);  
  const currentPageLocale = cookies['Language'] || Locales.EN;  

  useEffect(() => {  
    router.beforePopState(({ url, as, options }) => {  
      const { locale: prevPageLocale } = options;  
      if (prevPageLocale !== currentPageLocale) {  
        const newPath = getNewPath(currentPageLocale, as);  
        const newOptions = getNewOptions(currentPageLocale, options);  

        router.replace(newPath, undefined, newOptions);  
        window.history.replaceState(newOptions, '', newPath);  

        return false;  
      }  
      return true;  
    });  

    return () => {  
      router.beforePopState(() => true);  
    };  
  }, [router, currentPageLocale]);  
};

결과

  • 언어 전환 후 뒤로가기를 해도 Locale이 유지됩니다.

추가

  • next-intl와 같은 라이브러리를 사용하면 이런 작업을 수동으로 하지 않아도 됩니다.

    • URL·라우팅과 상태에 통합해 저장·복원하므로 히스토리 이동에도 언어가 그대로 유지됨