한땀한땀

Next.js 13에서 15로 마이그레이션 본문

FrontEnd Dev

Next.js 13에서 15로 마이그레이션

junfromkorea 2025. 4. 1. 22:26

현재 회사의 프로젝트는 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을 더 이상 사용할 수 없기 때문에 대체 라이브러리를 찾아야 했습니다. 候보았던 옵션은 JotaiZustand였습니다.

  • Jotai: React 기반으로 설계된 상태 관리 라이브러리로, 서버 컴포넌트와 함께 사용하기 용이함.
  • Zustand: Flux 패턴 기반의 상태 관리 라이브러리로, 사용이 간편하고 직관적임.

결과적으로 React의 최신 동작 방식과 가장 잘 맞는 Jotai를 선택했습니다.

 

문제 해결 및 적용 과정

Recoil → Jotai 마이그레이션

  • 기존 Recoil의 atom을 Jotai의 atom으로 변환.
  • Recoil의 selector는 Jotai의 computed atom으로 대체.
  • Jotai의 Provider를 최상위 컴포넌트에 추가하여 상태 관리.
  • 각 컴포넌트에서 Recoil의 useRecoilStateuseAtom으로 변경

기존 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 환경과의 호환성을 높였다는 점이 가장 큰 성과였습니다.

앞으로도 최신 기술을 적극적으로 적용하여 프로젝트를 지속적으로 발전시켜 나갈 계획입니다!