CH 1. Next.js 16 ๊ธฐ๋ณธ ๊ตฌ์กฐ ๋ฐ ์ ์ UI ๊ตฌ์ถ
1-1. React์ Next.js์ ์ฐจ์ด์
- React๋ UI๋ฅผ ๋ง๋ค๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ. ๊ธฐ๋ณธ์ ์ผ๋ก ํด๋ผ์ด์ธํธ ์ธก ๋ ๋๋ง(CSR)๋ง ์ง์.
- Next.js๋ React๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๋ฉฐ, ๊ฐ๋ฐ์ ํ์ํ ๋ชจ๋ ๊ธฐ๋ฅ(๋ผ์ฐํ , ๋ฐ์ดํฐ ํ์นญ, ๋น๋ ์์คํ )์ ๊ฐ์ถ ํ์คํ ํ๋ ์์ํฌ.
- ์๋ฒ ์ธก ๋ ๋๋ง(SSR), ์ ์ ์์ฑ(SSG) ๋ฑ ๋ค์ํ ๋ ๋๋ง ๋ฐฉ์์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณต
- SEO(๊ฒ์์์ง ์ต์ ํ)์ ์ ๋ฆฌ
- ๋ ๋น ๋ฅธ ๋ก๋ฉ ์ง์ (ํด๋ผ์ด์ธํธ ๋ก๋ ๋ถ๋ด ๊ฐ์ )
- Server Component๋ฅผ ํตํด ์๋ฒ์ ํด๋ผ์ด์ธํธ์ ๊ฒฝ๊ณ๋ฅผ ๋ช ํํ ๊ตฌ๋ถํ์ฌ ์ฑ๋ฅ์ ์ต์ ํํจ.
1-2 todo ๊ฐ๋ฐ ๋ฒ์
- Read (์ฝ๊ธฐ): ToDo ๋ฆฌ์คํธ๋ฅผ ์๋ฒ์์ ๊ฐ์ ธ์ ๋ณด์ฌ์ค.
- ๊ฐ๋จํ ํ ๊ธ : ToDo ์๋ฃ ํ์
- Create (์์ฑ): ์๋ก์ด ToDo ํญ๋ชฉ์ ์ถ๊ฐํจ.
1-3. ToDo ์ฑ ์์: ๊ธฐ๋ณธ ํ๊ฒฝ ์ค์ ๋ฐ ์ ์ ๋ ์ด์์ (์ค์ต)
ํ๋ก์ ํธ ์์ฑ ๋ฐ ์คํ:
npx create-next-app@latest todo-app
cd todo-app
npm run dev
1-4. Next.js ์ค์น ๋ฐ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ ์ค๋ช

ํ์ผ/๋๋ ํ ๋ฆฌ์ค๋ช
| todo-app/ | ํ๋ก์ ํธ์ ๋ฃจํธ ๋๋ ํ ๋ฆฌ. |
| app/ | App Router์ ํต์ฌ ๋๋ ํ ๋ฆฌ. ๋ผ์ฐํ , ๋ ์ด์์, ํ์ด์ง ์ปดํฌ๋ํธ๊ฐ ์์นํจ. |
| app/layout.js | ๋ฃจํธ ๋ ์ด์์. ์ ์ฒด ์ฑ์ ์ ์ฉ๋๋ฉฐ <html > ๊ตฌ์กฐ๋ฅผ ํฌํจ. |
| app/page.js | ํน์ ๊ฒฝ๋ก์ ๊ณ ์ UI (ํ์ด์ง). / (๋ฉ์ธ ๊ฒฝ๋ก)์ ํด๋นํจ. page.js ํ์ผ์ด ์์ผ๋ฉด route ๊ฒฝ๋ก๋ก ์๋์ธ์๋จ. ์) app/dashboard/page.js -> http://code.com/dashboard |
| public/ | ์ด๋ฏธ์ง, ํฐํธ ๋ฑ ์ ์ ํ์ผ์ ์ ์ฅํ๋ ๋๋ ํ ๋ฆฌ. |
| .next/ | ๋น๋ ๊ฒฐ๊ณผ๋ฌผ์ด ์ ์ฅ๋๋ ๋๋ ํ ๋ฆฌ. ๊ฐ๋ฐ ์ค์๋ ์ ๊ฒฝ ์ธ ํ์ ์์. |
| node_modules/ | ํ๋ก์ ํธ์ ์ข ์์ฑ(ํจํค์ง)์ด ์ ์ฅ๋๋ ๋๋ ํ ๋ฆฌ |
| package.json | ํ๋ก์ ํธ์ ์ด๋ฆ, ๋ฒ์ , ์คํฌ๋ฆฝํธ, ์ค์น๋ ํจํค์ง ๋ชฉ๋ก(์ข ์์ฑ)์ด ์ ์๋จ |
| next.config.js | Next.js ํ๋ ์์ํฌ์ ์ค์ ํ์ผ |
1-5 Root Layout (app/layout.js)
app/layout.tsx๋ Root Layout์ผ๋ก, Next.js ์ฑ์ ์ต์์ UI๋ฅผ ์ ์.
์ ์ฒด HTML ๊ตฌ์กฐ(<html>, <body>)๋ฅผ ์ค์ .
์ด ์ปดํฌ๋ํธ ๋ด๋ถ์ ์ ์๋ UI๋ ๋ชจ๋ ํ์ด์ง์ ๊ฑธ์ณ ๊ณต์ ๋จ.
{children} ์์ฑ์ ํ์ฌ ๊ฒฝ๋ก์ ํด๋นํ๋ ํ์ด์ง(page.tsx)๋ ํ์ ๋ ์ด์์์ ๋ ๋๋งํ ์์น๋ฅผ ์ง์ ํจ.
์ค์ต: ToDo ์ฑ ์ ์ฒด์ ์ ์ฉ๋ Header์ ๊ธฐ๋ณธ ์คํ์ผ๋ง์ Root Layout์ ์ถ๊ฐํจ.
import './globals.css';
// children์ ํ์ฉํด์ ๋ ๋๋ง.
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<header style={{ padding: '20px',borderBottom: '1px solid #eee',textAlign:'center' }}>
<h1>Next.js ToDo App</h1>
</header>
<main style={{ maxWidth: '600px', margin: '20px auto', padding: '0 20px' }}>
{children}
</main>
</body>
</html>
);
}
1-6 ๋ฉ์ธ ํ์ด์ง์์ :
- app/page.js
- ์ผ๋จ ๊ป๋ฐ๊ธฐ๋ง.
// app/page.js
export default function HomePage() {
return (
<div>
<h2>๋์ ํ ์ผ ๋ชฉ๋ก</h2>
<p>์ฌ๊ธฐ์ ToDo ์ถ๊ฐ ํผ์ด ๋ค์ด๊ฐ ์์ </p>
</div>
)
}
Ch2. Server Component๋ฅผ ์ด์ฉํ data fetching
React Server Component ์ดํด
- ๊ธฐ์กด data fetching ๋ฐฉ์์ ์ ํ์ ์ธ ํด๋ผ์ด์ธํธ ๋ ๋๋ง์ ํ์ํ ๋ฐฉ์์ด์์.
- document์์ฒญ -> JS ์๋ต / ๋ค์ด๋ก๋ -> JSํด์-> API ์์ฒญ -> ์๋ฒ API ์ฒ๋ฆฌ / ์๋ต -> ํด๋ผ์ด์ธํธ ์์ -> ํ์ฑ -> ๋ ๋๋ง
- SSR๋ฐฉ์์ ์ด๋ฏธ ์๋ฒ์์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ด์์.
- document ์์ฒญ -> ์๋ฒ์์ ๋ชจ๋ ์ฒ๋ฆฌ -> HTML ์๋ต -> ๋น ๋ฅธ ๋ ๋๋ง
- Server component๋ ํด๋ผ์ด์ธํธ ํจ์นญ์ ์์ ๊ณ ์๋ฒ์์ data ํ๋/๋ฆฌ์กํธ ๋ถ๋ถ ๋ ๋๋ง ์ดํ์ ๊ฒฐ๊ณผ๋ฅผ ํด๋ผ์๊ฒ ์ ๋ฌ.
- Client ์ปดํฌ๋ํธ์ Server ์ปดํฌ๋ํธ๊ฐ ๊ณต์กดํ๋ ํํ.
- ์ app/page.tsx ๊ฐ ์ด๋ฏธ ์๋ฒ์ปดํฌ๋ํธ.

- React Server Component(RSC)๋ ์๋ฒ์์ ์คํ๋๋ฉฐ, async ํจ์ ํํ์ด ๊ฐ๋ฅํ๋ค.
- async/await๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ์ง์ ๊ฐ์ ธ์ด.
- RSC๋ ํด๋ผ์ด์ธํธ ์ธก ๋ ๋๋ง(CSR)์์ ๋ฐ์ํ๋ ์ถ๊ฐ์ ์ธ ๋คํธ์ํฌ ์์ฒญ์ ์ค์.
- ํด๋ผ์ด์ธํธ ๋ด๋ ค๋ฐ๋ ๋ฒ๋ค ์ฌ์ด์ฆ ์์์ง
- ๋ณต์กํ ํด๋ผ์ด์ธํธ ๋ก์ง ๊ฐ์
- RSC๋ฅผ ์ํด์๋ ์๋ฒ์ฌ์ด๋ ๊ด๋ฆฌ๊ฐ ํ์ํจ.
ToDo ๋ชฉ๋ก ๊ฐ์ ธ์ ๋ณด์ฌ์ฃผ๊ธฐ
- ๋ฐ์ดํฐ ์๋น์ค ํจ์ ์์ฑ
- src/lib/types.ts
- ํ์ ์ ์ ํ๋ ํด๋๊ธฐ.
export type Todo = { id: string; title: string; isCompleted: boolean; };- src/lib/todos.ts
- ์๋ฒ์ปดํฌ๋ํธ ์ฝ๋๋ก ์๋ฒ ์๋ต์ ๋ชจ์๋ก ์์ฑ.
- ์๋๋ DB์ฐ๊ฒฐ๋ฑ ํ์..
import { Todo } from './types'; export async function getTodos(): Promise<Todo[]> { console.log('--- [์๋ฒ ๋ก๊ทธ] ToDo ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์์ ํจ์นญ ์ค ---'); // ์ค์ API ํธ์ถ์ ์๋ฎฌ๋ ์ด์ . ๋ฐ์ดํฐ ํ๋์ฝ๋ฉํด์ ๋ฐํ. await new Promise(resolve => setTimeout(resolve, 1000)); // ๋์ถฉ 1์ด ์ง์ฐ return [ { id: '1', title: 'Next.js ํํ ๋ฆฌ์ผ ์ค๋น', isCompleted: false }, { id: '2', title: '์ฝ๋ ๋ฆฌ๋ทฐ ์งํ', isCompleted: true }, { id: '3', title: '์ ์ฌ ์์ฌ ์์ฝ', isCompleted: false }, ]; }
๋ฉ์ธ ํ์ด์ง (app/page.tsx) ์์
- app/page.tsx
- getTodoํจ์๋ฅผ ํธ์ถํด์ ๋ฐ์ดํฐ ๋ฐ์์ ๋ ๋๋ง.
- async ํจ์๋ผ๊ณ await๋ก ๋ฐ์ ์๊ฐ ์์.
import { getTodos } from '@/lib/todos';
import { Todo } from '@/lib/types';
// async ํจ์ํ ์ปดํฌ๋ํธ ์ ์ (Server Component)
export default async function HomePage() {
const todos: Todo[] = await getTodos(); // ์๋ฒ ์ปดํฌ๋ํธ์์ ์ง์ ๋ฐ์ดํฐ ํจ์นญ
console.log('--- [์๋ฒ ๋ก๊ทธ] ToDo ๋ชฉ๋ก ๋ ๋๋ง ์๋ฃ ---');
return (
<div>
<h2>๋์ ํ ์ผ ๋ชฉ๋ก</h2>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.isCompleted ? 'line-through' : 'none', margin: '8px 0' }}>
{todo.title}
</li>
))}
</ul>
{/* ToDo ์ถ๊ฐ ํผ์ ๋ค์ ์ฑํฐ์์ ๊ตฌํ */}
</div>
);
}
- ํ์ธํ ๊ฒ
- ํฐ๋ฏธ๋ ์ฐฝ์์ [์๋ฒ ๋ก๊ทธ]๊ฐ ์ฐํ๋์ง ํ์ธ.
- ๊ฐ๋ฐ์ ๋๊ตฌ์ '๋คํธ์ํฌ' ํญ์์ ๋ฉ์ธ ํ์ด์ง ์์ฒญ(localhost:3000)์ ์๋ต(Response/Payload)์ ํ์ธ.
- ์๋ต HTML ๋ด๋ถ์ ์ด๋ฏธ ToDo ๋ชฉ๋ก์ด ํฌํจ๋์ด ์์์ ํ์ธ.
Ch 3. Client Component๋ก ์ธํฐ๋์ ์ถ๊ฐ
- Client Component๋ ๋ธ๋ผ์ฐ์ ์์ ๋ ๋๋ง๋จ
- ๋ฐ๋์ ํ์ผ ์๋จ์ 'use client' ์ง์์ด๋ฅผ ์ ์ธํด์ผ ํจ.
- useState, useEffect, onClick ๋ฑ ํด๋ผ์ด์ธํธ ์ํ ๊ด๋ฆฌ๊ฐ ํ์ํ ๊ฒฝ์ฐ์ ์ฌ์ฉ.
ToDo ์ฑ ์์: ToDo ํญ๋ชฉ ์๋ฃ/๋ฏธ์๋ฃ ์ํ ํ ๊ธ (UI๋์๋ง)
Client Component ์์ฑ
- src/components/TodoItem.tsx ์์ฑ
'use client';
import { useState } from 'react';
import { Todo } from '@/lib/types';
interface TodoItemProps {
initialTodo: Todo;
}
export default function TodoItem({ initialTodo }: TodoItemProps) {
const [todo, setTodo] = useState<Todo>(initialTodo);
const handleToggle = () => {
// ์ฐ์ ํด๋ผ์ด์ธํธ ์ํ๋ง ๋ณ๊ฒฝํด์ ๋ ๋๋ง.(์๋ฒ ๋ฐ์ํ์ง ์์)
setTodo(prev => ({ ...prev, isCompleted: !prev.isCompleted }));
};
return (
<li style={{ listStyle: 'none', display: 'flex', alignItems: 'center', margin: '8px 0' }}>
<input
type="checkbox"
checked={todo.isCompleted}
onChange={handleToggle}
style={{ marginRight: '10px' }}
/>
<span style={{ textDecoration: todo.isCompleted ? 'line-through' : 'none' }}>
{todo.title}
</span>
</li>
);
}
๋ฉ์ธ ํ์ด์ง (app/page.tsx)์ Client Component import
- app/page.tsx ์์
import { getTodos } from '@/lib/todos';
import TodoItem from '@/components/TodoItem'; // Client Component import
import { Todo } from '@/lib/types';
export default async function HomePage() {
const todos: Todo[] = await getTodos();
return (
<div>
<h2>๋์ ํ ์ผ ๋ชฉ๋ก</h2>
<ul style={{ padding: 0 }}>
{todos.map(todo => (
// Server Component๊ฐ Client Component๋ฅผ ๋ ๋๋ง (๋ฐ์ดํฐ ์ ๋ฌ)
<TodoItem key={todo.id} initialTodo={todo} />
))}
</ul>
</div>
);
}
Ch 4. Server Actions๋ฅผ ์ด์ฉํ ๋ฐ์ดํฐ ๋ณ๊ฒฝ(Mutation)
- Server Actions๋ API Route ์์ด ํด๋ผ์ด์ธํธ ํผ์์ ์ง์ ์๋ฒ ์ธก ํจ์๋ฅผ ํธ์ถํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝ(Mutation)ํ๋ ๊ธฐ๋ฅ.
- API Route ๋ API๋ฅผ ์์ฑํ๋ Server side ์ฝ๋
- 'use server' ์ง์์ด๋ฅผ ์ฌ์ฉํด์ผ ํจ,
- ๋ฐ์ดํฐ ๋ณ๊ฒฝ ํ revalidatePath๋ฅผ ์ด์ฉํด ํด๋น ๊ฒฝ๋ก์ ์บ์๋ฅผ ๋ฌดํจํํ๋ฉด ํ์ด์ง๊ฐ ์๋์ผ๋ก re-fetch๋๊ณ UI๊ฐ ๊ฐฑ์ ๋จ.
ToDo ์ฑ ์์: ์๋ก์ด ToDo ํญ๋ชฉ ์ถ๊ฐ
๋ฐ์ดํฐ ์๋น์ค ํจ์ ์์
- src/lib/todos.ts
import { revalidatePath } from 'next/cache';
import { Todo } from './types';
const todos: Todo[] = [
{ id: '1', title: 'Next.js ํํ ๋ฆฌ์ผ ์ค๋น', isCompleted: false },
{ id: '2', title: '์ฝ๋ ๋ฆฌ๋ทฐ ์งํ', isCompleted: true },
{ id: '3', title: '์ ์ฌ ์์ฌ ์์ฝ', isCompleted: false },
];
export async function getTodos(): Promise<Todo[]> {
//...
// todos ๋ฅผ ๋ฐํํ๋๋ก ์์
return todos;
}
// ์๋ฒ ์ก์
ํจ์ ('use server' ์ ์ธ)
// Server Action์ FormData๋ฅผ ์ธ์๋ก ๋ฐ์
export async function addTodo(formData: FormData) {
'use server';
const title = formData.get('title') as string; // as string์ผ๋ก ํ์
๋จ์ธ
if (!title) return;
todos.push({
id: Date.now().toString(),
title: title,
isCompleted: false,
});
// ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋์์ผ๋ฏ๋ก, ํ('/') ํ์ด์ง ๊ฒฝ๋ก์ ์บ์๋ฅผ ๋ฌดํจํ
revalidatePath('/');
}
๋ฉ์ธ ํ์ด์ง์ ํผ ์ถ๊ฐ:
- app/page.tsx
- // app/page.tsx (์์ ) import { getTodos, addTodo } from '@/lib/todos'; // Server Action import import TodoItem from '@/components/TodoItem'; import { Todo } from '@/lib/types'; export default async function HomePage() { const todos: Todo[] = await getTodos(); return ( <div> <h2>๋์ ํ ์ผ ๋ชฉ๋ก</h2> <ul style={{ padding: 0 }}> {todos.map(todo => ( <TodoItem key={todo.id} initialTodo={todo} /> ))} </ul> {/* Server Action์ ์ฌ์ฉํ๋ ํผ */} <form action={addTodo} style={{ marginTop: '20px' }}> <input type="text" name="title" placeholder="์๋ก์ด ํ ์ผ" required style={{ padding: '8px', marginRight: '10px', border: '1px solid #ccc' }} /> <button type="submit" style={{ padding: '8px 15px', cursor: 'pointer' }}> ์ถ๊ฐ </button> </form> </div> ); }
Ch 5. ์๋ฒ์ปดํฌ๋ํธ ์ ๋ฆฌ
Next.js 16์ Server Components์ Server Actions๋ฅผ ํตํด ํ์คํ React ๊ฐ๋ฐ์ ๊ฐ์ํํจ.
Pages Router์์ API Routes๊ฐ ํ์ํ๋ ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์์ ์ด Server Actions๋ก ๋์ฒด๋์ด ๊ฐ๋ฐ ์์ฐ์ฑ์ด ํฅ์.
- Server Component (SC):
- ์๋ฒ์์๋ง ์คํ๋๋ฉฐ, ๋ฐ์ดํฐ ํ์นญ, DB ์ ๊ทผ, ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ฑ์ ๋ด๋นํจ. ๋ธ๋ผ์ฐ์ ๋ก ์ ์ก๋๋ JavaScript ๋ฒ๋ค ํฌ๊ธฐ๋ฅผ ์ค์ฌ ์ด๊ธฐ ๋ก๋ฉ ์๋๋ฅผ ๊ทน๋ํํจ.
- Client Component (CC):
- ๋ธ๋ผ์ฐ์ ์์ ์คํ๋๋ฉฐ, ์ฌ์ฉ์ ์ํธ์์ฉ(ํด๋ฆญ, ์ํ ๊ด๋ฆฌ ๋ฑ)์ ๋ด๋นํจ. ๋ฐ๋์ 'use client'๋ฅผ ๋ช ์ํด์ผ ํจ.
- Server Actions (SA):
- ํด๋ผ์ด์ธํธ ์ธก์์ API Route๋ฅผ ๊ฑฐ์น์ง ์๊ณ ์ง์ ์๋ฒ ์ธก ํจ์๋ฅผ ํธ์ถํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํจ.
- revalidatePath๋ฅผ ํตํด ์บ์๋ฅผ ์๋์ผ๋ก ๋ฌดํจํํ๊ณ UI๋ฅผ ๊ฐฑ์ ํจ์ผ๋ก์จ, ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ์ ๋ฐ์ดํฐ ๋๊ธฐํ ๋ฌธ์ ๋ฅผ ํฌ๊ฒ ๋จ์ํํจ.
Ch 6. Routing (๋ผ์ฐํ )
Next.js 16์ ๋ผ์ฐํ ์ app ๋๋ ํ ๋ฆฌ ๊ธฐ๋ฐ์ ํ์ผ ์์คํ ๋ผ์ฐํ ์ ์ฌ์ฉ
1. ์ค์ฒฉ ๋ผ์ฐํ (Nested Routing) ๋ฐ ๋ ์ด์์ (Layouts)
- ํด๋ ๊ตฌ์กฐ ์์ฒด๊ฐ URL ๊ฒฝ๋ก๋ฅผ ์ ์ํ๋ฉฐ, ํด๋ ์์ ์ ์๋ layout.tsx๋ ํด๋น ๊ฒฝ๋ก์ ํ์ ๊ฒฝ๋ก ๋ชจ๋์ ์ ์ฉ๋์ด UI๋ฅผ ๊ณต์
- / ๊ฒฝ๋ก ์ธ์ /settings ๊ฒฝ๋ก๋ฅผ ์ถ๊ฐํ๊ณ , next/link๋ฅผ ์ฌ์ฉํด ํ์ด์ง๋ฅผ ์ด๋.
ํ์ผ ๊ตฌ์กฐURL ๊ฒฝ๋ก์ค๋ช
| app/layout.tsx | (์ ์ฒด) | ๋ชจ๋ ํ์ด์ง๋ฅผ ๊ฐ์ธ๋ ์ต์์ ๋ ์ด์์ |
| app/page.tsx | / | ToDo ๋ชฉ๋ก ํ์ด์ง |
| app/settings/page.tsx | /settings | ์ค์ ํ์ด์ง |
- app/settings/page.tsx ์์ฑ
export default function SettingsPage() {
return <h2>์ค์ ํ์ด์ง (์ถ๊ฐ ๊ฐ๋ฐ ํ์)</h2>;
}
- ์ฌ์ฉ์๊ฐ /settings๋ก ์ด๋ํ ์ ์๋๋ก next/link ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉ
- app/page.tsx (์์ )
import Link from 'next/link';
// ... HomePage ์ปดํฌ๋ํธ ์ ์
export default async function HomePage() {
// ... (๊ธฐ์กด ToDo ํ์นญ ๋ฐ ๋ ๋๋ง ๋ก์ง)
return (
<div>
{/* ... ToDo ๋ฆฌ์คํธ ๋ฐ ํผ */}
<Link href="/settings" style={{ display: 'block', marginTop: '20px' }}>
์ค์ ์ผ๋ก ์ด๋
</Link>
</div>
);
}
2. ํด๋ผ์ด์ธํธ ๋ผ์ฐํ ํ (Hooks) ํ์ฉ: ๊ฒฝ๋ก ํ์ ๋ฐ ์ ์ด
- next/navigation์์ ์ ๊ณตํ๋ ํ ๋ค์ Client Component์์๋ง ์ฌ์ฉํ ์ ์์
- URL, ๊ฒฝ๋ก, ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ๋ฑ์ ์ฝ๊ฑฐ๋ ํ์ด์ง ์ด๋์ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ์ ์ดํ ๋ ์ฌ์ฉ.
- ๋ฐ๋์ ํ์ผ ์๋จ์ 'use client ๋ฅผ ์ ์ธํ Client Component ๋ด์์๋ง ์ฌ์ฉ
Hook์ฉ๋์ฌ์ฉ ์์
| usePathname | ํ์ฌ ์์ฒญ๋ URL์ ๊ฒฝ๋ก(pathname)๋ฅผ ๋ฌธ์์ด๋ก ์ฝ์. | /settings ๋๋ /todos/123 ํ์ |
| useRouter | ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ํ์ด์ง ์ด๋์ ์ ์ด. | router.push('/new-path') |
- ํ์ฌ ๊ฒฝ๋ก๋ฅผ ์ฝ์ด ํ์ฑ ๋งํฌ๋ฅผ ๊ฐ์กฐํ๋ ๋ด๋น๊ฒ์ด์ ์ปดํฌ๋ํธ ์์ฑ
// components/Nav.tsx (์ ํ์ผ)
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export default function Nav() {
const pathname = usePathname();
const links = [
{ href: '/', label: 'Todo ๋ชฉ๋ก' },
{ href: '/settings', label: '์ค์ ' },
];
return (
<nav style={{ padding: '10px 0', display: 'flex', gap: '15px', justifyContent: 'center' }}>
{links.map(link => (
<Link
key={link.href}
href={link.href}
style={{
// ๐ก ์์ ์์ : ๊ธฐ๋ณธ์ #555, ํ์ฑํ ์ #0070f3 (ํ๋์)
color: pathname === link.href ? '#0070f3' : '#555',
fontWeight: pathname === link.href ? 'bold' : 'normal',
textDecoration: 'none'
}}
>
{link.label}
</Link>
))}
</nav>
);
}
- ๋ชจ๋ ํ์ด์ง์ ํค๋์ Nav ์ปดํฌ๋ํธ๋ฅผ ๋ฐฐ์นํ์ฌ ๊ฒฝ๋ก ์ด๋์ ๊ด์ฐฐ
// app/layout.tsx (์์ )
// ...
import Nav from '@/components/Nav';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<header style={{ padding: '20px', borderBottom: '1px solid #eee', textAlign: 'center' }}>
<h1>Next.js ToDo App</h1>
<Nav /> {/* ํค๋ ๋ด๋ถ์ Nav ๋ฐฐ์น */}
</header>
<main style={{ maxWidth: '600px', margin: '20px auto', padding: '0 20px' }}>
{children}
</main>
</body>
</html>
);
}
Ch 7. ๋ฆฌ์์ค ์ต์ ํ
Next.js์ <Image>์ next/font๋ฅผ ํตํ ๋ฆฌ์์ค ์ต์ ํ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณต๋จ.
1. ์ด๋ฏธ์ง ์ต์ ํ (next/image)
<img /> ํ๊ทธ ๋์ Next.js์ ๋ด์ฅ <Image /> ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํด์ผ ํจ.
- ์ด๋ฏธ์ง ํฌ๊ธฐ๋ฅผ ์๋์ผ๋ก ์ต์ ํํ์ฌ ๋ธ๋ผ์ฐ์ ์ ๋ง๋ WebP/AVIF ๊ฐ์ ์ต์ ํฌ๋งท์ผ๋ก ๋ณํํจ.
- Lazy Loading(์ง์ฐ ๋ก๋ฉ)์ด ๊ธฐ๋ณธ ์ ์ฉ๋์ด ๋ทฐํฌํธ ๋ฐ๊นฅ์ ์ด๋ฏธ์ง๋ ๋ก๋ฉํ์ง ์์.
- app/layout.tsx (์์)
import Image from 'next/image'; // Image ์ปดํฌ๋ํธ import
// ...
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<header style={{ display: 'flex', alignItems: 'center', padding: '20px', borderBottom: '1px solid #eee' }}>
{/* public/logo.png๋ฅผ ๊ฐ์ */}
<Image src="/logo.png" alt="Logo" width={40} height={40} style={{ marginRight: '10px' }} />
<h1>Next.js ToDo App</h1>
</header>
{/* ... main */}
</body>
</html>
);
}
2. ํฐํธ ์ต์ ํ (next/font)
next/font๋ ์ธ๋ถ ๋คํธ์ํฌ ์์ฒญ ์์ด ํฐํธ ํ์ผ์ ๋น๋ ์์ ์ ์ต์ ํํ๊ณ ์๋์ผ๋ก ํธ์คํ ํ์ฌ ํฐํธ ๋ก๋ฉ์ผ๋ก ์ธํ ๋ ์ด์์ ์ด๋(CLS, Cumulative Layout Shift)์ ๋ฐฉ์งํจ.
- ์ฌ์ฉ๋ฒ: Google Fonts ๋๋ Local Fonts๋ฅผ importํ์ฌ ์ฌ์ฉํจ.
// app/layout.tsx (์ผ๋ถ)
import { Inter } from 'next/font/google'; // Google Fonts ์ฌ์ฉ ์์
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
// font ๋ณ์๋ฅผ className์ผ๋ก body์ ์ ์ฉ
<html lang="ko" className={inter.className}>
<body>
{/* ... */}
</body>
</html>
);
}'๐ ํ๋ก ํธ์๋(FE)' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ํ๋ก ํธ์๋ ์์ํ ํผ๋๋ฐฑ (0) | 2026.01.25 |
|---|---|
| ํ๋ก ํธ์๋ ํ ์คํธ ๋๊ตฌ ( MSW, Vitest, React Testing Library ) (0) | 2026.01.21 |
| FE ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ(FE ์ํคํ ์ณ) (0) | 2026.01.20 |
| React ์ํ๊ด๋ฆฌ ( ์ง์ญ, ์ ์ญ, ์๋ฒ ์ปดํฌ๋ํธ ) (0) | 2026.01.20 |
| React ์ฑ๋ฅ ์ต์ ํ (0) | 2026.01.20 |