최근 프로젝트에서 주요 기술 스택을 업그레이드 햇습니다
-Next.js 14 => 15
-React 18 => 19
-Recoil => Zustand
-eslint 8 => 9
업그레이드 과정에서 겪은 주요 변화, 트러블슈팅, 마이그레이션 이유, 느낀점 정리해보려 합니다
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 19부터 도입된 새로운기능
const data = React.use(fetchData())
// React가 Promise가 resolve될때까지 컴포넌트를 Suspense로 멈춥니다
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()에서 예외 처리
기존에는 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()
캐시위치 : 서버/ CDN / 브라우저
설명 : SSR/SSG 단계에서 재사용 여부 결정
Tanstack Query
캐시위치 : 클라이언트 JS 메모리
설명 : hydration 이후 상태관리
Tanstack Qurey 같은 라이브러리랑 중복? 되지않는가?
이는 TanStack Query나 SWR 같은 클라이언트 측 캐싱과는 별도로,
Next.js의 렌더링/재빌드 수준에서 작동하는 캐시이기 때문에,
중복되거나 충돌되지 않는다.
기존에는 React Strict Mode에서 useEffect가 mount → unmount → remount를 2번 반복했지만,
이제는 1회만 실행되도록 변경되어 불필요한 side-effect 제거가 가능해졌습니다.
기존 방식
컴포넌트에서 '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} />
));
기존 recoil 한계
zustand 로 전환한 이유