Vue 2의 지원 중단
2023년 12월 31일을 기점으로 프레임워크 Vue 2는 공식적으로 지원이 종료(EOL, End of Life)되었습니다. 지원이 종료된 프레임워크를 계속 사용하는 경우, 향후 보안 취약점에 노출될 가능성이 높고, 최신 브라우저나 라이브러리와의 호환성에서도 문제가 발생할 수 있습니다. 또한, 보안 패치나 기능 업데이트가 더 이상 제공되지 않는 상태에서 Vue 2를 사용하는 것은 기술 부채를 쌓는 일과 다르지 않다는 프론트엔드 팀 내부의 의견이 있었습니다. 특히 Vue 2로 개발된 프로젝트는 소스 서비스를 전반적으로 관리하고 조정하는 중심 역할의 어드민 서비스였기 때문에, 기술적 리스크는 물론 다른 서비스들과의 연계성과 확장성 측면에서도 신중한 판단이 필요했습니다. 이러한 상황에서 Vue 3로의 마이그레이션을 고려하거나, React 등의 다른 프레임워크로의 마이그레이션을 검토할 필요가 있었습니다.
왜 Next.js 였을까?
Vue 3는 Composition API, 향상된 타입 지원, 성능 최적화 등 다양한 면에서 Vue 2보다 발전된 프레임워크임은 분명했습니다. 그러나 Vue 3로의 마이그레이션을 바로 결정하기에는 현실적인 제약들이 존재했습니다. 수년간 누적된 기능과 로직들이 스파게티처럼 꼬여있어 기획과 코드 흐름을 완전히 파악하기 어려웠습니다. 이런 상태에서는 Vue 3 로 바로 마이그레이션을 진행해도 유지보수가 어려워 보였습니다. 또한, 프레임워크를 Vue 3로 교체한다면 전체 QA 를 새로 진행할텐데, Vue 3 말고 다른 옵션을 선택해도 동일한 리소스가 필요할 것이 자명했습니다. 따라서 Vue 3 외의 다른 옵션도 고려하게 되었습니다.
프론트엔드 팀이 가장 중점적으로 고려한 부분은 개발 및 유지 보수 효율성 이었습니다.
소스 서비스에는 방송 외에도 방송에서 파생된 콘텐츠를 고객사들이 활용할 수 있도록 돕는 10개 이상의 다양한 프로덕트가 존재하며, Vue 2로 작성된 어드민 서비스를 제외한 모든 프로젝트가 React 혹은 Next.js를 기반으로 개발되고 있었습니다. 특히, 다른 프로덕트를 개선하거나 신규 기능을 도입할 때는 콘텐츠 관리의 중심이 되는 어드민 서비스에도 변경이 필연적으로 필요하기 때문에, 기술의 일관성은 곧 생산성과 안정성에 직접적인 영향을 미친다고 판단했습니다. 이러한 맥락에서 Vue 3로의 단독 마이그레이션보다는, React 및 Next.js 기반으로 기술 스택을 통합하는 것이 유지보수와 개발 리소스 측면에서 더욱 합리적이라는 결론에 도달했습니다.
이 과정에서 React와 Next.js 중 어떤 옵션을 선택할 것인가에 대한 내부 논의도 있었습니다. 최종적으로는 Next.js가 다음과 같은 기술적 이점을 제공한다는 점에서 선택되었습니다.
파일 기반 라우팅 시스템 덕분에 라우팅 구조가 직관적이고 명확하게 관리되어, 협업이 잦고 과거 변경 이력을 추적하기 어려운 상황에서도 유지보수가 용이했습니다.
next/image 컴포넌트를 통한 이미지 최적화 기능은 방송 특성상 대용량 이미지 리소스를 많이 다루는 어드민 서비스에 적합한 성능 개선 요소였습니다.
자동 코드 스플리팅, prefetching, 기본적인 성능 최적화 기능이 프레임워크 레벨에서 기본 제공되어, 사용자 경험과 초기 로딩 속도 모두에서 유리했습니다.
SSR, SSG, ISR 등 다양한 렌더링 옵션을 통해 페이지의 성격에 따라 유연하게 전략을 구성할 수 있는 점도 큰 강점이었습니다.
달리는 자동차의 바퀴를 안전하게 바꾸는 전략: 점진적 마이그레이션
마이그레이션 대상 프로젝트는 다수의 고객사들이 사용 중인 서비스였으며, 고객사로부터 발생하는 VOC를 기반으로 기능 개선 및 신규 개발이 지속적으로 이루어지고 있었습니다. 이러한 상황에서 모든 페이지를 한 번에 마이그레이션하게 되면, 제한된 개발 리소스가 전환 작업에 집중되면서 다른 작업들의 우선순위가 상대적으로 낮아질 수 있다는 우려가 있었습니다. 따라서 프론트엔드 팀에서는 프레임워크를 페이지 단위로, 점진적으로 마이그레이션하는 전략을 수립하였습니다.
우선 고객 VOC 혹은 운영 필요성에 따른 작업을 진행할 때 해당 페이지를 Next.js 기반으로 개발 및 배포했습니다. 이후 남은 페이지들에 대해 도메인 별로 범위를 나누어 여유가 있는 스프린트마다 일부 범위를 작업하는 식으로 개발 범위를 확대해 나갔습니다. 이러한 점진적 마이그레이션을 통해 기존 시스템의 동작 방식 및 히스토리를 파악, 기획 등 타 부서와 공유하며 도메인에 대한 이해를 향상시킬 수 있었습니다.
Next.js로 개발된 새로운 페이지는 기존 Vue 프레임워크 내부에 iframe 형태로 통합하였고, 각 페이지는 QA를 통해 기능 및 호환성 테스트를 거쳤습니다. 이를 통해 전체 시스템의 안정성을 해치지 않으면서도 점진적인 기술 마이그레이션을 실현할 수 있었고, 동시에 새로운 기술 스택의 적용에 따른 리스크를 최소화할 수 있었습니다.
마이그레이션이 거의 끝나갈 때쯤, 프로젝트 구성 요소의 변경이 필요해졌습니다. 기존에는 Vue 기반 프레임워크를 루트로 하여, 그 내부에 각 Next.js 페이지들을 iframe으로 연결해 QA를 병행하는 구조였지만, 마이그레이션이 어느 정도 완료된 시점에서는 Next.js를 메인 엔트리로 삼고, 필요한 Vue 페이지만을 iframe 형태로 포함하는 방식으로 구조를 변경한 후 검증을 진행할 필요가 있었습니다.
우선적으로 인증 흐름 및 메뉴 시스템을 Next.js 기반으로 별도 구성하고, 기존 Vue 프로젝트의 토큰 관리 및 인증 연동을 Next.js 측으로 이관하였습니다. 이를 통해 인증, 메뉴, 페이지 라우팅 등 사용자 진입 경로의 일관성을 확
보하면서, Next.js로 구현된 페이지와 Vue로 구현된 페이지 간의 연결이 자연스럽게 이어지도록 구성하였습니다.
기존 유저들의 사용성 역시 중요한 고려 요소였습니다. 기존에는 Vue 기반의 서비스가 진입 주소를 담당하고 있었으며, 점진적인 Next.js 마이그레이션을 진행하는 과정에서도 이 주소 체계를 유지하는 것이 중요했습니다. 특히 많은 고객들이 해당 주소를 즐겨찾기로 저장해두고 있었기 때문에, 주소 변경 없이 기존 URL을 그대로 사용하는 것이 UX 측면에서도 더 바람직하다고 판단했습니다. 이를 위해 기존 Vue의 진입 주소를 Next로 위임하고, Vue의 실제 렌더링 주소는 별도로 재정의하여 개발했습니다.
방송 도메인의 특성상 실시간성이 요구되는 만큼, 사용자 경험에 영향을 최소화하기 위해 철저한 QA 프로세스를 적용하였습니다. dev 환경에서는 새로운 기능 반영 후 약 2주간의 사전 QA(Pre-QA)를 진행하였고, 안정성이 확보된 이후 stage 환경에 배포되도록 하여, 최종적으로 prod 환경에 배포 시 발생할 수 있는 리스크를 최소화하였습니다.
이렇게 총 80여 페이지 중, 마이그레이션이 불필요한 페이지와 별도의 기획이 필요한 페이지들을 제외한 모든 페이지의 마이그레이션을 완료하였습니다. 점진적으로 꾸준히 작업했기에 기존 서비스의 사용성은 유지하면서도 꾸준히 배포와 안정적인 QA를 병행할 수 있었고, 전체 시스템의 유지보수성과 확장성 또한 크게 향상되었습니다.
확장성과 안정성을 위한 기술적 고민들
1. 유연한 프레임워크에서 구조화된 프레임워크로
Vue 2 프로젝트의 폴더 구조는 약 5년간 별다른 관리 없이 방치되어 왔습니다. 그 결과, 공통 유틸리티와 컴포넌트가 뒤섞여 있거나, API 호출 로직과 유틸 함수가 한 폴더에 섞여 있는 등 일관성 없는 구조가 자리잡게 되었습니다. 시간이 지날수록 개발자 간의 컨벤션 차이는 심해졌고, 이는 유지보수의 큰 장애물이 되었습니다.
Next.js로의 마이그레이션은 프로젝트 구조 자체를 근본적으로 혁신하는 계기가 되었습니다. Next.js의 파일 기반 라우팅과 폴더 구조 컨벤션을 받아들이면서, 프론트엔드 팀은 기존에 혼재되어 있던 코드들을 기능과 역할별로 명확히 분리하여 재배치했습니다.
예를 들어, Vue 2에서는 src/common
폴더 아래에 API, 유틸리티 함수, 공통 컴포넌트 등이 뒤섞여 있었고, 각 페이지의 컴포넌트와 서비스 코드 역시 일관성 없이 흩어져 있었습니다. 도메인별 컴포넌트 구분도 모호하고, 유틸 함수와 API 코드도 common/utils
, common/api
에 느슨하게 나누어져 있었습니다.
Next.js로 마이그레이션한 이후에는 폴더 구조가 곧 라우팅 구조가 되는 파일 기반 컨벤션을 따르게 되었습니다. 각 페이지에서 사용하는 컴포넌트와 비즈니스 로직은 도메인별 디렉토리에 명확하게 배치했고,
공통 컴포넌트, 유틸리티 함수, API 모듈 등은 shared
하위에 역할별로 분리해 어떤 코드가 어떤 역할을 하는지 훨씬 명확해졌습니다.
이 과정에서 도메인 단위로 기능을 재구성하고, 불필요하게 섞여 있던 레거시 코드들을 과감하게 정리했습니다. 결과적으로 협업과 코드 리뷰가 쉬워졌고, 신규 기능 추가와 유지보수의 효율도 크게 향상되었습니다.
2. CSR에서 SSR로의 사고방식 전환
Vue 2 프로젝트는 모든 페이지가 클라이언트 사이드 렌더링(CSR)을 전제로 동작했습니다. 서버는 단순히 HTML과 번들 파일만 내려주고, 나머지 모든 데이터 처리와 렌더링은 브라우저에서 처리하는 구조였죠.
이런 CSR 기반의 애플리케이션을 Next.js의 서버 사이드 렌더링(SSR) 환경으로 옮기는 과정은, 프론트엔드 팀에게 기술적으로도, 심리적으로도 새로운 도전이었습니다.
Next.js 도입 이후, 프론트엔드 팀은 각 페이지의 특성과 실제 서비스 흐름에 맞게 SSR, CSR, SSG를 혼합해서 사용하는 방식을 자연스럽게 선택하게 됐습니다.
처음에는 선택 기준이 명확하게 수립되지 않아 불필요한 SSR이 꽤 있었고 이로 인해 오히려 Vue 2 에 비해 페이지 성능이 저하되는 경우도 있었습니다. 이런 시행착오를 거치면서, 프론트엔드 팀은 실제 서비스 상황에 맞는 SSR 적용 기준을 하나씩 합의해나갔습니다.
특히, 사용자가 페이지에 진입할 때 주요 데이터를 즉시 볼 수 있어야 하는 화면은 SSR로, 반대로 인터랙션이 많거나 실시간성이 중요한 페이지(예: 채팅, 실시간 목록 등)는 CSR로 남겨두는 등 페이지별 특성에 따라 렌더링 방식을 고민하게 되었습니다.
2-1. @Tanstack/React-Query와의 결합
SSR 환경에서는 서버에서 데이터를 미리 패칭해서 내려주는 것이 핵심인데, 이때 @Tanstack/React-Query를 함께 사용하면서 새로운 패턴을 익혀야 했습니다.
서버에서 @Tanstack/React-Query로 데이터를 prefetch하고, 이를 dehydratedState로 클라이언트에 전달해, 클라이언트가 마운트될 때 바로 캐싱 데이터를 사용할 수 있도록 하는 구조였습니다.
하지만 초기에는 Query Key 설계, 캐싱 전략, staleTime 설정 등에서 시행착오를 많이 겪었고, 동적인 쿼리 구조나 의존성 관리에서도 헷갈리는 부분이 많았습니다. 이로 인해 SSR 과정에서 prefetch를 했음에도 Query Key가 달라서 CSR에서 다시 패칭하는 등의 비효율적인 동작이 많았습니다.
결국 Effective React Query Keys 같은 외부 레퍼런스를 참고해서 Query Key Factory 패턴을 적용하고, 상태와 데이터 흐름의 일관성을 높일 수 있었습니다.
const clipQueryKeys = {
base: ['clip'],
detail: (params: ClipRequest['get']) => [...clipQueryKeys.base, sortAndFilterObject(params)],
list: (params: ClipRequest['getList']) => [...clipQueryKeys.base, sortAndFilterObject(params)],
...
} as const
2-2. Hydration Mismatch
Next.js로 SSR을 도입하면서 Hydration Mismatch 문제를 자주 마주치게 되었습니다.
예를 들어, 서버에서 생성한 HTML과 클라이언트에서 동적으로 계산한 값이 다를 때 Text content did not match
와 같은 경고가 발생했습니다.
실제로 날짜/시간을 렌더링하는 부분이나 브라우저 API(window, document 등)를 사용하는 컴포넌트, 혹은 랜덤 값을 생성하는 영역에서 이러한 문제가 두드러졌습니다.
프론트엔드 팀은 이를 해결하기 위해
클라이언트 전용 코드에는 useEffect를 사용하거나,
SSR이 필요 없는 컴포넌트는
next/dynamic
의{ssr: false}
옵션을 적용해,
서버와 클라이언트의 렌더 결과가 일치하도록 코드를 리팩터링했습니다.
또한 CSR 환경에서는 로컬 스토리지와 같은 웹 스토리지를 적극적으로 활용했는데, UI가 이 값에 의존해 변할 경우에도 Hydration Mismatch가 발생할 수 있었습니다.
SSR된 HTML이 브라우저에 내려온 시점에는 로컬 스토리지 값을 알 수 없기 때문에, JS가 실행되어 로컬 스토리지 값을 읽고 UI가 다시 변하면서 Mismatch가 일어나는 것이었습니다.
이 문제를 해결하기 위해 HTML <head>
에 스크립트를 직접 삽입해, 자바스크립트 초기 구동 전에 로컬 스토리지 값을 먼저 읽어와 전역 변수로 노출하는 방식을 사용했습니다.
이렇게 하면 SSR로 내려오는 HTML과 브라우저에서 첫 렌더되는 값이 일치하게 되어, Hydration Mismatch를 예방할 수 있었습니다.
3. Vue 2 ↔ Next.js 간의 데이터 주고받기
Next.js로의 마이그레이션 과정에서 Vue 2 앱과 Next.js 앱이 일정 기간 공존하는 시기를 가졌습니다. 이때 가장 큰 기술적 챌린지 중 하나는 Vue 2와 iframe 기반의 Next.js 앱 간의 전달이였습니다. 특히 세션 정보, 인증 토큰, 사용자 언어 설정, URL 쿼리 파라미터 등의 데이터가 양 앱 간에 정확히 전달될 필요가 있었습니다.
3-1. postMessage API
프론트엔드 팀은 이 문제를 해결하기 위해 iframe 간 데이터 통신을 지원하는 postMessage API를 적극적으로 활용했습니다. Vue2의 부모 페이지에서 iframe으로 구성된 Next.js 앱에 메시지를 전달하고, 반대로 iframe 내에서 발생하는 이벤트나 상태 변화를 부모 페이지로 알릴 때도 postMessage를 사용하여 양방향 통신을 구현했습니다.
- 메시지 출처(origin) 검증
여러 환경에서 postMessage를 사용하다 보면, 의도하지 않은 외부 메시지가 들어올 수 있습니다.
프론트엔드 팀은 항상 event.origin
을 체크해서, 신뢰할 수 있는 도메인에서만 메시지를 처리하도록 했습니다.
window.addEventListener('message', (event) => {
if (event.origin !== 'https://프론트엔드 도메인') return;
...
});
- 메시지 타입 구분 및 스키마화
메시지 종류가 늘어나면서 혼동이 발생할 수 있었기 때문에, type, payload 구조로 메시지를 스키마화해 예상치 못한 메시지는 무시하거나 로깅하도록 설계했습니다.
특히 TypeScript를 적극적으로 활용해, 메시지 타입을 enum으로 정의하고 각 메시지의 payload 형태를 인터페이스로 명확하게 타입을 선언했습니다.
이렇게 하면 메시지 송수신 과정에서 타입 안전성이 보장되고, 협업 시 실수도 크게 줄일 수 있었습니다.
{
type: 'SET_LANGUAGE',
payload: { lang: 'en' }
}
- 세션/토큰 만료 동기화
인증 토큰이 만료됐을 때, 부모와 iframe이 각각 따로 로그아웃 처리되는 문제가 발생했습니다. 이때는 부모와 iframe 모두에게 LOGOUT 메시지를 전달해서, 동시에 세션을 만료시키는 처리를 추가했습니다.
3-2. 쿼리 파라미터
iframe으로 Next.js 앱을 로드할 때 URL 쿼리 파라미터를 통해 초기 데이터를 전달했습니다. 예를 들어 사용자의 언어 설정이나 특정 상태 정보를 URL에 포함해 전달함으로써, Next.js 앱이 iframe 내에서 초기 렌더링 단계부터 정확한 데이터를 기반으로 동작하도록 구성했습니다.
이러한 접근 방식을 통해 양쪽 앱 간의 데이터 일관성을 유지할 수 있었고, 사용자가 매끄럽고 일관된 조작을 할 수 있었습니다. 이 과정에서 iframe 기반 점진적 마이그레이션 전략의 실용성과 효과를 확인할 수 있었습니다.
맺으며
마이그레이션 과정은 쉽지 않았고, 기술적·조직적으로 많은 도전이 따랐습니다. 하지만 그만큼 팀 전체가 더 깊이 있는 고민을 하게 되었고, 다음과 같은 의미 있는 성과들을 얻을 수 있었습니다.
프로젝트를 면밀히 분석하고 이를 다른 팀과 적극적으로 공유하며 서비스 도메인에 대한 공동 이해를 높이고 협업의 밀도를 강화할 수 있었습니다.
개발 팀에서 PM을 맡아 관련 팀들과 주도적으로 의사소통하여 기능 충돌이나 일정 지연 등의 리스크를 사전에 최소화할 수 있었습니다.
개발 단계에서의 대응 속도와 정확도가 전반적으로 향상되었고, 프로젝트를 성공적으로 마무리하며 팀 전체의 자신감 또한 크게 향상되었습니다.
무엇보다도, 마이그레이션 과정에서 발생한 다양한 이슈들을 하나하나 해결해 나가며 팀원 개개인 역시 실질적인 기술적 성장과 문제 해결 역량을 키울 수 있었습니다.
집중도 높은 커뮤니케이션을 반복적으로 나눈 경험을 바탕으로 더 체계적으로 소통할 수 있게 되었고, 개발 프로세스와 코드 품질에 대한 기준 역시 한층 더 끌어올릴 수 있었습니다.
이번 경험은 팀의 기술적 역량과 자율적 성장 가능성을 입증한 전환점이었으며, 앞으로의 복잡한 문제에도 유연하게 대응할 수 있는 기반이 되었습니다.
모비두 프론트엔드 팀은 앞으로도 기술과 협업 전반에 걸쳐 더 높은 기준을 세우며, 지속적인 개선을 통해 흔들리지 않는 기술 조직으로 성장해 나가겠습니다.