Next.js 16 Beta & React 19.2 알아보기

_sw_·2025년 10월 26일
1
post-thumbnail

🎯 Next.js 16 Beta 주요 업데이트

그 전에!
업데이트 내용 옆에 alpha, beta 이런 식으로 키워드가 달려있는데 이것부터 정확하게 하고...

  • Alpha (알파): 초기 테스트 단계로, 소프트웨어가 불완전하고 불안정하며 버그를 포함할 수 있지만, 핵심 기능은 포함된 단계
  • Beta (베타): 다음 단계로, 알파보다는 안정적이지만, 여전히 버그가 있을 수 있으며 사용자 피드백에 따라 변경될 수 있는 단계
  • Stable (안정 버전): 최종적으로 공식 릴리스된 버전으로, 완전히 테스트되었으며 신뢰할 수 있고 일반 사용자가 사용할 준비가 된 단계

1. Turbopack (Stable) - 이제 기본 번들러

  • 성능 개선
    • 프로덕션 빌드: 2-5배 빠름
    • Fast Refresh: 최대 10배 빠름
  • Next.js 15.3+ 기준으로 개발 세션의 50%, 프로덕션 빌드의 20%가 이미 Turbopack 사용 중
  • 모든 새 프로젝트에 자동으로 적용
  • next dev --webpack 플래그로 계속 사용 가능

파일시스템 캐싱 (Beta)

→ turbopack에서의 Beta 기능 / webpack에는 이미 있음

→ 컴파일 결과물을 디스크에 저장

const nextConfig = {
  experimental: {
    turbopackFileSystemCacheForDev: true,
  },
};
  • 컴파일 아티팩트를 디스크에 저장하여 재시작 시 훨씬 빠른 컴파일 가능

2. React Compiler 지원 (Stable)

  • 컴포넌트를 자동으로 메모이제이션하여 불필요한 리렌더링 감소
  • 수동 최적화 코드 작성 불필요
  • Babel 의존성 때문에 빌드 시간이 증가할 수 있음
const nextConfig = {
  reactCompiler: true, // 이제 stable!
};
  • 예시 React Compiler는 빌드시 메모이제이션을 추가해준다!
    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를 사용하기 쉽도록 설정값을 추가해준 업데이트인 것이었다. 😥


3. 향상된 라우팅 & 내비게이션

Layout Deduplication (레이아웃 중복 제거)

  • 문제: 50개의 제품 링크가 있는 페이지에서 공유 레이아웃이 50번 다운로드됨
  • 해결: 공유 레이아웃(layout.tsx)은 한 번만 다운로드하고, 각 링크별로 차이나는 부분만 다운로드
  • 결과: 네트워크 전송 크기 대폭 감소

Incremental Prefetching (증분 프리페칭)

  • prefetching을 청크 단위로 구분 → 이미 캐시된 부분은 건너뛰고 필요한 부분만 프리페칭
  • 링크가 뷰포트를 벗어나면 요청 취소
  • 호버 시 우선순위 높여서 프리페칭
  • 데이터 무효화 시 자동으로 재프리페칭

트레이드오프: 개별 요청 수는 증가(청크)하지만, 전체 전송 크기는 크게 감소(캐싱)


4. 개선된 캐싱 API

revalidateTag() 변경 ⚠️

→ 기본동작 : 캐싱된 데이터를 반환하고, 새 데이터를 fetch 한 후에 캐시 값 갱신

// ✅ 새로운 방식 (필수)
revalidateTag('blog-posts', 'max');  // 대부분의 경우 'max' 권장
revalidateTag('news-feed', 'hours');
revalidateTag('analytics', 'days');
revalidateTag('products', { revalidate: 3600 }); // 인라인 설정

// ❌ 기존 방식 (deprecated)
revalidateTag('blog-posts');
  • 두 번째 인자 필수: cacheLife 프로필 지정
  • SWR 동작: 사용자는 캐시된 데이터를 즉시 받고, 백그라운드에서 재검증

updateTag() (신규) 🆕

→ 기본 동작 : 기존 캐시를 만료 시키고, 새 값을 fetch해서 반환 후 캐시 갱신

'use server';
import { updateTag } from 'next/cache';

export async function updateUserProfile(userId, profile) {
  await db.users.update(userId, profile);
  updateTag(`user-${userId}`); // 즉시 캐시 만료 및 갱신
}
  • Server Actions 전용
  • Read-your-writes: 사용자가 변경한 내용을 즉시 확인 가능
  • 용도: 폼, 사용자 설정 등 즉각적인 피드백이 필요한 경우

refresh() (신규) 🆕

'use server';
import { refresh } from 'next/cache';

export async function markNotificationAsRead(notificationId) {
  await db.notifications.markAsRead(notificationId);
  refresh(); // 캐시되지 않은 데이터만 갱신
}
  • Server Actions 전용
  • 캐시 건드리지 않음: 알림 카운트, 실시간 메트릭 등 캐시되지 않은 데이터만 갱신

6. 주요 Breaking Changes

필수 요구사항

  • Node.js: 20.9+ (18 지원 종료)
  • TypeScript: 5.1.0+
  • 브라우저: Chrome/Edge/Firefox 111+, Safari 16.4+

비동기로 변경 필수 ⚠️

→ 내부 동작 자체가 비동기로 마이그레이션됨
→ 성능 최적화

// ❌ 기존 방식
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();

next/image 변경사항

  • images.minimumCacheTTL: 60초 → 4시간 (14400초)
  • images.imageSizes: 16 제거 (사용률 4.2%에 불과)
  • images.qualities: [1..100] → [75]로 단순화
  • images.maximumRedirects: 무제한 → 3으로 제한
  • 보안: images.dangerouslyAllowLocalIP 기본값 false

Middleware 파일명

  • middleware.tsproxy.ts로 변경 권장 (deprecated)

⚛️ React 19.2 주요 업데이트

1. <Activity /> 컴포넌트 🆕

애플리케이션을 "활동" 단위로 나누어 제어 가능

// 기존 방식
{isVisible && <Page />}

// 새로운 방식
<Activity mode={isVisible ? 'visible' : 'hidden'}>
  <Page />
</Activity>

모드

  • visible: 자식 표시, 이펙트 마운트, 업데이트 정상 처리
  • hidden: 자식 숨김 (display: none), 이펙트 언마운트, 업데이트를 가장 낮은 우선순위로 지연

사용 사례

  • 사용자가 다음에 탐색할 가능성이 높은 부분을 미리 렌더링 (백그라운드에서 데이터, CSS, 이미지 로딩)
  • 뒤로 가기 시 입력 필드 등의 상태 유지

💡 기존 방식이랑 코드 작성법만 바뀐거 아니에요?! 🤬(억지)

Activity를 사용하면 아래와 같이 동작하게된다.
컴포넌트는 마운트와 백그라운드 랜더링, 데이터 또는 이미지 Fetching, 상태 유지가 가능하다. 하지만 useEffect는 동작하지 않는다.
컴포넌트는 마운트되어 있고 State도 유지되지만, 모든 Effect(부수효과)가 클린업되어 실행되지 않는 대기 상태로 유지된다.
기존 방식에 비해서 해당 컴포넌트가 화면에 보일 수 있게 사전 작업을 미리 해둘 수 있어 UX적으로도 뛰어난 업데이트라고 볼 수 있다.


2. 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만 의존성 배열에 남김
}

특징

  • Effect Event는 항상 최신 props/state를 "봄"
  • 의존성 배열에 포함하지 않음
  • eslint-plugin-react-hooks@6.1.0 업그레이드 필요

3. 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() });
    });

    케이스 1: 렌더링 정상 완료

    렌더링 시작
      ↓
    fetchProduct 완료 (1초)
      ↓
    fetchReviews 완료 (1초)
      ↓
    fetchRelated 완료 (1초)
      ↓
    렌더링 완료
      ↓
    cacheSignal 발동
      → 모든 fetch 이미 완료됨
      → 아무 일도 안 일어남 ✅

    케이스 2: 렌더링 중간에 중단 (페이지 이동)

    렌더링 시작
      ↓
    fetchProduct 시작 (3초 소요 예상)
    fetchReviews 시작 (3초 소요 예상)
    fetchRelated 시작 (3초 소요 예상)
      ↓
    1초 후 사용자가 다른 페이지로 이동!
      ↓
    렌더링 중단!
      ↓
    cacheSignal 발동
      → fetchProduct의 fetch 취소 ✅
      → fetchReviews의 fetch 취소 ✅
      → fetchRelated의 fetch 취소 ✅
    이 경우 3개 모두 취소되는 이유:
    • 3개 모두 같은 렌더링 사이클에 속함

    • 렌더링 중단 = 모든 cache() 정리

    • 각자의 cacheSignal이 각자의 fetch 취소

      케이스 3: 일부만 완료된 경우

      렌더링 시작
        ↓
      fetchProduct 완료 (0.5초) ✅
        ↓
      fetchReviews 시작 (3초 소요 예상) 🔄
      fetchRelated 시작 (3초 소요 예상) 🔄
        ↓
      1초 후 사용자가 페이지 이동
        ↓
      렌더링 중단!
        ↓
      cacheSignal 발동
        → fetchProduct: 이미 완료, 취소할 게 없음
        → fetchReviews: 진행 중이던 fetch 취소 ✅
        → fetchRelated: 진행 중이던 fetch 취소 ✅

      "각 fetch의 cacheSignal은 자기 자신의 요청만 취소한다. 단, 같은 렌더링 사이클에 속한 모든 cache()가 정리될 때 각자의 fetch가 각자 취소된다."


4. Performance Tracks 🆕

Chrome DevTools에 React 전용 성능 트랙 추가

Scheduler ⚛ Track

  • React가 다양한 우선순위로 작업하는 내용 표시
  • "blocking" (사용자 인터랙션) vs "transition" (startTransition 내부)
  • 업데이트를 스케줄한 이벤트 타입
  • 어떤 우선순위가 다른 우선순위를 기다리는지

Components ⚛ Track

  • React가 작업 중인 컴포넌트 트리 표시
  • "Mount", "Blocked" 등의 라벨
  • 컴포넌트가 렌더링되거나 이펙트를 실행하는 시점과 소요 시간

5. Partial Pre-rendering (PPR)

  • 앱의 일부를 정적 컨텐츠로 빌드 시 미리 생성(SSG) CDN에서 제공.
  • 동적 영역에 대해서는 SSR로 스트리밍하여 나머지 컨텐츠 제공

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);

6. 그 외 주요 변경사항

Suspense Boundary Batching (SSR)

  • 이전: 스트리밍 SSR 중 Suspense 콘텐츠가 준비되는 즉시 fallback 교체
  • 19.2: 짧은 시간 동안 배치하여 더 많은 콘텐츠를 함께 표시
  • 장점: <ViewTransition> 지원 준비, 애니메이션을 더 큰 배치로 실행

Node.js에서 Web Streams 지원

  • renderToReadableStream Node.js에서 사용 가능
  • prerender Node.js에서 사용 가능
  • resume, resumeAndPrerender API 추가

eslint-plugin-react-hooks v6

  • Flat config가 기본값 (ESLint v10 준비)
  • React Compiler 기반 규칙 opt-in 가능
  • 레거시 설정: recommended-legacy 사용

useId 기본 prefix 변경

  • _r_ 로 변경 (이전: :r: 또는 «r»)
  • 이유: View Transitions 지원을 위해 view-transition-name과 XML 1.0 이름에 유효한 ID 필요

📚 참고 자료

profile
나도 잘하고 싶다..!

0개의 댓글