그 전에!
업데이트 내용 옆에 alpha, beta 이런 식으로 키워드가 달려있는데 이것부터 정확하게 하고...
next dev --webpack 플래그로 계속 사용 가능→ turbopack에서의 Beta 기능 / webpack에는 이미 있음
→ 컴파일 결과물을 디스크에 저장
const nextConfig = {
experimental: {
turbopackFileSystemCacheForDev: true,
},
};
const nextConfig = {
reactCompiler: true, // 이제 stable!
};
function ProductList({ products, onSelect }) {
return (
<div>
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onClick={() => onSelect(product)}
/>
))}
</div>
);
}function ProductList({ products, onSelect }) {
// React Compiler가 자동으로 추가한 메모이제이션
const memoizedProducts = useMemo(() => products, [products]);
const memoizedOnSelect = useCallback(onSelect, [onSelect]);
const memoizedMap = useMemo(() =>
memoizedProducts.map(product => ({
key: product.id,
product,
onClick: () => memoizedOnSelect(product)
}))
, [memoizedProducts, memoizedOnSelect]);
return (
<div>
{memoizedMap.map(item => (
<ProductCard {...item} />
))}
</div>
);
}💡 React Compiler 업데이트가 왜 Next.js에?
React Compiler가 최신 React 19 버전으로 올라가면서 나오게 되어서, React에서 기본적으로 제공하는 기능을 Next.js에서 제한적으로 제공했었나? 라는 생각을 했었는데,
그건 오해였고,
React Compiler는 기본적으로 플러그인처럼 제공되어서, React에서도 기본적으로 설정이 필요했다. 이번 Next.js에서는 React Compiler를 사용하기 쉽도록 설정값을 추가해준 업데이트인 것이었다. 😥
트레이드오프: 개별 요청 수는 증가(청크)하지만, 전체 전송 크기는 크게 감소(캐싱)
revalidateTag() 변경 ⚠️→ 기본동작 : 캐싱된 데이터를 반환하고, 새 데이터를 fetch 한 후에 캐시 값 갱신
// ✅ 새로운 방식 (필수)
revalidateTag('blog-posts', 'max'); // 대부분의 경우 'max' 권장
revalidateTag('news-feed', 'hours');
revalidateTag('analytics', 'days');
revalidateTag('products', { revalidate: 3600 }); // 인라인 설정
// ❌ 기존 방식 (deprecated)
revalidateTag('blog-posts');
cacheLife 프로필 지정updateTag() (신규) 🆕→ 기본 동작 : 기존 캐시를 만료 시키고, 새 값을 fetch해서 반환 후 캐시 갱신
'use server';
import { updateTag } from 'next/cache';
export async function updateUserProfile(userId, profile) {
await db.users.update(userId, profile);
updateTag(`user-${userId}`); // 즉시 캐시 만료 및 갱신
}
refresh() (신규) 🆕'use server';
import { refresh } from 'next/cache';
export async function markNotificationAsRead(notificationId) {
await db.notifications.markAsRead(notificationId);
refresh(); // 캐시되지 않은 데이터만 갱신
}
→ 내부 동작 자체가 비동기로 마이그레이션됨
→ 성능 최적화
// ❌ 기존 방식
const params = props.params;
const searchParams = props.searchParams;
const cookieStore = cookies();
// ✅ 새로운 방식
const params = await props.params;
const searchParams = await props.searchParams;
const cookieStore = await cookies();
images.minimumCacheTTL: 60초 → 4시간 (14400초)images.imageSizes: 16 제거 (사용률 4.2%에 불과)images.qualities: [1..100] → [75]로 단순화images.maximumRedirects: 무제한 → 3으로 제한images.dangerouslyAllowLocalIP 기본값 falsemiddleware.ts → proxy.ts로 변경 권장 (deprecated)<Activity /> 컴포넌트 🆕애플리케이션을 "활동" 단위로 나누어 제어 가능
// 기존 방식
{isVisible && <Page />}
// 새로운 방식
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<Page />
</Activity>
visible: 자식 표시, 이펙트 마운트, 업데이트 정상 처리hidden: 자식 숨김 (display: none), 이펙트 언마운트, 업데이트를 가장 낮은 우선순위로 지연💡 기존 방식이랑 코드 작성법만 바뀐거 아니에요?! 🤬(억지)
Activity를 사용하면 아래와 같이 동작하게된다.
컴포넌트는 마운트와 백그라운드 랜더링, 데이터 또는 이미지 Fetching, 상태 유지가 가능하다. 하지만 useEffect는 동작하지 않는다.
컴포넌트는 마운트되어 있고 State도 유지되지만, 모든 Effect(부수효과)가 클린업되어 실행되지 않는 대기 상태로 유지된다.
기존 방식에 비해서 해당 컴포넌트가 화면에 보일 수 있게 사전 작업을 미리 해둘 수 있어 UX적으로도 뛰어난 업데이트라고 볼 수 있다.
useEffectEvent() 🆕Effect에서 상태 갱신으로 인한 side effect와 로직을 분리하는 패턴
→ Effect 내부에서 사용되는 값이 의존성 배열에 포함되면서 Effect가 지속적으로 재실행되는 현상을 개선
→ 최신 값을 사용해야 하지만, 그 값의 변경으로 인한 Effect 재실행(부수 효과)을 방지하고 싶을 때 사용
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme); // theme 사용
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // theme 변경 시 재연결 발생 🤦
}
// -> 채팅 룸 상태가 theme에 의존함
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // 항상 최신 theme 참조
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ roomId만 의존성 배열에 남김
}
eslint-plugin-react-hooks@6.1.0 업그레이드 필요cacheSignal() 🆕💡 React cache()
- 렌더링 사이클 동안 동일한 데이터 요청의 중복을 제거하는 함수
- 여러 중첩된 Server Component가 같은 데이터를 요청할 때, 실제 fetch는 1번만 수행
- 일반 캐싱과의 차이
- 일반 캐싱: 시간 기반 저장 (예: 1시간), 재방문 시 재사용
- React cache(): 렌더링 기반 저장, 렌더링 끝나면 즉시 삭제
cache() 라이프타임이 끝날 때를 알 수 있는 신호
import { cache, cacheSignal } from 'react';
const dedupedFetch = cache(fetch);
async function Component() {
await dedupedFetch(url, { signal: cacheSignal() });
}
async function ProductPage({ id }) {
const product = await fetchProduct(id); // 요청 1
const reviews = await fetchReviews(id); // 요청 2
const related = await fetchRelated(id); // 요청 3
return <div>...</div>;
}
const fetchProduct = cache(async (id) => {
return fetch(`/api/products/${id}`, { signal: cacheSignal() });
});
const fetchReviews = cache(async (id) => {
return fetch(`/api/reviews/${id}`, { signal: cacheSignal() });
});
const fetchRelated = cache(async (id) => {
return fetch(`/api/related/${id}`, { signal: cacheSignal() });
});렌더링 시작
↓
fetchProduct 완료 (1초)
↓
fetchReviews 완료 (1초)
↓
fetchRelated 완료 (1초)
↓
렌더링 완료
↓
cacheSignal 발동
→ 모든 fetch 이미 완료됨
→ 아무 일도 안 일어남 ✅렌더링 시작
↓
fetchProduct 시작 (3초 소요 예상)
fetchReviews 시작 (3초 소요 예상)
fetchRelated 시작 (3초 소요 예상)
↓
1초 후 사용자가 다른 페이지로 이동!
↓
렌더링 중단!
↓
cacheSignal 발동
→ fetchProduct의 fetch 취소 ✅
→ fetchReviews의 fetch 취소 ✅
→ fetchRelated의 fetch 취소 ✅ 이 경우 3개 모두 취소되는 이유:3개 모두 같은 렌더링 사이클에 속함
렌더링 중단 = 모든 cache() 정리
각자의 cacheSignal이 각자의 fetch 취소
렌더링 시작
↓
fetchProduct 완료 (0.5초) ✅
↓
fetchReviews 시작 (3초 소요 예상) 🔄
fetchRelated 시작 (3초 소요 예상) 🔄
↓
1초 후 사용자가 페이지 이동
↓
렌더링 중단!
↓
cacheSignal 발동
→ fetchProduct: 이미 완료, 취소할 게 없음
→ fetchReviews: 진행 중이던 fetch 취소 ✅
→ fetchRelated: 진행 중이던 fetch 취소 ✅
"각 fetch의 cacheSignal은 자기 자신의 요청만 취소한다. 단, 같은 렌더링 사이클에 속한 모든 cache()가 정리될 때 각자의 fetch가 각자 취소된다."
Chrome DevTools에 React 전용 성능 트랙 추가
→ SSG(Static Site Generation)는 빠르지만 동적 컨텐츠를 제공하지 못한다는 특징과, SSR(Server Side Rendering)은 동적 컨텐츠를 제공할 수 는 있지만 서버를 거쳐 컨텐츠를 생성해야한다는 점을 상호 보완한 하이브리드 랜더링 방식
→ ISR(Incremental Static Regeneration)은 정적 데이터를 주기적으로 생성해서 새로운 데이터를 보여주는 방식이라 PPR과의 랜더링 방식에는 차이가 있음
// 1. 사전 렌더링
const { prelude, postponed } = await prerender(<App />, {
signal: controller.signal,
});
await savePostponedState(postponed);
// prelude를 클라이언트나 CDN으로 전송
// 2. 나중에 재개 (SSR 스트림)
const postponed = await getPostponedState(request);
const resumeStream = await resume(<App />, postponed);
// 또는 SSG를 위한 정적 HTML
const { prelude } = await resumeAndPrerender(<App />, postponedState);
<ViewTransition> 지원 준비, 애니메이션을 더 큰 배치로 실행renderToReadableStream Node.js에서 사용 가능prerender Node.js에서 사용 가능resume, resumeAndPrerender API 추가recommended-legacy 사용_r_ 로 변경 (이전: :r: 또는 «r»)view-transition-name과 XML 1.0 이름에 유효한 ID 필요