포스트 목록으로

Next.js 16과 React 19로 시작하기

최신 Next.js와 React로 현대적인 웹 애플리케이션을 구축하는 방법을 알아봅니다.

2024년 11월 6일6분
Next.jsReactWeb Development

Next.js 16과 React 19로 시작하기

Next.js 16과 React 19가 출시되면서 웹 개발이 한층 더 강력해졌습니다. 이 글에서는 주요 기능과 변경사항을 살펴봅니다.

Next.js 16의 주요 변경사항

1. React 19 지원

Next.js 16은 React 19를 기본으로 지원합니다:

{
  "dependencies": {
    "next": "16.0.0",
    "react": "19.0.0",
    "react-dom": "19.0.0"
  }
}

2. App Router 안정화

App Router가 완전히 안정화되었습니다:

app/
├── layout.tsx       # 루트 레이아웃
├── page.tsx         # 홈페이지
├── about/
│   └── page.tsx     # /about
└── blog/
    ├── page.tsx     # /blog
    └── [slug]/
        └── page.tsx # /blog/[slug]

3. 향상된 Server Components

Before (Pages Router)

// pages/index.tsx
import { GetServerSideProps } from 'next';

export default function Home({ data }) {
  return <div>{data.title}</div>;
}

export const getServerSideProps: GetServerSideProps = async () => {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();
  return { props: { data } };
};

After (App Router)

// app/page.tsx
async function getData() {
  const res = await fetch('https://api.example.com/data');
  return res.json();
}

export default async function Home() {
  const data = await getData();
  return <div>{data.title}</div>;
}

훨씬 간결하고 직관적! 🎉

4. Streaming SSR

// app/page.tsx
import { Suspense } from 'react';
import { Posts } from './Posts';
import { PostsSkeleton } from './PostsSkeleton';

export default function Home() {
  return (
    <div>
      <h1>Blog</h1>
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
    </div>
  );
}

페이지를 부분적으로 렌더링하여 초기 로딩 속도 향상!

5. Server Actions

폼 처리가 이렇게 간단해졌습니다:

// app/actions.ts
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title');
  const content = formData.get('content');

  // 데이터베이스에 저장
  await db.posts.create({
    data: { title, content }
  });

  // 페이지 리로드
  revalidatePath('/posts');
}
// app/posts/new/page.tsx
import { createPost } from '@/app/actions';

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" />
      <textarea name="content" />
      <button type="submit">작성</button>
    </form>
  );
}

API 라우트 없이 직접 서버 함수 호출!

React 19의 새로운 기능

1. React Compiler (자동 최적화)

이제 useMemo, useCallback, memo를 수동으로 추가할 필요가 없습니다:

Before (React 18)

function TodoList({ todos, filter }) {
  const filteredTodos = useMemo(() => {
    return todos.filter(todo => todo.category === filter);
  }, [todos, filter]);

  const handleClick = useCallback((id) => {
    // ...
  }, []);

  return (
    <ul>
      {filteredTodos.map(todo => (
        <li key={todo.id} onClick={() => handleClick(todo.id)}>
          {todo.title}
        </li>
      ))}
    </ul>
  );
}

After (React 19)

function TodoList({ todos, filter }) {
  // React Compiler가 자동으로 최적화!
  const filteredTodos = todos.filter(todo => todo.category === filter);

  const handleClick = (id) => {
    // ...
  };

  return (
    <ul>
      {filteredTodos.map(todo => (
        <li key={todo.id} onClick={() => handleClick(todo.id)}>
          {todo.title}
        </li>
      ))}
    </ul>
  );
}

2. Actions

비동기 작업을 더 쉽게:

function UpdateName() {
  const [isPending, startTransition] = useTransition();

  async function handleSubmit(formData) {
    startTransition(async () => {
      const name = formData.get('name');
      await updateName(name);
    });
  }

  return (
    <form action={handleSubmit}>
      <input name="name" />
      <button disabled={isPending}>
        {isPending ? '저장 중...' : '저장'}
      </button>
    </form>
  );
}

3. use Hook

Promise와 Context를 직접 사용:

function Post({ postPromise }) {
  // Promise를 직접 사용!
  const post = use(postPromise);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

function App() {
  const postPromise = fetch('/api/post/1').then(r => r.json());

  return (
    <Suspense fallback={<Spinner />}>
      <Post postPromise={postPromise} />
    </Suspense>
  );
}

4. Document Metadata

메타데이터를 컴포넌트 내에서 직접 관리:

function BlogPost({ post }) {
  return (
    <>
      <title>{post.title} | My Blog</title>
      <meta name="description" content={post.excerpt} />
      <meta property="og:image" content={post.image} />

      <article>
        <h1>{post.title}</h1>
        <p>{post.content}</p>
      </article>
    </>
  );
}

실전 프로젝트 구조

권장 폴더 구조

my-app/
├── app/
│   ├── (auth)/           # 라우트 그룹
│   │   ├── login/
│   │   └── register/
│   ├── (dashboard)/
│   │   ├── layout.tsx    # 대시보드 레이아웃
│   │   ├── page.tsx
│   │   └── settings/
│   ├── api/              # API 라우트
│   │   └── posts/
│   ├── actions.ts        # Server Actions
│   ├── layout.tsx        # 루트 레이아웃
│   └── page.tsx          # 홈
├── components/
│   ├── ui/               # 재사용 UI 컴포넌트
│   └── features/         # 기능별 컴포넌트
├── lib/
│   ├── db.ts            # 데이터베이스 클라이언트
│   └── utils.ts         # 유틸리티 함수
└── public/
    └── images/

컴포넌트 패턴

Server Component (기본)

// app/posts/page.tsx
async function getPosts() {
  const posts = await db.posts.findMany();
  return posts;
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

Client Component (인터랙션 필요)

// components/LikeButton.tsx
'use client';

import { useState } from 'react';

export function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);

  async function handleLike() {
    setLikes(likes + 1);
    await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
  }

  return (
    <button onClick={handleLike}>
      ❤️ {likes}
    </button>
  );
}

혼합 사용

// app/posts/[id]/page.tsx
import { LikeButton } from '@/components/LikeButton';

async function getPost(id: string) {
  const post = await db.posts.findUnique({ where: { id } });
  return post;
}

export default async function PostPage({ params }) {
  const post = await getPost(params.id);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>

      {/* Client Component는 Server Component 안에 */}
      <LikeButton postId={post.id} initialLikes={post.likes} />
    </article>
  );
}

성능 최적화

1. Static Export

정적 사이트로 배포:

// next.config.ts
const config = {
  output: 'export',
};

export default config;

2. Incremental Static Regeneration (ISR)

// app/posts/[slug]/page.tsx
export const revalidate = 3600; // 1시간마다 재생성

export default async function Post({ params }) {
  const post = await getPost(params.slug);
  return <article>{post.content}</article>;
}

3. Image Optimization

import Image from 'next/image';

export function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      priority  // LCP 최적화
    />
  );
}

4. Font Optimization

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export default function RootLayout({ children }) {
  return (
    <html lang="ko" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

마이그레이션 가이드

Pages Router → App Router

  1. 점진적 마이그레이션 가능

    my-app/
    ├── app/      # 새로운 페이지
    └── pages/    # 기존 페이지
    
  2. 라우트 변환

    pages/index.tsx        → app/page.tsx
    pages/about.tsx        → app/about/page.tsx
    pages/blog/[slug].tsx  → app/blog/[slug]/page.tsx
    
  3. 데이터 페칭 변환

    // Before
    export async function getStaticProps() {
      const data = await fetchData();
      return { props: { data } };
    }
    
    // After
    async function getData() {
      return await fetchData();
    }
    
    export default async function Page() {
      const data = await getData();
      return <div>{data}</div>;
    }
    

배포

Vercel

npm install -g vercel
vercel

Cloudflare Pages

npm run build
# out/ 폴더 배포

Docker

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["npm", "start"]

결론

Next.js 16 + React 19 조합은:

  • 더 빠른 성능
  • 🎯 더 간단한 코드
  • 🔧 더 적은 보일러플레이트
  • 🚀 더 나은 개발자 경험

지금 바로 시작해보세요!

npx create-next-app@latest my-app
cd my-app
npm run dev

참고 자료