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 변경을 트리거 합니다.
언어 전환 시 화면 갱신 흐름
Router 객체의 Locale 변경 감지
Router에서 바뀐 Locale감지 > 쿠키 업데이트
쿼리 무효화 처리, 서버에 바뀐 쿠키로 데이터 재요청
예: ko에서 en으로 바뀔때
// useSetLanguage.ts if (prevLocale !== currLocale) { // 데이터 요청에 사용되는 'Language' 쿠키를 세팅합니다. (예: 'en') setCookie('Language', currLocale, { path: '/' }); // 모든 쿼리 invalidate → 재요청 → 영어 컨텐츠로 응답 queryClient.invalidateQueries(undefined); }
새 데이터 렌더링
언어에 영향받지 않는 컨텐츠(이미지, 영상 등)는 재렌더링되지 않고, 변경된 항목만 다시 그려집니다.


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·라우팅과 상태에 통합해 저장·복원하므로 히스토리 이동에도 언어가 그대로 유지됨
Humonnom's Tech Blog