Development Log
U+모아tv TanStack Query v5 마이그레이션과 iOS 14 WebView 호환성 대응.
U+모아tv의 React Query v3 데이터 계층을 TanStack Query v5로 전환하고, iOS 14 WebView의 최신 문법 호환 문제를 빌드 단계에서 해결한 사례입니다.
- #Next.js
- #TanStack Query
핵심 요약 React Query v3가 홈·상세·검색·댓글·평점·마이페이지와 SSR까지 광범위하게 연결된 상황에서, 단순 의존성 교체로는 서비스 동작을 보존하기 어려웠다. 이에 Query Key와 실행 조건 등 기존 데이터 조회 의미를 유지하면서 TanStack Query v5의 규격으로 전환하고, 마이그레이션 후 확인된 iOS 14 WebView의 패키지 문법 호환 문제는 빌드 단계에서 별도로 해결했다. 성과: 77개 파일에 걸친 데이터 계층을 v5 규격으로 정비하고 기존
react-queryimport를 제거했으며, SSR Hydration·Infinite Query·Mutation 갱신 흐름을 유지했다. 또한 TanStack 패키지를 Transpile 대상으로 포함해 구형 iOS WebView까지 지원할 수 있는 빌드 경로를 확보했다.
- 프로젝트: 기존 U+모아tv
- 기간: 2025-04-23 ~ 2025-07-01
- 환경: Next.js 12.3.5 / React 18.2 / TypeScript 4.7.4 / TanStack Query 5.74.4 / iOS 14 WebView
- 개인 기여 근거:
03fb224e,4dec13a0 - 저장소:
/Users/gimdongmin/project/LGUplus/moa
1. 문제 정의
1.1 데이터 계층이 React Query v3에 광범위하게 결합
기존 U+모아tv는 홈·상세·검색·댓글·평점·마이페이지 등 주요 화면의 서버 상태를 React Query v3로 관리하고 있었다. 홈과 일부 페이지는 getServerSideProps에서 데이터를 Prefetch한 후 Dehydrate하여 브라우저에 전달하는 SSR 구조도 사용했다.
따라서 버전 전환은 패키지 이름만 바꾸는 작업이 아니었다. 일부 코드만 잘못 전환해도 다음과 같은 회귀가 발생할 수 있었다.
- Query Key 변경에 따른 캐시 분리·충돌
enabled조건 누락으로 인한 불필요한 API 호출- SSR Prefetch 데이터가 재사용되지 않아 발생하는 중복 요청
- Infinite Query의 마지막 페이지 중복 조회 또는 누락
- 댓글·좋아요·신고 Mutation 이후 화면 미갱신
- Query callback에 의존하던 화면 상태·제목·오류 처리 누락
1.2 v5의 주요 Breaking Change를 한 번에 다뤄야 함
TanStack Query v5에서는 기존 위치 기반 Hook 인자가 객체 문법으로 통일됐고, Hydration 경계, Infinite Query 초기값, 캐시 보존 옵션, Mutation·캐시 무효화 API가 달라졌다. 또한 useQuery의 onSuccess·onError에 의존하던 화면 부수효과도 별도 생명주기로 옮겨야 했다.
즉, 문법 변환과 함께 기존 조회·캐시·페이지네이션의 의미를 보존하는 것이 핵심 문제였다.
1.3 최신 패키지와 iOS 14 WebView의 빌드 대상 불일치
v5 전환 후에는 TanStack 패키지 내부의 최신 JavaScript 문법을 구형 iOS WebView가 처리할 수 있도록 빌드 범위를 보완해야 했다. Next.js 12에서 애플리케이션 코드는 Babel을 거치지만, node_modules 패키지는 동일한 수준으로 변환되지 않을 수 있었다.
서비스가 iOS 14 WebView도 지원해야 했기 때문에 최신 Chrome·Safari에서 동작하는 것만으로 마이그레이션을 완료했다고 볼 수 없었다. TanStack 패키지까지 실제 Transpile 대상에 포함하는 조치가 필요했다.
2. 해결 목표와 제약
| 구분 | 목표 | 보존해야 할 조건 |
| API 전환 | React Query v3 의존성 제거 및 v5 규격 통일 | Query Key·`enabled`·`staleTime` 의미 유지 |
| SSR | v5 Hydration 구조 적용 | 서버 Prefetch 데이터의 클라이언트 재사용 |
| 목록 조회 | Infinite Query 규격 전환 | 초기 페이지와 종료 조건의 정확성 |
| Mutation | v5 Mutation·무효화 API 적용 | 사용자 동작 이후 필요한 목록만 갱신 |
| WebView | TanStack 패키지 문법을 iOS 14에서 처리 가능하게 변환 | 로컬·운영 빌드 설정의 일관성 |
3. 해결 전략
전략 1. 변경 범위를 기능이 아니라 Query 사용 유형으로 분류
화면별로 임의 수정하지 않고 Query 사용 방식을 기준으로 변경 범위를 나눴다.
- Provider와 SSR Hydration
useQuery·useQueries기반 조회useInfiniteQuery기반 목록useMutation과 캐시 무효화- Query callback을 사용하는 화면 부수효과 이렇게 분류해 동일한 API 패턴을 일관되게 전환하고, 빠질 가능성이 높은 영역을 줄였다.
전략 2. 기존 데이터 조회의 의미를 우선 보존
Query Key 구성, 로그인·파라미터 기반 enabled 조건, staleTime, 재조회 조건은 기존 동작을 기준으로 유지했다. v5 문법으로 바꾸면서 캐시의 식별 기준이나 API 호출 시점이 의도치 않게 달라지지 않도록 했다.
전략 3. v5에서 달라진 규격은 명시적인 코드로 전환
- 위치 기반 Hook 인자를 객체 문법으로 통일
Hydrate를HydrationBoundary로 교체- Query callback 기반 부수효과를
useEffect로 분리 - Infinite Query에
initialPageParam명시 cacheTime을gcTime으로 변경- 다음 페이지 계산을
Math.ceil기반의 명확한 종료 조건으로 정리 - Mutation 함수와 캐시 무효화 대상을 객체 옵션으로 명시
전략 4. iOS 14 문제는 애플리케이션 우회가 아닌 빌드 경계에서 해결
TanStack Query 호출부마다 우회 코드를 넣는 대신, @tanstack/react-query와 @tanstack/query-core를 Transpile 대상으로 포함했다. Babel 플러그인은 로컬·운영 설정에 함께 적용해 환경별 결과 차이를 줄였다.
4. 실제 변경 내용
4.1 React Query v3 의존성 제거
03fb224e에서 react-query 3.39.1을 제거하고 @tanstack/react-query 5.74.4를 적용했다. TypeScript는 4.5.5에서 4.7.4로 함께 상향했다.
| 구분 | 변경 범위 | 대표 내용 |
| API Hook | 6개 파일 | `useQuery`·`useQueries`·`useInfiniteQuery` 규격 전환 |
| 화면·컴포넌트 | 53개 파일 | Query callback·Mutation·로딩 및 오류 상태 정비 |
| 페이지 | 13개 파일 | SSR Prefetch·Dehydrate·Hydration 연계 전환 |
| 설정·기타 | 5개 파일 | 패키지·TypeScript·Provider 설정 변경 |
| 전체 | 77개 파일 | 1,106줄 추가·988줄 삭제 |
마이그레이션 커밋 시점 기준 @tanstack/react-query를 사용하는 소스 파일은 61개였고, 기존 react-query import는 0개로 정리됐다.
4.2 Query Hook 객체 문법 적용
// Before: React Query v3
useQuery(
['contentInfo', albumId, catId],
() => getNXContInfo(params),
{
enabled: !!albumId && !!catId,
staleTime: 3000,
}
);
// After: TanStack Query v5
useQuery({
queryKey: ['contentInfo', albumId, catId],
queryFn: () => getNXContInfo(params),
enabled: !!albumId && !!catId,
staleTime: 3000,
});
queryKey·queryFn·실행 조건을 명시적으로 분리하면서 기존 Query Key와 호출 조건은 유지했다.
4.3 SSR Hydration 경계 전환
// Before
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
// After
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<Component {...pageProps} />
</HydrationBoundary>
</QueryClientProvider>
Pages Router의 getServerSideProps에서 수행하던 Prefetch·Dehydrate 흐름은 유지하고, 브라우저에서 서버 Query Cache를 이어받는 경계만 v5 규격으로 전환했다.
4.4 Query callback과 화면 부수효과 분리
const {
data: contentInfo,
isSuccess,
isError,
} = useQuery({
queryKey: ['contentInfo', albumId, catId],
queryFn: () => getNXContInfo(params),
enabled: !!albumId && !!catId,
});
useEffect(() => {
if (isSuccess && contentInfo) {
updateTitle(contentInfo.record.album_name);
} else if (isError) {
updateIsError(true);
}
}, [isSuccess, isError, contentInfo]);
데이터 조회와 화면 제목·오류 상태 변경을 분리해 Query 상태 변화가 React 생명주기 안에서 처리되도록 했다.
4.5 Infinite Query 초기값과 종료 조건 정비
useInfiniteQuery({
queryKey: ['MyFavoriteList', pageCount, userKey, order],
queryFn: ({ pageParam = 0 }) => {
const pageNo = pageParam + 1;
return getMyFavorite({ pageNo, pageCount, userKey, order });
},
initialPageParam: 0,
getNextPageParam: (lastPage) => {
const { pageNo, totalCount } = lastPage.data;
return pageNo < Math.ceil(totalCount / 20)
? pageNo
: undefined;
},
gcTime: 0,
});
v5에서 필수가 된 초기 페이지를 명시하고, 전체 개수가 페이지 크기로 나누어떨어지지 않는 경우까지 반영해 마지막 페이지 조건을 정리했다.
4.6 Mutation과 캐시 무효화 범위 명시
const like = useMutation({
mutationFn: getCommentLike,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['commentList'],
type: 'active',
});
},
});
문자열 Query Key와 refetchInactive 조합 대신 queryKey와 type: 'active'로 실제 갱신 대상을 명확히 했다.
4.7 TanStack 패키지 Transpile과 Babel 설정
4dec13a0에서 TanStack Query의 구형 브라우저 지원을 위해 두 패키지를 빌드 변환 범위에 포함했다.
const withTM = require('next-transpile-modules')([
'@tanstack/react-query',
'@tanstack/query-core',
]);
module.exports = withTM(nextConfig);
.babelrc와 .babelrc_prd에는 동일한 문법 변환 플러그인을 적용했다.
{
"presets": ["next/babel"],
"plugins": [
["babel-plugin-styled-components", {
"fileName": true,
"displayName": true,
"pure": true
}],
"@babel/plugin-proposal-private-methods",
"@babel/plugin-proposal-class-properties"
]
}
Babel 플러그인만 추가하면 node_modules 내부의 TanStack 패키지가 변환 대상에서 빠질 수 있으므로, 패키지 Transpile과 문법 변환 설정을 함께 적용했다.
5. 성과
| 성과 | 확인 가능한 결과 |
| 데이터 계층 표준화 | 기존 `react-query` import 0개, TanStack Query v5 사용 소스 61개로 통일 |
| 대규모 전환 완료 | API Hook·화면·페이지·설정을 포함한 77개 파일 마이그레이션 |
| SSR 구조 유지 | Prefetch·Dehydrate 흐름을 유지하고 `HydrationBoundary`로 전환 |
| 페이지네이션 안정성 정비 | `initialPageParam`과 마지막 페이지 종료 조건을 명시 |
| 캐시 갱신 의도 명확화 | Mutation 이후 활성 Query를 대상으로 무효화 범위 명시 |
| iOS 14 호환 계층 확보 | TanStack 패키지를 Transpile하고 로컬·운영 Babel 설정을 동일하게 보완 |
이 작업의 성과는 “v5로 바꿨다”는 데 그치지 않는다. 상용 서비스 전반에 퍼진 데이터 조회 코드를 새로운 API로 전환하면서 SSR 데이터 재사용, 사용자 조건별 요청, Infinite Query, Mutation 이후 갱신이라는 기존 서비스 동작을 함께 보존했다. 또한 패키지 내부 문법까지 빌드 범위에 포함해 지원 대상인 iOS 14 WebView를 마이그레이션 완료 조건에 포함했다.
6. 검증 근거와 해석 범위
소스로 확인 가능한 내용
- React Query 3.39.1 제거 및 TanStack Query 5.74.4 적용
- 77개 파일, 1,106줄 추가·988줄 삭제 규모의 마이그레이션
- TanStack Query 사용 소스 61개와 기존 import 0개
HydrationBoundary·initialPageParam·gcTime·객체형 Mutation 적용@tanstack/react-query·@tanstack/query-coreTranspile 설정- 로컬·운영 Babel 설정의 private method·class property 변환 추가
성과 표현 시 주의 당시 iOS 14 기기의 JavaScript 오류 원본 로그와 수정 전·후 번들 분석 결과는 남아 있지 않다. 따라서 특정 오류 메시지, 영향 사용자 수, 성능 개선율을 임의로 적지 않는다. 경력기술서에서는 “iOS 14 WebView 호환성 보완” 또는 “구형 WebView용 빌드 변환 경로 확보”로 표현하는 것이 정확하다.
7. 회고 및 재발 방지
- 주요 라이브러리 상향 전 Hook API뿐 아니라 SSR·캐시·Mutation·Infinite Query의 변경점을 체크리스트로 관리
- Query Key와
enabled조건을 마이그레이션 전후 비교 항목으로 고정 - 서버 Prefetch 데이터의 Hydration과 브라우저 재요청 여부를 대표 회귀 시나리오로 검증
- 지원 브라우저와 새 패키지의 배포 문법을 함께 확인
- 애플리케이션 코드뿐 아니라 교체되는
node_modules패키지가 실제 빌드에서 Transpile되는지 확인 - 개발·운영 Babel 설정을 함께 변경해 환경별 빌드 차이 방지