체크히어: React 19, Next.js 15, zusatnd, eslint 9 마이그레이션 후기

nemo0824·2025년 8월 1일
0
post-thumbnail

최근 프로젝트에서 주요 기술 스택을 업그레이드 햇습니다
-Next.js 14 => 15
-React 18 => 19
-Recoil => Zustand
-eslint 8 => 9

업그레이드 과정에서 겪은 주요 변화, 트러블슈팅, 마이그레이션 이유, 느낀점 정리해보려 합니다

업그레이드 배경

  1. recoil의 React 19 / next.js 15 버전 지원 중단
  2. react 19 버전의 안정화

주요 변화 및 트러블슈팅

1. params, searchParams, cookies → 비동기(Promise)로 변경

Next.js 15부터 특수props가 동기 값에서 Promise 객체로 변경되었습니다.

변경 배경
기존에 "params, searchPrams cookies"등은 동기적으로 보이지만
사실 내부적으로 next.js가 request.url 에서 값을 동기적이게 보여줬엇습니다

export default function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = React.use(params);
  return <div>{id}</div>;
}

변경이후
Promise를 React.use(), await으로 처리해야합니다

번외 React.use

React 19부터 도입된 새로운기능

const data = React.use(fetchData()) 
// React가 Promise가 resolve될때까지 컴포넌트를 Suspense로 멈춥니다 

2. Webpack 기반 강화 esm 해석 강화

Next.js 15는 .js 파일 내부에 import 또는 export 구문이 존재하면 해당 파일을 ESM으로 간주합니다

발생한 문제

라이센스 관련주석 (/ !@license / ) 포함된 패키지들이 트리셰이킹에서 제외되지 않거나 경고 발생
.js 파일임에도 내부 import 구문이있으면 ESM으로 간주

해결방법 webpack 커스터마이징
.LICENSE, .md, .txt, .log 확장자는 번들에서 제외
해당 파일이 import 되어도 무시하도록 커스텀 로더(webpack-ignore-loader.cjs) 설정

webpack(config) {
  config.module.rules.push({
    test: /\.(LICENSE|md|txt|log)$/i,
    use: [
      {
        loader: path.resolve('./webpack-ignore-loader.cjs'),
      },
    ],
  });

next.config.mjs로 전환 + ESM 형식 사용 (type:moudle 필요없음)
commonjs 패키지는 dynamic() 등으로 래핑해서 사용
Webpack 설정이 필요한경우 webpack() 함수에서 명시적 예외 처리 필요

next.config.js → next.config.mjs로 변경
필요 시 webpack()에서 예외 처리

3. fetch() 캐시 정책 명시 필수화

기존에는 fetch() 사용시 cache 옵션을 주지않아도 force-cache가 암묵적으로 적용
이제는 매 요청마다 새로운 요청이 일어날수 있음으로 명시적으로 작성해야합니다.

await fetch('https://api.example.com/posts', {
  cache: 'force-cache',
});

// SSR 시 강제 새 요청
await fetch('https://api.example.com/user', {
  cache: 'no-store',
});

번외 fetch vs tanstack Query 캐시 차이

fetch()
캐시위치 : 서버/ CDN / 브라우저
설명 : SSR/SSG 단계에서 재사용 여부 결정

Tanstack Query
캐시위치 : 클라이언트 JS 메모리
설명 : hydration 이후 상태관리

Tanstack Qurey 같은 라이브러리랑 중복? 되지않는가?

이는 TanStack Query나 SWR 같은 클라이언트 측 캐싱과는 별도로,
Next.js의 렌더링/재빌드 수준에서 작동하는 캐시이기 때문에,
중복되거나 충돌되지 않는다.

4. React 19의 useEffect 변화

기존에는 React Strict Mode에서 useEffect가 mount → unmount → remount를 2번 반복했지만,
이제는 1회만 실행되도록 변경되어 불필요한 side-effect 제거가 가능해졌습니다.

5. ref의 자동 전달 강화

기존 방식
컴포넌트에서 'ref'를 전달하고싶을때 아래처럼
<Button ref={btnRef}>로 넘겨도 내부에서 해당 ref를 직접 받을 수 없었습니다.

//기존 방식 (ref를 받을 수 없음)
<button ref={btnRef}>Click me</button>

export const Button = ({ ...props }) => {
  return <button {...props} />;
};

기존 방식 해결방법
forwardRef

그래서 React에서 ref를 외부에서 전달받고 내부 Dom에 연결하기위해서
반드시 React.forwardRef를 사용해야했습니다

// forwardRef 사용
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ ...props }, ref) => {
    return <button ref={ref} {...props} />;
  },
);

React 19이후 변화

<Button ref={btnRef}/> 

export const Button = ({...props}: ButtonProps) => {
	return <button {...props} /> 
}

하지만 여전히 shadcn/ui, headless UI등 라이브러리는 여전히 forwardRef를 사용해 명시적 전달을 요구합니다.

export const Button = React.forwardRef((props, ref) => (
  <button ref={ref} {...props} />
));

6. Recoil => zustand 마이그레이션

기존 recoil 한계

  • react 19에서 비동기 selector 작동 불안정
  • 전역 상태 재사용이 어렵고, 상태 구독 단위가 커짐
  • ssr/클라이언트 경계에서 불안정

zustand 로 전환한 이유

  • 도메인 단위 store 구성 가능
  • 필요한 stlae만 선택적 구독 가능 -> 리렌더링 최소화
  • 파일 단위 모듈화가 쉬워 도메인별 store 분리가 자연스럽고 유지보수 유리

7. eslint 8 => 9

  • eslin9
  • Flat Config 도입, eslint.config.mjs, eslint.config.js 사용 .eslintrc는 무시
  • 설정 방식: 반드시 Flat Config 방식
  • 파일 형식: esm 모듈형식
    flat config 란?
    -직접 구성된 JS 배열 기반 설정 시스템
    -계층 x , 평평하게 나열한 구조
    • 플러그인, 룰, 파일 범위 명시적으로 직접 지정
profile
개발 일기

0개의 댓글