포스트 목록으로
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
-
점진적 마이그레이션 가능
my-app/ ├── app/ # 새로운 페이지 └── pages/ # 기존 페이지 -
라우트 변환
pages/index.tsx → app/page.tsx pages/about.tsx → app/about/page.tsx pages/blog/[slug].tsx → app/blog/[slug]/page.tsx -
데이터 페칭 변환
// 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