- 발행일
Next.js 안에 react-router-dom이 두 달 있었다 — 한 번도 마운트되지 않은 훅
Next.js 안에 react-router-dom이 두 달 있었다
이 글은 2026년 9월에 당시 커밋 이력을 다시 보며 정리한 것입니다. 날짜는 작업한 날 기준입니다.
TripTune은 Next.js 14 App Router로 시작한 프로젝트입니다. 프런트는 저 혼자였고, App Router는 처음이었어요. 그 저장소의 package.json에 2024년 7월 8일부터 9월 7일까지 react-router-dom이 들어 있었습니다.
Next.js에는 라우터가 이미 있습니다. next/navigation의 useRouter가 그것이에요. 그런데 다른 라우터 라이브러리를 깔아 두고 두 달을 보냈습니다.
언제 들어왔고, 언제 쓰였나
들어온 커밋은 bcefe6d(7월 8일, "로그아웃 / 아이디 찾기 / 비밀번호 찾기")입니다. 이 커밋의 package.json diff에 react-router-dom과 crypto-js가 같이 추가됐어요.
+ "crypto-js": "^4.2.0",
"jest": "^29.7.0",
"js-cookie": "^3.0.5",
"next": "14.2.4",
@@
+ "react-router-dom": "^6.24.1",
그런데 같은 커밋의 src/ 안에는 react-router를 import하는 파일이 하나도 없습니다. 패키지만 먼저 깔린 거예요. 왜 깔았는지는 커밋 메시지에 남아 있지 않습니다.
실제로 코드에 등장한 건 3주 뒤 7349a53(8월 1일, "로딩 추가 / accessToken 처리 변경")입니다. axios 인터셉터를 세우는 훅 useAxiosConfig.ts에서였어요.
import { useEffect, useRef } from 'react';
import { useNavigate, NavigateFunction } from 'react-router-dom';
import { AxiosInstance } from 'axios';
import Cookies from 'js-cookie';
const setAxiosInterceptors = (instance: AxiosInstance, navigate: NavigateFunction) => {
// ... 요청에 Bearer 토큰 붙이기
instance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response && error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// refresh 요청, 새 토큰 저장, 원래 요청 재시도
} catch (refreshError) {
navigate('/login');
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
};
const useAxiosConfig = (axiosInstance: AxiosInstance) => {
const navigate = useNavigate();
const interceptorsRegistered = useRef(false);
useEffect(() => {
if (!interceptorsRegistered.current) {
setAxiosInterceptors(axiosInstance, navigate);
interceptorsRegistered.current = true;
}
}, [axiosInstance, navigate]);
};
의도는 읽힙니다. 토큰 갱신에 실패하면 로그인 화면으로 보내고 싶었고, 그러려면 훅 안에서 이동 함수를 얻어야 했어요. React에서 늘 하던 대로 useNavigate를 꺼냈습니다. 인터셉터가 렌더마다 중복 등록되는 걸 막으려고 useRef로 한 번만 걸도록 한 것도 보이고요.
문제는 여기가 Next.js라는 겁니다. useNavigate는 react-router의 <BrowserRouter> 안에서만 동작합니다. 그 라우터를 앱 어디에도 세우지 않았으니, 이 훅이 실제로 불렸다면 즉시 예외가 났을 거예요.
그런데 왜 터지지 않았나
같은 커밋의 axiosInstance.ts를 보면 답이 나옵니다.
const axiosInstance = axios.create({
baseURL: 'http://13.209.177.247:8080/:path*',
timeout: 5000,
});
export const AxiosProvider = ({ children }: AxiosProviderProps) => {
useAxiosConfig(axiosInstance);
return {children};
};
export default axiosInstance;
useAxiosConfig를 부르는 곳은 AxiosProvider 하나뿐입니다. 그리고 AxiosProvider를 마운트하는 파일은 저장소 전체에 없습니다. git grep AxiosProvider를 그 커밋에서 돌리면 정의한 파일 한 곳만 나와요.
그러니까 훅은 한 번도 실행되지 않았습니다. 인터셉터도 걸리지 않았고, 401을 받아도 로그인 화면으로 가지 않았고, useNavigate도 던질 기회가 없었습니다. return {children}도 JSX가 아니라 객체를 돌려주는 코드인데, 실행된 적이 없으니 그것도 문제가 되지 않았어요.
7월 17일 판(ccb81a7)의 같은 파일에는 navigate가 없었습니다. useEffect 안에서 인터셉터를 걸고 갱신 실패는 Promise.reject만 했어요. 8월 1일에 "실패하면 로그인으로"를 붙이면서 라우터가 필요해졌고, 그때 손에 익은 것을 집었습니다.
지운 날
훅 파일은 9월 1일 f55d4de("여행지 탐색 - 홈 / 검색")에서 통째로 지워졌습니다. 인터셉터는 훅 밖으로 나와 axiosInstance.ts 모듈 최상단에 직접 걸리게 됐고, 갱신 실패 시의 이동은 없어졌어요. console.error만 남았습니다.
axiosInstance.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError) => {
if (error.response?.status === 401) {
console.error('401 Unauthorized - 토큰이 만료되었습니다.');
// refresh 시도, 실패하면 console.error('토큰 갱신 실패:', refreshError);
}
return Promise.reject(error);
}
);
이동이 필요 없어지니 라우터도 필요 없어졌는데, 패키지는 엿새 더 남아 있다가 06121bd(9월 7일)에서 빠졌습니다. 그 커밋 제목은 "여행지 탐색 개선, 라이트 하우스 개선, seo 추가"예요. 라이트하우스 점수를 보다가 안 쓰는 의존성을 정리한 김에 같이 뺀 것으로 보이는데, 커밋 메시지에 그 말은 없습니다. crypto-js도 이때 같이 나갔습니다. 두 패키지 모두 들어온 날과 나간 날이 같습니다.
검증
git log -S'react-router-dom' -- package.json으로 추가 커밋(bcefe6d)과 삭제 커밋(06121bd) 둘만 나오는 것을 확인했습니다.git log -S'useNavigate' -- src로 코드에 등장한 커밋이7349a53(추가)과f55d4de(삭제) 둘뿐인 것을 확인했습니다.7349a53시점에서git grep AxiosProvider -- src를 돌려 정의 파일 한 곳만 나오는 것을 확인했습니다. 마운트한 곳이 없다는 근거는 이것입니다.- 당시 실제로 실행해서 확인한 기록은 없습니다. 위 결론은 전부 저장소를 다시 읽어서 낸 것입니다.
남은 것 · 한계
- 왜
react-router-dom을 깔았는지는 모릅니다. 커밋 메시지에도, 코드 주석에도 없습니다. 3주 동안 import 없이 있었으니 "일단 깔아 둔" 쪽에 가깝지만 그것도 추측입니다. - 인터셉터가 걸리지 않은 채 한 달을 보냈다는 걸 당시에 알았는지 모릅니다. 401 처리가 없어도 화면이 뜨는 데는 지장이 없어서 눈에 안 띄었을 가능성이 큽니다.
- 9월 1일 판은 갱신 실패를
console.error로만 남깁니다. 사용자는 로그인이 풀린 채 화면에 남아요. 이 문제가 여러 갈래로 갈라져 나타난 것을 2년 뒤에야 한곳으로 모았습니다. 그 결말은 로그인이 자꾸 풀리던 서비스2026-07-29로그인이 자꾸 풀리던 서비스 — 401 처리를 한곳으로 모으기까지로그인 풀림, alert 두 번 뜸, 무한 토큰 갱신 루프 — 증상은 달랐지만 전부 한 뿌리였습니다. 401 처리가 화면마다 흩어져 있었거든요. 서버 에러를 enum 4종으로 분류하는 공통 fetch 래퍼로 모은 과정과, 인증 상태를 boolean이 아니라 3-state로 두는 이유, 그리고 지금 코드에 남아 있는 결함까지 정리했습니다.에 있습니다. - 라우터 혼용은 이때뿐이었지만, App Router를 App Router답게 쓰지 못한 흔적은 다른 데도 남아 있었습니다. 루트 레이아웃의
'use client'가 그것이고, 그 글2026-03-14루트 레이아웃에 'use client'가 붙어 있었다 — 라이트하우스 점수를 보러 갔다가App Router로 만들었다고 믿었던 프로젝트의 루트 레이아웃에 use client가 붙어 있었습니다. 그 한 줄 때문에 트리 전체가 클라이언트 컴포넌트였고, next/head는 무동작이었으며, QueryClient는 모듈 최상단에서 만들어지고 있었어요. 점수를 올리러 갔다가 경계를 다시 그은 기록입니다.에서 다뤘습니다.
관련 글: 로그인이 자꾸 풀리던 서비스 — 401 처리를 한곳으로 모으기까지2026-07-29로그인이 자꾸 풀리던 서비스 — 401 처리를 한곳으로 모으기까지로그인 풀림, alert 두 번 뜸, 무한 토큰 갱신 루프 — 증상은 달랐지만 전부 한 뿌리였습니다. 401 처리가 화면마다 흩어져 있었거든요. 서버 에러를 enum 4종으로 분류하는 공통 fetch 래퍼로 모은 과정과, 인증 상태를 boolean이 아니라 3-state로 두는 이유, 그리고 지금 코드에 남아 있는 결함까지 정리했습니다. · 루트 레이아웃에 'use client'가 붙어 있었다2026-03-14루트 레이아웃에 'use client'가 붙어 있었다 — 라이트하우스 점수를 보러 갔다가App Router로 만들었다고 믿었던 프로젝트의 루트 레이아웃에 use client가 붙어 있었습니다. 그 한 줄 때문에 트리 전체가 클라이언트 컴포넌트였고, next/head는 무동작이었으며, QueryClient는 모듈 최상단에서 만들어지고 있었어요. 점수를 올리러 갔다가 경계를 다시 그은 기록입니다. · fetch가 Next.js에서 선호되는 이유, 그리고 axios와의 차이점2023-06-01fetch가 Next.js에서 선호되는 이유, 그리고 axios와의 차이점fetch와 axios의 차이를 정리했던 글입니다. 내장 API·번들 크기·SSR 호환이라는 당시 결론은 지금도 유효하지만, 다시 보니 진짜 이유를 놓치고 있었어요 — Next.js App Router는 fetch 자체를 확장해 캐싱·중복 제거를 얹습니다. 실제 프로젝트에서 fetch를 직접 채택하고 래퍼를 만들며 치른 비용까지 붙여서 업데이트했습니다. · axios를 걷어냈다가 이틀 만에 되돌렸다가 다시 걷어냈다2024-09-14axios를 걷어냈다가 이틀 만에 되돌렸다가 다시 걷어냈다 — 문제는 라이브러리가 아니라 프록시였다사이드 프로젝트에서 axios를 fetch로 바꿨다가 28시간 뒤 되돌리고, 30분 뒤 다시 fetch로 갔습니다. 그 사이 커밋 메시지는 "프록시 시도 중", "프록시 실험 일단 종료", "credentials 처리 방법 실험 시도"였어요. 되돌아보니 HTTP 클라이언트를 바꾼 게 아니라 Netlify 리다이렉트와 Next 리라이트 사이를 헤맨 이틀이었습니다. 왜 흔들렸는지를 커밋 순서대로 적습니다. · JWT exp를 프런트가 읽던 함수를 지웠다2025-02-03JWT exp를 프런트가 읽던 함수를 지웠다 — 만료 판단을 서버에 맡기기까지 한 달액세스 토큰의 payload를 atob로 풀어 exp를 비교하는 함수가 2024년 9월부터 있었습니다. 2025년 1월에 그 함수를 지우고, 2월에는 화면마다 쿠키를 직접 읽던 로그인 유도를 useAuth 훅 하나로 모았어요. 그날 커밋은 모달을 띄우지 못하는 코드를 남겼고 사흘 뒤에 고쳤습니다. 401 처리를 한곳으로 모으는 이야기의 첫 장면입니다.