Next.js ์‹œ์ž‘ํ•˜๊ธฐ

2026. 1. 22. 05:16ยท๐Ÿ’™ ํ”„๋ก ํŠธ์—”๋“œ(FE)
728x90

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
'๐Ÿ’™ ํ”„๋ก ํŠธ์—”๋“œ(FE)' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • ํ”„๋ก ํŠธ์—”๋“œ ์ž์ž˜ํ•œ ํ”ผ๋“œ๋ฐฑ
  • ํ”„๋ก ํŠธ์—”๋“œ ํ…Œ์ŠคํŠธ ๋„๊ตฌ ( MSW, Vitest, React Testing Library )
  • FE ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ(FE ์•„ํ‚คํ…์ณ)
  • React ์ƒํƒœ๊ด€๋ฆฌ ( ์ง€์—ญ, ์ „์—ญ, ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ )
์—ฐ์žŽ(lotus leaf)
์—ฐ์žŽ(lotus leaf)
  • ์—ฐ์žŽ(lotus leaf)
    lotus' s develog ๐Ÿƒ
    ์—ฐ์žŽ(lotus leaf)
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (79)
      • โœ๏ธ ๊ฐœ๋ฐœํšŒ๊ณ ๋ก (5)
      • ๐Ÿงฎ ์•Œ๊ณ ๋ฆฌ์ฆ˜ (3)
      • ๐Ÿ’™ ํ”„๋ก ํŠธ์—”๋“œ(FE) (19)
        • HTML (0)
        • CSS (0)
        • Javascript (0)
        • React (0)
        • Next.js (0)
        • webpack & babel (0)
      • ๐Ÿ’ป ๋ฐฑ์—”๋“œ(BE) (2)
        • Nest.js (0)
        • Express.js (0)
        • MySQL (1)
      • โš™๏ธ ์ธํ”„๋ผ(Devops) (2)
      • ๐Ÿค– AI (1)
      • ๐ŸŒ WEB (8)
      • ๐Ÿ’ป CS (16)
        • ์ž๋ฃŒ๊ตฌ์กฐ (0)
        • ์ปดํ“จํ„ฐ ๋„คํŠธ์›Œํฌ (1)
        • ์šด์˜์ฒด์ œ (0)
        • ์ธ๊ณต์ง€๋Šฅ (8)
        • ์›น ๋ณด์•ˆ (1)
        • ํด๋ผ์šฐ๋“œ ์ปดํ“จํŒ… (6)
      • ๐Ÿ–‹๏ธ DevLog (1)
      • ๐Ÿฆพ ๋กœ๋ณดํ‹ฑ์Šค (4)
      • ๐Ÿ“— ๋„ค์ด๋ฒ„๋ถ€์ŠคํŠธ์บ ํ”„ ์›น ๋ชจ๋ฐ”์ผ (0)
      • ๐ŸŽฎ Unity(C#) (10)
      • ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด (4)
        • C (4)
        • C++ (0)
        • Java (0)
        • Python (0)
      • MSA (1)
  • ๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

    • ํ™ˆ
    • ํƒœ๊ทธ
    • ๋ฐฉ๋ช…๋ก
  • ๋งํฌ

  • ๊ณต์ง€์‚ฌํ•ญ

  • ์ธ๊ธฐ ๊ธ€

  • ํƒœ๊ทธ

    deploy-aws
    next.js12
    ros bridge
    ํŒŒ์ผํŠธ๋ฆฌ
    ros workspace
    ์•Œ๊ณ ๋ฆฌ์ฆ˜
    client-streaming
    ๋ฆฌ์•กํŠธ
    advaned detail
    hard margin svm
    ์ŠคํŒธ๋ถ„๋ฅ˜๊ธฐ
    ๊ธฐ์ดˆ์•Œ๊ณ ๋ฆฌ์ฆ˜
    isaac automator
    AWS
    auto-scaling
    gaussian rbf svm
    ์กฐํ•ฉ
    soft margin svm
    ์ดํ™”์—ฌ์ž๋Œ€ํ•™๊ต #๋„์ „ํ•™๊ธฐ์ œ
    ์ˆœ์—ด
    c++
    turtlebot
    ์ŠคํŒธ๋ฉ”์ผ๋ถ„๋ฅ˜๊ธฐ
    C#
    nav2
    c์–ธ์–ด
    Devops #๋Œ€๊ทœ๋ชจํŠธ๋ž˜ํ”ฝ์ฒ˜๋ฆฌ
    ๋ฐฑ์ค€
    C
    ์ฝ”๋”ํŒจ๋“œ
  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • ์ตœ๊ทผ ๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.6
์—ฐ์žŽ(lotus leaf)
Next.js ์‹œ์ž‘ํ•˜๊ธฐ
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”