🚀 Next.js에서 React.js로 전환하고 동시에 TanStack Router를 도입하면서 배운 점 회고
우리 서비스는 처음에는 Next.js 기반으로 개발되었지만, 운영과 개발을 진행하면서 Next.js가 꼭 필요한가에 대한 고민이 많았다. Next.js의 대표적인 강점으로 흔히 SEO 최적화, SSR(서버 사이드 렌더링), SSG(정적 사이트 생성)을 꼽을 수 있다. 하지만 우리 서비스는 이러한 기능이 크게 필요하지 않았고, 오히려 Next.js를 유지하는 것이 비효율적이라고 판단했다.
SEO가 중요하지 않은 서비스
Next.js를 사용할 가장 큰 이유 중 하나가 편리한 SEO(검색 엔진 최적화)다. 하지만 우리 서비스는 검색엔진에서 노출되지 않는 구조였다. 따라서 Next.js의 강점 중 하나인 SEO 최적화 기능을 활용할 이유가 없었다.
SSR이 불필요한 환경
SSR(서버 사이드 렌더링)은 페이지를 서버에서 미리 렌더링한 후 클라이언트에 전달하는 방식으로, 초기 로딩 속도를 개선하는 데 유용하다고 알려져 있다. 하지만 우리 서비스에서는 SSR이 오히려 불필요한 오버헤드가 되었다.
우리 서비스는 페이지를 처음 로딩할 때 필수적으로 데이터를 불러와야 화면을 제대로 렌더링할 수 있는 구조였다. 하지만 SSR 환경에서는 모든 데이터를 서버에서 다 불러온 후에야 페이지를 완전히 렌더링해 응답을 보낼 수 있었다.
반면, CSR(클라이언트 사이드 렌더링)에서는 데이터를 불러오는 동안에도 먼저 화면의 일부를 렌더링하여 사용자 경험을 더 빠르게 제공할 수 있었다.
결과적으로, SSR을 유지하는 것이 성능 면에서 오히려 불리했다. CSR 방식으로 전환하면 데이터 로딩이 끝나지 않아도 데이터가 필요하지 않은 UI는 먼저 보여주고, 데이터가 필요한 UI는 스켈레톤 등으로 처리하면 사용자 경험이 더 개선될 것이라고 판단했다.
SSG가 적합하지 않은 데이터 구조
SSG(정적 사이트 생성)는 미리 페이지를 생성해 빠른 응답 속도를 제공하지만, 우리 서비스의 99% 이상의 페이지가 사용자 데이터에 기반하여 서버에서 동적으로 생성되어야 했다. 즉, 대부분의 페이지가 정적으로 미리 생성될 수 없었기 때문에, Next.js의 SSG 기능을 활용할 의미가 없었다.
운영 환경과 맞지 않는 배포 방식과 경험 부족
Next.js는 Vercel을 이용하면 편리하게 배포할 수 있지만, 회사의 운영 정책상 nginx + pm2를 이용한 배포가 필수였고, Next.js를 이런 방식으로 운영한 경험이 사내에 없었다.
부족한 실무 경험이 초기에 시스템을 구축할때 많은 시간을 소요하게 했고, 실제로 Next.js 운영 경험이 부족한 상태에서 장애가 발생한 적도 있어, 유지보수 부담이 컸다.
빌드 및 개발 속도 문제
Next.js의 빌드 속도가 느려, 최적화해도 배포에 10분 이상 걸리는 경우가 많았다. 로컬 개발 시, 사소한 변경에도 SSR 환경이 적용되어 페이지 리로드 속도가 느렸다.
이러한 이유로 Next.js를 유지하는 것이 오히려 서비스에 부담이 되는 기술 선택이라고 판단했다.
사실 프로젝트 초반에 Next.js로 구현하자는 의견에 대해서는 이미 위와 같은 이유로 개인적으로 회의적이였다. 팀원들도 같은 의견이였지만 전사적으로 결정된 내용이기에 새로운 기술 적용을 좋아하는 나로써 이것저것 열심히 찾아보고 배포 과정도 혼자 고민하고 짜보면서 재미있게 프로젝트를 진행했던 기억이 있다.
하지만 결국, 우리는 필요하지 않은 기능이 많고, 오버헤드가 큰 Next.js 대신 React.js로 전환하는 것이 더 적절하다고 결론 내렸다.
마이그레이션 과정에서 가장 중요하게 고려한 요소는 기존 코드베이스의 수정 최소화였다.
개인 프로젝트가 아닌 회사에서 진행하는 프로젝트인 만큼, 기한을 엄수하는 것이 필수적이었기 때문이다.
특히, SSR 환경에서 CSR 환경으로 전환하면서 코드 수정이 불가피할 수밖에 없었지만, 이를 최소화하면서 마이그레이션을 완료하는 것이 목표였다.
기존 프로젝트는 Next.js의 Pages Router를 사용하고 있었으며, SSR을 위해 getServerSideProps를 활용해 데이터를 컴포넌트에 전달하는 방식을 사용하고 있었다.
하지만 React 환경에서는 getServerSideProps 같은 데이터 페칭 방식이 기본적으로 존재하지 않기 때문에, 이를 어떻게 대체할지가 핵심적인 고민이었다.
Next.js에서 React로 전환하면서 라우팅 방식을 선택하는 것도 중요한 요소였다.
자료를 조사하던 중, TanStack Router의 loader 함수로 데이터를 페칭하고 component에 응답 데이터를 전달하는 패턴이 기존에 우리가 사용하던 getServerSideProps 패턴과 매우 유사하다는 점을 발견했다.
SSR 환경의 코드를 CSR 환경으로 전환하기 전 가장 우려됐던 부분은 데이터를 페칭하는 패턴을 어떻게 변경 할 것인가 였는데, 기존에 사용하던 getServerSideProps 패턴을 전부 useEffect로 바꿔야 하는건가? 그럼 공통 컴포넌트에서는 사용되는 곳 만큼 분기처리를 해야하는건가? 라는 걱정부터 앞섰다.
하지만 loader를 활용하면 SSR 환경에서 CSR 환경으로 전환하면서도 기존 코드 구조를 최대한 유지할 수 있을 것이라 판단했다.
또한, 우리 서비스에는 40개 이상의 페이지가 존재했고, Next.js의 Pages Router에서 폴더 기반 라우팅을 적극적으로 활용하고 있었다. TanStack Router는 파일 기반 라우팅(File-based Routing)을 지원하여, 파일을 생성하면 자동으로 라우트를 설정해주는 기능이 Next.js의 Pages Router와 상당히 유사했다. 이를 통해 기존 라우팅 구조를 크게 변경하지 않고도 React 환경으로 전환할 수 있을 것이라 판단했다.
반면, React Router는 코드 기반 라우팅(Code-based Routing)을 주로 사용하며, 파일 기반 라우팅을 기본적으로 지원하지 않는다. 따라서, 기존의 폴더 구조와 라우팅 방식을 유지하면서 코드 수정을 최소화하기 위해 TanStack Router를 선택하게 되었다.
결과적으로, TanStack Router를 도입하면 코드 수정을 최소화하면서 기존의 SSR 패턴을 CSR 환경에 맞게 변환할 수 있을 것이라 생각했고, 이를 중심으로 마이그레이션을 진행하기로 결정했다.
프로젝트에 TanStack Router를 적용하면서 여러 가지 장단점을 경험했다.
파일 기반 라우팅 지원
파일을 생성하면 자동으로 라우트를 설정해주는 기능이 있어서, Next.js의 Pages Router와 유사한 방식으로 라우팅을 구성할 수 있었고 기존 코드를 많이 수정하지 않고 작동 가능하게 수정 할 수 있었다.
타입 안전성 보장
경로 매개변수와 검색 매개변수에 대한 타입 안전성을 제공해서, 컴파일 시점에 오류를 사전에 방지할 수 있었다. 특히, 경로 매개변수는 타입 안정성으로 ide에서 자동 완성 기능을 완벽하게 지원했고, 지원검색 매개변수는 기존 Next.js에서는 타입이 string 하나만 존재했는데 TanStack Router에서는 데이터를 넣었을 때 타입 그대로 설정되어서 신기했다.
레이아웃과 효율적인 데이터 로딩
라우팅 트리의 모든 지점에서 중첩된 레이아웃과 효율적인 데이터 로딩 기능을 제공해서, 복잡한 UI 구조를 효율적으로 관리할 수 있었다. Next.js에서 사용하던 layout을 route라는 예약어로 파일을 생성하면 그대로 사용이 가능했고, 특정 조건에서 리다이렉트 하는 로직을 해당 기능을 이용해 컴포넌트 코드와 분리할 수 있어 코드의 복잡도를 줄일 수 있었다.
학습 곡선
기존에 React Router에 익숙해 TanStack Router의 새로운 개념과 API에 적응하는 데 시간이 필요했다.
생태계의 성숙도
TanStack Router는 비교적 새로운 라이브러리라서, React Router에 비해 커뮤니티와 생태계가 아직 성숙하지 않아 자료나 예제가 부족했다.
실제로 프로젝트 중간에 pendingcomponent가 라우팅이 아닌, 새로고침 시점에만 작동하는 버그가 있어 검색을 통해 해결하려 했지만 관련된 내용이 없어 github discussions에 관련된 글을 남겼던 적이 있다. 링크
글을 작성하고 2주 뒤 버전 업데이트를 통해 해결된 것을 확인할 수 있다는 답변을 받았고 실제로 해결 되었지만 어디가 문제였고 어떻게 해결된지는 히스토리 추적이 어려웠다.
이러한 장단점을 고려해서 TanStack Router를 선택하고 적용했다. 전반적으로 프로젝트의 요구사항에 부합했고, 특히 파일 기반 라우팅과 타입 안전성 측면에서 큰 이점을 얻을 수 있었다.
useRouter().query와는 다르게, params와 search로 나뉘어 관리해야 한다는 점이 불편했다.useRouter().query를 사용해 params와 search 값을 한 곳에서 관리할 수 있었지만 TanStack Router에서는 path params는 params로, URL 쿼리는 search로 나뉘어서 관리해야 했다.useRouter().query 기반으로 작성된 코드들을 모두 수정해야 했고, 이 과정에서 꽤 시간이 걸렸음. 기본적으로는 타입 안정성이 강화된다는 점이 장점이지만, 기존 Next.js의 방식에 익숙했던 만큼 코드 수정이 많아지는 점은 예상하지 못했던 불편함이었다.
결국, 공통 컴포넌트를 활용한 데이터 페칭 전략을 일부 수정해야 했고, 기대했던 방식으로 동작하지 않아 초반에는 꽤나 당황했던 기억이 있다.
loader 함수에서 AbortController의 비정상 동작loader 함수에서 AbortController를 활용해 데이터 페칭을 취소하는 기능이 있다고 해서 불필요한 데이터 페칭이 발생하면 취소 시키기 위해 사용해봤다.
하지만 실제로 적용해보니, 이 기능이 전혀 동작하지 않는 문제를 발견했고 이를 해결하기 위해 라이브러리 코드를 직접 분석하여 문제의 원인을 파악했고, 수정한 코드를 기반으로 PR을 올렸지만, 아직까지 maintainer의 답변을 받지 못한 상태이다. 코드를 분석하고 수정하는데 꽤 시간을 들였는데 많이 아쉬움이 남는 부분이다.
이 경험을 통해, 오픈 소스 라이브러리를 사용할 때 예상치 못한 버그를 마주할 수 있으며, 이를 해결하기 위해 직접 기여하는 과정의 중요성을 깨달았다.
이런 예상 밖의 문제들이 있었지만, 결국 이를 해결하는 과정에서 TanStack Router의 동작 방식을 더 깊이 이해하게 되었다.
route.pathname vs useLocation().pathnamelocation.name을 기반으로 코드 분기 처리를 하는 부분이 있었다. 프로젝트 특성상 공통적으로 사용되는 컴포넌트가 많았고, 라우트에 따라 특정 UI를 다르게 렌더링해야 하는 경우가 많았던 것이 그 이유였다.route.pathname과 useLocation().pathname의 동작이 다르다는 점을 직접 적용하면서 알게 되었다.route.pathname → 라우트의 loader가 완료되지 않아도 변경됨. useLocation().pathname → 라우트의 loader가 완료되기 전까지는 이전 pathname을 반환함. 즉, route.pathname은 라우트 변경 시 즉시 반영되지만, useLocation().pathname은 loader가 완료될 때까지 갱신되지 않는 차이가 있었다.
이 때문에 기존의 location.name을 기반으로 한 코드가 제대로 동작하지 않는 경우가 발생했고, 결국 loader의 상태를 체크해서 분기 처리하는 방식으로 수정했다.
beforeLoader와 loader의 차이점loader 외에도 beforeLoader라는 개념이 있었다. 처음에는 단순히 실행 순서의 차이 정도로 생각했는데, 직접 콘솔을 찍어가면서 분석해보니 차이점이 명확했다. beforeLoader → 모든 beforeLoader가 완료되어야 렌더링이 시작됨. loader → 렌더링이 시작된 후 실행됨. 이 구조를 이용해서,
beforeLoader에서 처리 loader에서 처리 하는 방식으로 설계했다.
이렇게 하면 리다이렉트와 인증 로직이 먼저 실행되고, 이후 페이지가 렌더링되는 동안 데이터가 비동기적으로 로딩되기 때문에 전체적인 로딩 속도를 조금이라도 줄일 수 있겠다고 판단했다.
TanStack Router에 대한 자료가 많지 않아서, beforeLoader와 loader의 실행 순서, useLocation().pathname과 route.pathname의 차이 같은 것들을 직접 콘솔 찍어가면서 조사했던 기억이 있다.
이 과정에서 TanStack Router의 내부 동작을 더 깊이 이해할 수 있었고, 공식 문서만으로는 부족한 부분을 직접 실험하고 분석하는 게 얼마나 중요한지 다시 한번 깨달았다.
사실 TanStack Router를 도입하는 것은 내가 주장한 의견이었다. 그만큼 이 라이브러리가 어떤 기능을 제공하는지, 어떻게 적용할 수 있는지, 우리 프로젝트에 어떤 이점을 가져올 수 있는지를 설득할 필요가 있었다. 그리고 팀원들에게 설득하려면, 내가 직접 이 라이브러리를 깊이 이해하고 있어야 한다고 생각했다.
TanStack Router에 대한 깊이 있는 학습
도입을 주장한 입장에서, 단순히 "좋아 보인다"는 이유만으로 밀어붙일 수는 없었다.
그래서 TanStack Router의 공식 문서를 기반으로 직접 공부하기 시작했다.
하지만 공식 문서는 영어 원문만 제공되었고, 깊이 있게 학습하기가 쉽지 않았다.
TanStack Query 한글 번역 프로젝트와의 인연
그러던 중, TanStack Query의 공식 문서를 한글로 번역해서 공개한 사람을 찾았다. 링크
이 사람과 연락하여 어떻게 번역을 진행했는지, TanStack Router에도 같은 방식을 적용해도 될지 물어봤다.
다행히 긍정적인 답변을 받았고, 같은 방식을 적용해서 TanStack Router 공식 문서의 한글 번역본을 만들기 시작했다.

처음 목표는 공식 문서를 하나하나 번역하면서 라이브러리를 깊이 이해하는 것이었다. 하지만 프로젝트 일정이 앞당겨지면서, 일부 문서는 충분히 자세하게 보지 못하고 번역해야 하는 상황이 발생했다.
이 점은 아쉬웠지만, 문서를 번역하는 과정에서 TanStack Router에 대한 이해도가 크게 향상되었고, 팀원들과의 공유도 더 원활하게 진행할 수 있었다.

수줍게 공유..
결국, 공식 문서 번역은 단순히 한글화하는 작업이 아니라, 내가 라이브러리를 깊이 이해하는 과정이기도 했다.
팀원들에게 TanStack Router를 효과적으로 설명할 수 있는 기반이 되었고, 단순히 개인적으로 학습하는 것에서 그치는 것이 아니라, TanStack Router의 공식 문서를 한글로 번역하여 배포하는 것까지 진행했다.
이를 통해, Tanstack Router를 학습하기 위한 다른 개발자들에게도 도움이 될 수 있다고 생각했다.

DNS 등록 후 google search console 설정까지 마쳐 해당 키워드로 검색하면 최상단에 노출된다.
이번 Next.js에서 React.js + TanStack Router로의 마이그레이션을 진행하면서,
단순히 기술을 변경하는 작업이 아니라, 기술 스택을 선택하는 과정 자체가 얼마나 중요한지 다시 한번 깨닫게 되었다.
이 과정에서, "Next.js 없이도 충분히 좋은 서비스를 만들 수 있다"는 확신을 가지게 되었다.
이번 경험을 통해, "무조건 신기술이 좋은 것은 아니다"라는 점을 다시 한번 실감했다.
기술을 도입하는 것은 단순히 "더 좋은 기술이니까 바꿔야 한다"가 아니라,
"현재의 서비스와 팀의 상황에서 최적의 선택이 무엇인가"를 고려해야 한다는 점을 다시 한번 배우게 되었다.
이번 마이그레이션에서 공식 문서만 보고 진행했다면,
AbortController 미작동 문제나 beforeLoader와 loader의 실행 순서 차이 같은 문제들은 쉽게 해결할 수 없었을 것이다.
결국, 직접 코드 레벨에서 분석하고, 콘솔을 찍고 디버깅을 하며 내부 동작 방식을 이해해야만 문제를 해결할 수 있었다.
이 과정에서, 단순히 문서를 읽고 따라 하는 것만으로는 기술을 온전히 이해할 수 없으며, 직접 실험하고 분석하는 과정이 필수적이라는 점을 깨달았다.
마이그레이션을 진행하면서 TanStack Router의 loader에서 AbortController를 활용한 데이터 페칭 취소 기능이 제대로 동작하지 않는 문제를 발견했다.
공식 문서만으로는 원인을 찾을 수 없었기 때문에, 라이브러리의 코드를 직접 분석하여 문제를 해결했다.
이 과정에서,
이전까지는 라이브러리를 그저 "사용하는 것"에만 익숙했는데,
이번 경험을 통해 오픈 소스 라이브러리의 코드 구조를 분석하고, 직접 수정하여 기여하는 과정이 얼마나 중요한지 배울 수 있었다.
Next.js를 도입했던 초기에는 "좋은 프레임워크니까 사용해야 한다"라는 접근 방식이었지만,
결국 우리 서비스의 요구사항과 운영 환경에는 맞지 않는 선택이었다.
React.js와 TanStack Router로 전환하는 과정에서도,
TanStack Router의 강력한 기능이 우리 서비스에 실질적으로 도움이 되는지를 검토하는 과정이 필수적이었다.
Next.js에서 벗어나면서 가장 크게 배운 점은,
"최신 기술을 도입하는 것보다, 지금 우리 서비스에 최적화된 기술을 선택하는 것이 더 중요하다"는 것이다.
뿐만 아니라, 오픈 소스 라이브러리를 그저 사용하는 것이 아니라, 직접 코드 분석을 하고, 기여하는 경험까지 하면서 기술에 대한 시야를 넓힐 수 있었다. 라이브러리의 내부 구조를 분석하고 문제를 해결하는 과정뿐만 아니라, 공식 문서를 번역하면서 라이브러리에 대한 깊은 이해를 쌓았고, 이를 팀원들과 함께 공유하는 과정에서 기술을 더욱 입체적으로 바라볼 수 있었다.
결국, Next.js 없이도 충분히 좋은 서비스를 만들 수 있었고, 앞으로도 기술을 선택할 때는 우리 환경에 맞는 최선의 선택을 하는 것이 가장 중요하다는 것을 배웠다.