일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 알고리즘스터디
- WebDev
- 리액트쿼리
- frontend
- Design system
- 개발공부
- 개발
- 리액트
- 백준
- NextJs
- 웹프로토콜
- 개발자성장
- 개발블로그
- Nest.js
- react
- 그룹단어
- prettier
- securitypatch
- nextjsmiddleware
- 웹개발
- jotai
- TypeScript
- 모음의개수
- Next.js
- 알고리즘
- 프론트엔드
- 코드개선
- React Query
- 백준1264
- 디자인 시스템
- Today
- Total
한땀한땀
Next.js 13에서 15로 마이그레이션 본문
현재 회사의 프로젝트는 Next.js 13을 기반으로 개발되었으며, 최신 버전인 Next.js 15로 마이그레이션을 결정하였다.
왜 진행했는가?
최신 기능 및 성능 개선
- 최신 React 및 Next.js의 최적화 기능을 활용할 수 있다.
- 더 빠른 빌드 및 서버 렌더링 속도를 기대할 수 있다.
- Hydration error 관련 개선이 이루어져, 구체적인 소스 코드 및 해결 방법을 제시해주기 때문에 디버깅에 좀 더 수월해질 것 같다.
App Router 정식 지원 및 서버 컴포넌트 활용
Next.js 13에서 실험적으로 도임된 app 디렉토리 기반의 라우팅이 Next.js 15에선 더욱 안정적으로 지원되고, 서버컴포넌트를 활용하여 코드를 좀 더 직관적이고 간결하게 유지할 수 있을 것 같다.
기존 Pages 라우터에서는 특정 페이지에서 getServerSideProps를 활용하여 아래와 같이 구현하였다.
export const getServerSideProps: GetServerSideProps = async ctx => {
const queryClient = new QueryClient();
const defaultFilter = {
size: 30,
page: 0,
};
queryClient.prefetchQuery({
...krReitsQueries.getReitsDividendNoticeList(),
});
const marketListResponse = await queryClient.fetchQuery(marketListQueries.list({ filters: defaultFilter }));
const allReitIdList = marketListResponse.data.map(reit => reit.reitId);
await Promise.all([
queryClient.prefetchQuery(marketListQueries.dividendList(allReitIdList)),
queryClient.prefetchQuery(marketListQueries.profitList(allReitIdList)),
queryClient.prefetchQuery(marketListQueries.currentPriceList(allReitIdList)),
queryClient.prefetchQuery(marketListQueries.navList(allReitIdList)),
queryClient.prefetchQuery(marketListQueries.ffoList(allReitIdList)),
queryClient.prefetchQuery(marketListQueries.financeList(allReitIdList)),
]);
return {
props: {
dehydratedState: dehydrate(queryClient),
referer: ctx.req.headers.referer ?? null,
},
};
};
아직 서버컴포넌트를 활용하여 리팩토링을 진행하진 않았지만, 아래와 같이 작성해볼 수 있지 않을까 한다 (잘못되었다면, 나중에 리팩토링하고 수정해놓겠습니다.)
import { marketListQueries } from '@/lib/api';
async function fetchMarketData(reitIds) {
const [
dividendList,
profitList,
currentPriceList,
navList,
ffoList,
financeList
] = await Promise.all([
marketListQueries.dividendList(reitIds),
marketListQueries.profitList(reitIds),
marketListQueries.currentPriceList(reitIds),
marketListQueries.navList(reitIds),
marketListQueries.ffoList(reitIds),
marketListQueries.financeList(reitIds)
]);
return {
dividendList,
profitList,
currentPriceList,
navList,
ffoList,
financeList
};
}
유지보수성 향상
- 최신 Next.js 버전을 사용함으로써 장기적인 유지보수 부담을 줄일 수 있다.
- 커뮤니티에서 최신 트렌드와 공식 문서의 지원을 쉽게 받을 수 있다.
현재까지 마이그레이션 과정에서 발생한 문제
Next.js 및 관련 라이브러리를 최신 버전으로 업데이트한 후, 여러 문제가 발생했습니다.
🔴 Recoil 미지원
Next.js 15에서는 React 18의 새로운 렌더링 방식과 맞지 않는 부분이 있어 Recoil이 제대로 동작하지 않는 문제가 발생했습니다.
🔴 상태 관리 라이브러리 변경 필요
Recoil을 더 이상 사용할 수 없기 때문에 대체 라이브러리를 찾아야 했습니다. 候보았던 옵션은 Jotai와 Zustand였습니다.
- Jotai: React 기반으로 설계된 상태 관리 라이브러리로, 서버 컴포넌트와 함께 사용하기 용이함.
- Zustand: Flux 패턴 기반의 상태 관리 라이브러리로, 사용이 간편하고 직관적임.
결과적으로 React의 최신 동작 방식과 가장 잘 맞는 Jotai를 선택했습니다.
문제 해결 및 적용 과정
Recoil → Jotai 마이그레이션
- 기존 Recoil의 atom을 Jotai의 atom으로 변환.
- Recoil의 selector는 Jotai의 computed atom으로 대체.
- Jotai의 Provider를 최상위 컴포넌트에 추가하여 상태 관리.
- 각 컴포넌트에서 Recoil의 useRecoilState를 useAtom으로 변경
기존 Recoil Atom 코드
import { atom, selector } from 'recoil';
/* ============================== Atom =============================== */
const themeState = atom<State>({
key: 'themeState',
default: {
mode: 'Default',
system: 'Pending',
},
});
/** 테마 모드 타입 */
export type Mode = 'Default' | 'Light' | 'Dark';
/** 시스템 테마 타입 */
export type System = 'Pending' | 'Light' | 'Dark';
export type State = {
mode: Mode;
system: System;
};
/* ============================= Selector ============================= */
/** 현재 테마 설정 */
export const themeModeState = selector<State | Mode>({
key: 'themeModeState',
get: ({ get }) => get(themeState).mode,
set: ({ set }, newValue) => {
const mode = newValue as Mode;
set(themeState, (prevState): State => ({ ...prevState, mode }));
},
});
/** 현재 시스템 테마 설정 */
export const themeSystemState = selector<State | System>({
key: 'themeSystemState',
get: ({ get }) => get(themeState).system,
set: ({ set }, newValue) => {
const system = newValue as System;
set(themeState, (prevState): State => ({ ...prevState, system }));
},
});
export default themeState;
Jotai를 활용한 Atom 코드
import { atom } from 'jotai';
/** 테마 모드 타입 */
export type Mode = 'Default' | 'Light' | 'Dark';
/** 시스템 테마 타입 */
export type System = 'Pending' | 'Light' | 'Dark';
export type State = {
mode: Mode;
system: System;
};
/* ============================== Atom =============================== */
export const themeState = atom<{
mode: Mode;
system: System;
}>({
mode: 'Default',
system: 'Pending',
});
/* ============================= Derived Atoms ============================= */
/** 현재 테마 설정 */
export const themeModeState = atom(
get => get(themeState).mode,
(get, set, newValue: Mode) => {
set(themeState, { ...get(themeState), mode: newValue });
}
);
/** 현재 시스템 테마 설정 */
export const themeSystemState = atom(
get => get(themeState).system,
(get, set, newValue: System) => {
set(themeState, { ...get(themeState), system: newValue });
}
);
Next.js 13에서 15로의 마이그레이션 과정에서 예상치 못한 문제가 발생했지만, 이를 해결하면서 프로젝트의 전반적인 구조를 개선할 수 있었습니다.
특히 Recoil → Jotai 마이그레이션을 통해 최신 React 환경과의 호환성을 높였다는 점이 가장 큰 성과였습니다.
앞으로도 최신 기술을 적극적으로 적용하여 프로젝트를 지속적으로 발전시켜 나갈 계획입니다!
'FrontEnd Dev' 카테고리의 다른 글
React에서 Buzzvil CPE/CPA 연동하기 – 실제 사례 기반 구현 가이드 (2) | 2025.04.14 |
---|---|
React Query 버전 업그레이드 및 커스텀 타입 제거 (0) | 2025.04.04 |
Next.js 보안 이슈 대응: x-middleware-subrequest 헤더 우회 차단하기 (0) | 2025.03.31 |
헤드리스 컴포넌트와 합성 컴포넌트: 학습한 내용 정리 (0) | 2025.03.18 |
나만의 Next.js 프로젝트 컨벤션 (0) | 2025.03.09 |