一覧へ戻る

Next.js 라우팅 트러블슈팅: URL vs 로컬 스토리지

로컬 스토리지 기반의 히스토리 탭 기능을 구현하면서 겪은 강제 리다이렉트 문제와 이를 해결하기 위해 URL을 진실의 원천(Source of Truth)으로 변경한 과정을 공유합니다.
Next.js
Routing
Troubleshooting
UX
2025-12-217 min read

Next.js 라우팅 트러블슈팅: URL vs 로컬 스토리지

VS Code 스타일의 UI를 웹으로 구현하면서, **"이전에 열어둔 탭을 기억하는 기능"**은 필수적이었습니다. 이를 위해 localStorage를 사용하여 히스토리 상태를 관리했는데, 이 과정에서 예상치 못한 라우팅 문제가 발생했습니다.오늘은 딥링킹(Deep Linking)이 불가능했던 이슈와 이를 해결해나가는 과정을 정리해보려 합니다.

1. 기본 플로우 (문제점)

상황
사용자가 사이트를 재방문했을 때 이전 작업 환경을 그대로 보여주기 위해, HistoryContext를 만들어 로컬 스토리지에 열려있는 탭 목록과 활성 탭 정보를 저장했습니다.
문제 발생
"첫 유저를 어디론가 랜딩 시켰을 때" 문제가 터졌습니다. 예를 들어, 친구에게 "나 게임 만들었어! 해봐"라며 https://vscoke.icecoke.kr/game 링크를 공유했다고 가정해봅시다.
  1. 사용자는 /game으로 접속합니다.
  2. 하지만 앱이 켜지는 순간, HistoryTabs 컴포넌트가 로컬 스토리지를 확인합니다.
  3. "어? 저장된 히스토리가 없네? (또는 마지막에 본 게 홈이네?)"
  4. "메인으로 가!" -> /로 강제 리다이렉트.
결국 사용자는 링크를 타고 왔음에도 무조건 메인 페이지(Hello, I'm Icecoke!)만 보게 되는 치명적인 이슈가 있었습니다.

2. 고민했던 방법들

이 문제를 해결하기 위해 몇 가지 방법을 고민했습니다.
방법 A: 예외 처리 추가
  • 히스토리가 비어있을 때는 리다이렉트를 하지 않는다?
  • 하지만 만약 히스토리가 있으면? 사용자가 /game으로 들어왔는데, 저장된 기록에 따라 /About으로 보내버린다면 더 이상하겠죠.
방법 B: URL 우선권 부여
  • 로컬 스토리지 데이터보다 현재 접속한 URL을 더 높은 우선순위로 둔다.
  • "기억된 것"보다 "지금 사용자가 보러 온 것"이 더 중요하다는 접근입니다.
결국 방법 B가 웹의 기본 원칙(URL이 리소스를 가리킨다)에 맞다고 판단하여 진행했습니다.

3. 해결 시도와 사이드 이펙트

1차 시도: Source of Truth 변경
기존에는 HistoryTabs가 주도권을 가지고 라우터를 조종했습니다. "히스토리 탭이 바뀌면 -> 라우터도 이동해"이것을 반대로 뒤집었습니다. "URL이 바뀌면 -> 히스토리 탭을 맞춰"
// 변경 전 (HistoryTabs가 대장)
useEffect(() => {
  if (current) router.replace(current.path);
}, [current]);

// 변경 후 (URL이 대장)
const pathname = usePathname();
useEffect(() => {
  // URL에 맞는 탭이 없으면 추가, 있으면 활성화
  syncTabWithUrl(pathname);
}, [pathname]);
발생한 사이드 이펙트 (Side Effect)
이렇게 바꾸고 났더니, 갑자기 사이드바(탐색기)가 고장 났습니다. 파일을 클릭해도 URL은 안 바뀌고, 탭만 번쩍거리는 기이한 현상이 발생했죠.원인은 사이드바 아이템(ExplorerItem)에 있었습니다. 예전에는 사이드바가 history.add()만 호출하면, HistoryTabs가 그걸 감지해서 router.push를 대신 해줬던 구조였기 때문입니다.HistoryTabsrouter 제어권을 뺏으니, 아무도 주소를 바꿔주지 않게 된 것입니다.

4. 최종 적용된 방법

결국 **"URL을 모든 상태의 중심(Single Source of Truth)으로 둔다"**는 원칙을 확고히 했습니다.
  1. 사이드바 수정: 아이템 클릭 시 상태를 건드리지 않고, 무조건 router.push(url)만 수행합니다. Next.js의 방식 그대로 따르는 것이죠.
  2. HistoryTabs 수정: router가 변경되면(URL이 변하면) 그때 useEffect가 돌면서 **"어? 새 주소네? 탭 열어줄게"**라고 반응형으로 동작합니다.
  3. Hydration 처리: 서버 사이드 렌더링(SSR)과 로컬 스토리지 간의 불일치를 막기 위해, isHydrated 체크를 넣어 브라우저 로딩이 끝난 후에만 동기화 로직이 돌도록 안전장치를 걸었습니다.
// 최종 로직 흐름
User Click -> router.push('/game') -> URL Change -> HistoryTabs useEffect -> Add Tab & Active

5. 후기

"Web is all about URLs."이번 트러블슈팅을 통해 웹 애플리케이션에서 URL이 가진 힘을 다시 한번 깨닫게 되었습니다. 화려한 SPA(Single Page Application) 기능을 구현하느라 가끔 기본을 잊을 때가 있는데, **"상태 관리가 복잡해질수록 URL을 믿어라"**는 교훈을 얻었습니다.IDE 같은 UI를 구현하더라도, 웹이라면 결국 URL 하나로 모든 상태가 설명될 수 있어야 진짜 웹 앱이 아닐까 싶습니다.