🎯 2일차: 파일 기반 λΌμš°νŒ… + λΆˆλ³€μ„± μ™„μ „ 정볡!

짱효·2025λ…„ 9μ›” 21일

✏️

λͺ©λ‘ 보기
7/7

πŸ“š ν•™μŠ΅ λͺ©ν‘œ

  • Next.js 파일 기반 λΌμš°νŒ… μ‹œμŠ€ν…œ λ§ˆμŠ€ν„°
  • React λΆˆλ³€μ„±κ³Ό μƒνƒœ μ—…λ°μ΄νŠΈ μ™„μ „ 이해
  • 동적 λΌμš°νŒ… μ‹€μŠ΅μœΌλ‘œ 싀무 λŠ₯λ ₯ ν–₯상

πŸ“‚ Part 1: 파일 기반 λΌμš°νŒ… μ‹œμŠ€ν…œ

🎯 전톡적인 λΌμš°νŒ… vs 파일 기반 λΌμš°νŒ…

전톡적인 React λΌμš°νŒ… (React Router)

// App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/blog" element={<Blog />} />
        <Route path="/blog/:slug" element={<BlogPost />} />
        <Route path="/products/:category" element={<Products />} />
        <Route path="/user/:id/profile" element={<UserProfile />} />
      </Routes>
    </BrowserRouter>
  )
}

문제점:

  • λΌμš°νŠΈκ°€ λ§Žμ•„μ§ˆμˆ˜λ‘ μ½”λ“œκ°€ λ³΅μž‘ν•΄μ§
  • 파일 ꡬ쑰와 URL ꡬ쑰가 λ”°λ‘œ 관리됨
  • 라우트 μΆ”κ°€ν•  λ•Œλ§ˆλ‹€ μ„€μ • 파일 μˆ˜μ • ν•„μš”

Next.js 파일 기반 λΌμš°νŒ… (혁λͺ…적!)

app/                    URL
β”œβ”€β”€ page.js            β†’ '/'
β”œβ”€β”€ about/
β”‚   └── page.js        β†’ '/about'
β”œβ”€β”€ blog/
β”‚   β”œβ”€β”€ page.js        β†’ '/blog'
β”‚   └── [slug]/
β”‚       └── page.js    β†’ '/blog/[slug]'
β”œβ”€β”€ products/
β”‚   └── [category]/
β”‚       └── page.js    β†’ '/products/[category]'
└── user/
    └── [id]/
        └── profile/
            └── page.js β†’ '/user/[id]/profile'

μž₯점:

  • 파일 = URL 직관적 λ§€ν•‘
  • μžλ™ 라우트 생성 - μ„€μ • λΆˆν•„μš”
  • μ½”λ“œ λΆ„ν•  μžλ™ν™” - μ„±λŠ₯ μ΅œμ ν™”
  • 쀑첩 λΌμš°νŒ… 쉬움

πŸ”₯ App Router 파일 기반 λΌμš°νŒ… μ™„μ „ 뢄석

1️⃣ κΈ°λ³Έ 파일 ꡬ쑰와 μ—­ν• 

app/
β”œβ”€β”€ layout.js          🏠 루트 λ ˆμ΄μ•„μ›ƒ (ν•„μˆ˜)
β”œβ”€β”€ page.js            πŸ“„ ν™ˆνŽ˜μ΄μ§€
β”œβ”€β”€ loading.js         ⏳ λ‘œλ”© UI
β”œβ”€β”€ error.js           ❌ μ—λŸ¬ UI  
β”œβ”€β”€ not-found.js       πŸ” 404 νŽ˜μ΄μ§€
β”œβ”€β”€ global-error.js    πŸ’₯ μ „μ—­ μ—λŸ¬
└── favicon.ico        🎯 νŒŒλΉ„μ½˜

2️⃣ 각 파일의 μ‹€μ œ μ‚¬μš©λ²•

page.js - νŽ˜μ΄μ§€ μ»΄ν¬λ„ŒνŠΈ

// app/page.js - ν™ˆνŽ˜μ΄μ§€ ('/' 경둜)
export default function HomePage() {
  return (
    <div>
      <h1>ν™ˆνŽ˜μ΄μ§€μ— μ˜€μ‹  것을 ν™˜μ˜ν•©λ‹ˆλ‹€!</h1>
      <p>Next.js App Routerλ₯Ό μ‚¬μš©ν•œ ν™ˆνŽ˜μ΄μ§€μž…λ‹ˆλ‹€.</p>
    </div>
  )
}

// app/about/page.js - μ†Œκ°œ νŽ˜μ΄μ§€ ('/about' 경둜)
export default function AboutPage() {
  return (
    <div>
      <h1>νšŒμ‚¬ μ†Œκ°œ</h1>
      <p>μš°λ¦¬λŠ” ν˜μ‹ μ μΈ 기술둜 세상을 λ°”κΏ”λ‚˜κ°€κ³  μžˆμŠ΅λ‹ˆλ‹€.</p>
    </div>
  )
}

layout.js - λ ˆμ΄μ•„μ›ƒ μ»΄ν¬λ„ŒνŠΈ

// app/layout.js - 루트 λ ˆμ΄μ•„μ›ƒ (λͺ¨λ“  νŽ˜μ΄μ§€μ— 적용)
export const metadata = {
  title: '우리 νšŒμ‚¬',
  description: 'ν˜μ‹ μ μΈ 기술 νšŒμ‚¬'
}

export default function RootLayout({ children }) {
  return (
    <html lang="ko">
      <body>
        <header>
          <nav>
            <a href="/">ν™ˆ</a>
            <a href="/about">μ†Œκ°œ</a>
            <a href="/blog">λΈ”λ‘œκ·Έ</a>
          </nav>
        </header>
        
        <main>{children}</main>
        
        <footer>
          <p>&copy; 2024 우리 νšŒμ‚¬</p>
        </footer>
      </body>
    </html>
  )
}

// app/blog/layout.js - λΈ”λ‘œκ·Έ μ„Ήμ…˜ λ ˆμ΄μ•„μ›ƒ
export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      <aside>
        <h3>μΉ΄ν…Œκ³ λ¦¬</h3>
        <ul>
          <li><a href="/blog/tech">기술</a></li>
          <li><a href="/blog/business">λΉ„μ¦ˆλ‹ˆμŠ€</a></li>
        </ul>
      </aside>
      <div className="blog-content">
        {children}
      </div>
    </div>
  )
}

loading.js - λ‘œλ”© UI

// app/loading.js - μ „μ—­ λ‘œλ”©
export default function Loading() {
  return (
    <div className="loading-container">
      <div className="spinner"></div>
      <p>λ‘œλ”© 쀑...</p>
    </div>
  )
}

// app/blog/loading.js - λΈ”λ‘œκ·Έ μ„Ήμ…˜ λ‘œλ”©
export default function BlogLoading() {
  return (
    <div>
      <div className="skeleton-title"></div>
      <div className="skeleton-content"></div>
    </div>
  )
}

error.js - μ—λŸ¬ 처리

// app/error.js - μ „μ—­ μ—λŸ¬
'use client' // μ—λŸ¬ μ»΄ν¬λ„ŒνŠΈλŠ” ν΄λΌμ΄μ–ΈνŠΈ μ»΄ν¬λ„ŒνŠΈμ—¬μ•Ό 함

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>λ‹€μ‹œ μ‹œλ„</button>
    </div>
  )
}

3️⃣ 쀑첩 λΌμš°νŒ…κ³Ό λ ˆμ΄μ•„μ›ƒ 상속

app/
β”œβ”€β”€ layout.js           (루트 λ ˆμ΄μ•„μ›ƒ)
β”œβ”€β”€ page.js            (ν™ˆ)
β”œβ”€β”€ dashboard/
β”‚   β”œβ”€β”€ layout.js      (λŒ€μ‹œλ³΄λ“œ λ ˆμ΄μ•„μ›ƒ)
β”‚   β”œβ”€β”€ page.js        (λŒ€μ‹œλ³΄λ“œ ν™ˆ)
β”‚   β”œβ”€β”€ analytics/
β”‚   β”‚   β”œβ”€β”€ layout.js  (뢄석 μ„Ήμ…˜ λ ˆμ΄μ•„μ›ƒ)  
β”‚   β”‚   └── page.js    (뢄석 νŽ˜μ΄μ§€)
β”‚   └── settings/
β”‚       └── page.js    (μ„€μ • νŽ˜μ΄μ§€)

λ ˆμ΄μ•„μ›ƒ 상속 ꡬ쑰:

/dashboard/analytics νŽ˜μ΄μ§€ μ ‘μ†μ‹œ:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 루트 λ ˆμ΄μ•„μ›ƒ (app/layout.js)         β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ λŒ€μ‹œλ³΄λ“œ λ ˆμ΄μ•„μ›ƒ (dashboard/)    β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ 뢄석 λ ˆμ΄μ•„μ›ƒ (analytics/)   β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ 뢄석 νŽ˜μ΄μ§€ λ‚΄μš©        β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”„ Part 2: React λΆˆλ³€μ„±κ³Ό μƒνƒœ μ—…λ°μ΄νŠΈ

🎯 λΆˆλ³€μ„±μ΄ λ­”κ°€μš”?

λΆˆλ³€μ„±(Immutability) = 원본을 κ±΄λ“œλ¦¬μ§€ 말고 μƒˆλ‘œμš΄ 것을 λ§Œλ“€μ–΄!

일상 μ˜ˆμ‹œλ‘œ μ΄ν•΄ν•˜κΈ°:

❌ 가변적(Mutable):
원본 λ¬Έμ„œμ— 직접 μˆ˜μ • β†’ 원본이 변경됨

βœ… λΆˆλ³€μ (Immutable):  
원본 λ¬Έμ„œλŠ” κ·ΈλŒ€λ‘œ 두고 β†’ 볡사본에 μˆ˜μ • β†’ μƒˆ λ¬Έμ„œ 생성

πŸ”₯ JavaScriptμ—μ„œ λΆˆλ³€μ„± 원칙

1️⃣ μ›μ‹œ νƒ€μž…μ€ 기본적으둜 λΆˆλ³€

let a = 5
let b = a  // κ°’ 볡사
b = 10     // aλŠ” μ—¬μ „νžˆ 5

console.log(a) // 5
console.log(b) // 10

2️⃣ 객체와 배열은 μ°Έμ‘° νƒ€μž… (주의!)

// ❌ 잘λͺ»λœ 방법 - 원본 μˆ˜μ •
let user = { name: 'κΉ€κ°œλ°œ', age: 25 }
let updatedUser = user  // μ°Έμ‘° 볡사!
updatedUser.name = '이개발'

console.log(user.name)        // '이개발' (원본이 λ°”λ€œ!)
console.log(updatedUser.name) // '이개발'

// βœ… μ˜¬λ°”λ₯Έ 방법 - μƒˆ 객체 생성
let user = { name: 'κΉ€κ°œλ°œ', age: 25 }
let updatedUser = { ...user, name: '이개발' }  // μŠ€ν”„λ ˆλ“œλ‘œ 볡사

console.log(user.name)        // 'κΉ€κ°œλ°œ' (원본 μœ μ§€!)
console.log(updatedUser.name) // '이개발'

πŸ”₯ Reactμ—μ„œ λΆˆλ³€μ„±μ΄ μ€‘μš”ν•œ 이유

React의 μƒνƒœ 비ꡐ 방식

// React λ‚΄λΆ€ λ™μž‘ (λ‹¨μˆœν™”)
function useState(initialValue) {
  let state = initialValue
  
  function setState(newValue) {
    // πŸ”₯ 얕은 비ꡐ (μ°Έμ‘° 비ꡐ)
    if (state === newValue) {
      return // κ°™μœΌλ©΄ λ¦¬λ Œλ”λ§ μ•ˆν•¨!
    }
    
    state = newValue
    reRender() // λ‹€λ₯΄λ©΄ λ¦¬λ Œλ”λ§
  }
  
  return [state, setState]
}

λΆˆλ³€μ„± μœ„λ°˜μ‹œ λ°œμƒν•˜λŠ” 문제

// ❌ λ¬Έμ œκ°€ μžˆλŠ” μ½”λ“œ
function UserProfile() {
  const [user, setUser] = useState({
    name: 'κΉ€κ°œλ°œ',
    hobbies: ['μ½”λ”©', 'λ…μ„œ'],
    address: { city: 'μ„œμšΈ', district: '강남ꡬ' }
  })
  
  const addHobby = (newHobby) => {
    // ❌ 원본 λ°°μ—΄ 직접 μˆ˜μ •!
    user.hobbies.push(newHobby)
    setUser(user) // React: "μ–΄? 같은 객체넀? λ¦¬λ Œλ”λ§ μ•ˆν•΄!"
  }
  
  const updateCity = (newCity) => {
    // ❌ 원본 객체 직접 μˆ˜μ •!
    user.address.city = newCity
    setUser(user) // React: "μ–΄? 같은 객체넀? λ¦¬λ Œλ”λ§ μ•ˆν•΄!"
  }
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>μ·¨λ―Έ: {user.hobbies.join(', ')}</p>  {/* μ—…λ°μ΄νŠΈ μ•ˆλ¨! */}
      <p>λ„μ‹œ: {user.address.city}</p>        {/* μ—…λ°μ΄νŠΈ μ•ˆλ¨! */}
    </div>
  )
}

μ˜¬λ°”λ₯Έ λΆˆλ³€μ„± μ—…λ°μ΄νŠΈ

// βœ… μ˜¬λ°”λ₯Έ μ½”λ“œ
function UserProfile() {
  const [user, setUser] = useState({
    name: 'κΉ€κ°œλ°œ',
    hobbies: ['μ½”λ”©', 'λ…μ„œ'],
    address: { city: 'μ„œμšΈ', district: '강남ꡬ' }
  })
  
  const addHobby = (newHobby) => {
    // βœ… μƒˆ λ°°μ—΄ 생성
    setUser({
      ...user,                          // κΈ°μ‘΄ 객체 볡사
      hobbies: [...user.hobbies, newHobby]  // μƒˆ λ°°μ—΄ 생성
    })
  }
  
  const removeHobby = (hobbyToRemove) => {
    // βœ… ν•„ν„°λ‘œ μƒˆ λ°°μ—΄ 생성
    setUser({
      ...user,
      hobbies: user.hobbies.filter(hobby => hobby !== hobbyToRemove)
    })
  }
  
  const updateHobby = (index, newHobby) => {
    // βœ… map으둜 μƒˆ λ°°μ—΄ 생성
    setUser({
      ...user,
      hobbies: user.hobbies.map((hobby, i) => 
        i === index ? newHobby : hobby
      )
    })
  }
  
  const updateCity = (newCity) => {
    // βœ… 쀑첩 객체도 μƒˆλ‘œ 생성
    setUser({
      ...user,                    // μ΅œμƒμœ„ 볡사
      address: {
        ...user.address,          // address 객체 볡사
        city: newCity             // city만 μ—…λ°μ΄νŠΈ
      }
    })
  }
  
  return (
    <div>
      <h2>{user.name}</h2>
      <div>
        <h3>μ·¨λ―Έ λͺ©λ‘:</h3>
        {user.hobbies.map((hobby, index) => (
          <div key={index}>
            {hobby}
            <button onClick={() => removeHobby(hobby)}>μ‚­μ œ</button>
            <button onClick={() => updateHobby(index, hobby + '!')}>μˆ˜μ •</button>
          </div>
        ))}
        <button onClick={() => addHobby('μ˜ν™”κ°μƒ')}>μ·¨λ―Έ μΆ”κ°€</button>
      </div>
      
      <div>
        <p>μ£Όμ†Œ: {user.address.city}, {user.address.district}</p>
        <button onClick={() => updateCity('λΆ€μ‚°')}>λ„μ‹œ λ³€κ²½</button>
      </div>
    </div>
  )
}

🎯 Part 3: 동적 λΌμš°νŒ… μ‹€μŠ΅

πŸ”₯ 동적 λΌμš°νŒ…μ΄λž€?

URL의 일뢀λ₯Ό λ³€μˆ˜λ‘œ μ‚¬μš©ν•˜λŠ” λΌμš°νŒ…

정적 λΌμš°νŒ…: /blog/react-tutorial
동적 λΌμš°νŒ…: /blog/[slug]  β†’ /blog/어떀값이든

πŸ“‚ 동적 λΌμš°νŒ… 파일 ꡬ쑰

1️⃣ κΈ°λ³Έ 동적 λΌμš°νŒ…

app/
β”œβ”€β”€ blog/
β”‚   β”œβ”€β”€ page.js          β†’ '/blog'
β”‚   └── [slug]/
β”‚       └── page.js      β†’ '/blog/react-tutorial'
β”‚                        β†’ '/blog/javascript-guide'  
β”‚                        β†’ '/blog/next-js-tips'
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
  const { slug } = params  // URLμ—μ„œ slug μΆ”μΆœ
  
  return (
    <div>
      <h1>λΈ”λ‘œκ·Έ 포슀트</h1>
      <p>ν˜„μž¬ 슬러그: {slug}</p>
    </div>
  )
}

// λ°©λ¬Έ μ˜ˆμ‹œ:
// /blog/react-tutorial β†’ params = { slug: 'react-tutorial' }
// /blog/javascript-guide β†’ params = { slug: 'javascript-guide' }

2️⃣ 쀑첩 동적 λΌμš°νŒ…

app/
β”œβ”€β”€ user/
β”‚   └── [id]/
β”‚       β”œβ”€β”€ page.js           β†’ '/user/123'
β”‚       β”œβ”€β”€ profile/
β”‚       β”‚   └── page.js       β†’ '/user/123/profile'  
β”‚       └── settings/
β”‚           └── page.js       β†’ '/user/123/settings'
// app/user/[id]/page.js
export default function UserPage({ params }) {
  const { id } = params
  
  return (
    <div>
      <h1>μ‚¬μš©μž λŒ€μ‹œλ³΄λ“œ</h1>
      <p>μ‚¬μš©μž ID: {id}</p>
    </div>
  )
}

// app/user/[id]/profile/page.js
export default function UserProfile({ params }) {
  const { id } = params
  
  return (
    <div>
      <h1>μ‚¬μš©μž ν”„λ‘œν•„</h1>
      <p>μ‚¬μš©μž ID: {id}의 ν”„λ‘œν•„ νŽ˜μ΄μ§€μž…λ‹ˆλ‹€.</p>
    </div>
  )
}

3️⃣ 닀쀑 동적 λΌμš°νŒ…

app/
β”œβ”€β”€ shop/
β”‚   └── [category]/
β”‚       └── [productId]/
β”‚           └── page.js    β†’ '/shop/electronics/laptop-123'
β”‚                          β†’ '/shop/clothing/shirt-456'
// app/shop/[category]/[productId]/page.js
export default function ProductPage({ params }) {
  const { category, productId } = params
  
  return (
    <div>
      <h1>μƒν’ˆ 상세</h1>
      <p>μΉ΄ν…Œκ³ λ¦¬: {category}</p>
      <p>μƒν’ˆ ID: {productId}</p>
    </div>
  )
}

// λ°©λ¬Έ μ˜ˆμ‹œ:
// /shop/electronics/laptop-123 β†’ { category: 'electronics', productId: 'laptop-123' }

4️⃣ Catch-all λΌμš°νŒ… (...)

app/
β”œβ”€β”€ docs/
β”‚   └── [...slug]/
β”‚       └── page.js    β†’ '/docs/a'
β”‚                      β†’ '/docs/a/b'  
β”‚                      β†’ '/docs/a/b/c'
// app/docs/[...slug]/page.js
export default function DocsPage({ params }) {
  const { slug } = params  // λ°°μ—΄λ‘œ λ“€μ–΄μ˜΄!
  
  return (
    <div>
      <h1>λ¬Έμ„œ νŽ˜μ΄μ§€</h1>
      <p>경둜: {slug.join(' / ')}</p>
      <ul>
        {slug.map((segment, index) => (
          <li key={index}>Level {index + 1}: {segment}</li>
        ))}
      </ul>
    </div>
  )
}

// λ°©λ¬Έ μ˜ˆμ‹œ:
// /docs/getting-started β†’ slug = ['getting-started']
// /docs/api/users/create β†’ slug = ['api', 'users', 'create']

🎯 싀무 적용 κ°€μ΄λ“œ

동적 λΌμš°νŒ… 베슀트 ν”„λž™ν‹°μŠ€

1️⃣ SEO μΉœν™”μ  슬러그 λ§Œλ“€κΈ°

// ν•œκΈ€ 제λͺ©μ„ URL μΉœν™”μ μœΌλ‘œ λ³€ν™˜
function createSlug(title) {
  return title
    .toLowerCase()
    .replace(/[^a-z0-9κ°€-힣]/g, '-')  // 특수문자λ₯Ό λŒ€μ‹œλ‘œ
    .replace(/-+/g, '-')             // 연속 λŒ€μ‹œ 제거
    .replace(/^-|-$/g, '')           // μ•žλ’€ λŒ€μ‹œ 제거
}

// μ˜ˆμ‹œ:
// "Next.js μ™„μ „ 정볡!" β†’ "next-js-μ™„μ „-정볡"

2️⃣ νƒ€μž… μ•ˆμ „ν•œ νŒŒλΌλ―Έν„° 처리

// TypeScript μ‚¬μš©μ‹œ
interface PageProps {
  params: { slug: string }
  searchParams: { [key: string]: string | string[] | undefined }
}

export default function BlogPost({ params, searchParams }: PageProps) {
  // νŒŒλΌλ―Έν„° 검증
  if (!params.slug || typeof params.slug !== 'string') {
    notFound() // Next.js 404 νŽ˜μ΄μ§€λ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈ
  }
  
  return <div>μ•ˆμ „ν•œ νŒŒλΌλ―Έν„°: {params.slug}</div>
}

3️⃣ 메타데이터 동적 생성

// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug)
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.thumbnail]
    }
  }
}

πŸ’‘ 였늘의 핡심 정리

파일 기반 λΌμš°νŒ…

  • 파일 = URL λ§€ν•‘μœΌλ‘œ 직관적
  • μžλ™ μ½”λ“œ λΆ„ν• λ‘œ μ„±λŠ₯ μ΅œμ ν™”
  • 쀑첩 λ ˆμ΄μ•„μ›ƒμœΌλ‘œ μœ μ§€λ³΄μˆ˜μ„± ν–₯상

λΆˆλ³€μ„± 원칙

  • 객체/배열은 항상 μƒˆλ‘œ 생성
  • μŠ€ν”„λ ˆλ“œ μ—°μ‚°μž 적극 ν™œμš©
  • React의 λ¦¬λ Œλ”λ§ μ΅œμ ν™” 핡심

동적 λΌμš°νŒ… νŒ¨ν„΄

  • [param]: 단일 동적 μ„Έκ·Έλ¨ΌνŠΈ
  • [...slug]: 닀쀑 μ„Έκ·Έλ¨ΌνŠΈ (catch-all)
  • params 객체둜 νŒŒλΌλ―Έν„° μ ‘κ·Ό

πŸ”₯ μ‹€λ¬΄μ—μ„œ 정말 많이 μ“°λŠ” 것듀 (찐 κ²½ν—˜λ‹΄)
πŸ“ˆ 싀무 ν™œμš©λ„ λž­ν‚Ή

πŸ₯‡ 1μœ„: λΆˆλ³€μ„± μ—…λ°μ΄νŠΈ νŒ¨ν„΄ (99% μ‚¬μš©)

거의 맀일 μ“°λŠ” ν•„μˆ˜ 기술!
jsx// πŸ”₯ μ‹€λ¬΄μ—μ„œ 맀일 λ³΄λŠ” νŒ¨ν„΄λ“€
const [user, setUser] = useState({...})

// 폼 μ—…λ°μ΄νŠΈ
setUser({...user, name: newName})

// 배열에 μ•„μ΄ν…œ μΆ”κ°€
setItems([...items, newItem])

// λ°°μ—΄μ—μ„œ μ‚­μ œ
setItems(items.filter(item => item.id !== deleteId))

μ™œ μ€‘μš”ν•œκ°€?

λͺ¨λ“  React ν”„λ‘œμ νŠΈμ—μ„œ μƒνƒœ 관리할 λ•Œ ν•„μˆ˜
μ•ˆ μ•Œλ©΄ 버그가 λŠμž„μ—†μ΄ λ°œμƒ
μ„±λŠ₯ μ΅œμ ν™”μ˜ κΈ°λ³Έ

πŸ₯ˆ 2μœ„: 동적 λΌμš°νŒ… (80% μ‚¬μš©)

μ›¬λ§Œν•œ ν”„λ‘œμ νŠΈμ—λŠ” λ‹€ 듀어감

jsx// μ‹€μ œ ν”„λ‘œμ νŠΈ μ˜ˆμ‹œλ“€
/blog/[slug]           // λΈ”λ‘œκ·Έ, λ‰΄μŠ€ μ‚¬μ΄νŠΈ
/user/[id]/profile     // μ‚¬μš©μž μ‹œμŠ€ν…œ
/product/[category]/[id] // 이컀머슀
/admin/[...slug]       // κ΄€λ¦¬μž νŽ˜μ΄μ§€

싀무 사둀:

  • λͺ¨λ“  상세 νŽ˜μ΄μ§€ (μ œν’ˆ, κ²Œμ‹œκΈ€, ν”„λ‘œν•„)
  • RESTful API와 μ™„λ²½ λ§€μΉ­
  • SEO μΉœν™”μ  URL ꡬ쑰

πŸ₯‰ 3μœ„: 파일 기반 λΌμš°νŒ… (70% μ‚¬μš©)

Next.js μ“°λ©΄ μžλ™μœΌλ‘œ μ‚¬μš©
싀무 μž₯점:

μƒˆ νŽ˜μ΄μ§€ μΆ”κ°€ = 파일만 생성
폴더 ꡬ쑰 = URL ꡬ쑰
μ½”λ“œ 리뷰할 λ•Œ ꡬ쑰 νŒŒμ•… 쉬움

🎯 μ‹€λ¬΄μ—μ„œ μ΄λ ‡κ²Œ ν™œμš©λΌμš”
μ „μžμƒκ±°λž˜ μ‚¬μ΄νŠΈ μ˜ˆμ‹œ
πŸ“‚ 파일 ꡬ쑰 (μ‹€μ œ νšŒμ‚¬ ꡬ쑰와 λΉ„μŠ·)

app/
β”œβ”€β”€ page.js                    // 메인 νŽ˜μ΄μ§€
β”œβ”€β”€ products/
β”‚   β”œβ”€β”€ page.js               // μƒν’ˆ λͺ©λ‘
β”‚   β”œβ”€β”€ [category]/
β”‚   β”‚   β”œβ”€β”€ page.js           // μΉ΄ν…Œκ³ λ¦¬λ³„ μƒν’ˆ
β”‚   β”‚   └── [productId]/
β”‚   β”‚       └── page.js       // μƒν’ˆ 상세
β”œβ”€β”€ user/
β”‚   β”œβ”€β”€ [userId]/
β”‚   β”‚   β”œβ”€β”€ page.js           // λ§ˆμ΄νŽ˜μ΄μ§€
β”‚   β”‚   β”œβ”€β”€ orders/
β”‚   β”‚   β”‚   └── page.js       // μ£Όλ¬Έ λ‚΄μ—­
β”‚   β”‚   └── wishlist/
β”‚   β”‚       └── page.js       // 찜 λͺ©λ‘
└── admin/
    └── [...slug]/
        └── page.js            // κ΄€λ¦¬μž λͺ¨λ“  νŽ˜μ΄μ§€

πŸ›’ μ‡Όν•‘μΉ΄νŠΈ μƒνƒœ 관리 (λΆˆλ³€μ„±)

jsx// μ‹€λ¬΄μ—μ„œ 맀일 μ“°λŠ” νŒ¨ν„΄
function ShoppingCart() {
  const [cart, setCart] = useState([])
  
  // μƒν’ˆ μΆ”κ°€
  const addItem = (product) => {
    setCart(prev => {
      const existing = prev.find(item => item.id === product.id)
      if (existing) {
        return prev.map(item =>
          item.id === product.id 
            ? { ...item, quantity: item.quantity + 1 }
            : item
        )
      }
      return [...prev, { ...product, quantity: 1 }]
    })
  }
  
  // μˆ˜λŸ‰ λ³€κ²½
  const updateQuantity = (id, quantity) => {
    setCart(prev =>
      prev.map(item =>
        item.id === id ? { ...item, quantity } : item
      )
    )
  }
  
  // μƒν’ˆ μ‚­μ œ
  const removeItem = (id) => {
    setCart(prev => prev.filter(item => item.id !== id))
  }
}

πŸ’Ό 싀무 ν”„λ‘œμ νŠΈλ³„ ν™œμš©λ„

μŠ€νƒ€νŠΈμ—…/μŠ€μΌ€μΌμ—… νšŒμ‚¬
jsx// πŸ”₯ 맀우 자주 μ‚¬μš©

  • 동적 λΌμš°νŒ…: /company/[companyId]/job/[jobId]
  • λΆˆλ³€μ„±: λ³΅μž‘ν•œ 폼, μ‹€μ‹œκ°„ 데이터
  • 파일 λΌμš°νŒ…: λΉ λ₯Έ ν”„λ‘œν† νƒ€μ΄ν•‘
    λŒ€κΈ°μ—… μ„œλΉ„μŠ€
    jsx// πŸ”₯ ν•„μˆ˜ 기술둜 κ°„μ£Ό
  • 동적 λΌμš°νŒ…: /service/[category]/[subcategory]
  • λΆˆλ³€μ„±: λŒ€μš©λŸ‰ 데이터 처리
  • 파일 λΌμš°νŒ…: λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€ ꡬ쑰
    μ—μ΄μ „μ‹œ/μ™Έμ£Ό
    jsx// πŸ”₯ ν΄λΌμ΄μ–ΈνŠΈ μš”κ΅¬μ‚¬ν•­ λŒ€λΆ€λΆ„
  • 동적 λΌμš°νŒ…: λ‹€κ΅­μ–΄ /[locale]/product/[id]
  • λΆˆλ³€μ„±: CMS 연동, μ‹€μ‹œκ°„ μ—…λ°μ΄νŠΈ
  • 파일 λΌμš°νŒ…: λΉ λ₯Έ 개발, μœ μ§€λ³΄μˆ˜

🚨 μ‹€λ¬΄μ—μ„œ 자주 ν•˜λŠ” μ‹€μˆ˜λ“€

  • λΆˆλ³€μ„± μ‹€μˆ˜ (μ‹ μž…κ°œλ°œμž 90%κ°€ κ²½ν—˜)
jsx// ❌ 이런 버그 λ•Œλ¬Έμ— λ°€μƒˆλŠ” 경우 많음
const updateUser = () => {
  user.name = newName  // 화면이 μ•ˆ λ°”λ€œ!
  setUser(user)
}
// βœ… μ΄λ ‡κ²Œ ν•΄μ•Ό 함
const updateUser = () => {
  setUser({...user, name: newName})
}
동적 λΌμš°νŒ… μ‹€μˆ˜
jsx// ❌ 이런 μ‹μœΌλ‘œ ν•˜λ“œμ½”λ”©
<Link href={`/product/electronics/123`}>
// βœ… λ™μ μœΌλ‘œ 처리
<Link href={`/product/${category}/${productId}`}>

🎯 λ©΄μ ‘μ—μ„œλ„ 자주 λ¬Όμ–΄λ΄„

  • 면접관이 μ’‹μ•„ν•˜λŠ” λ‹΅λ³€
  • λ©΄μ ‘κ΄€: "Reactμ—μ„œ μƒνƒœ μ—…λ°μ΄νŠΈν•  λ•Œ μ£Όμ˜ν•  점이 μžˆλ‚˜μš”?"

쒋은 λ‹΅λ³€:
"λΆˆλ³€μ„±μ„ μ§€μΌœμ•Ό ν•©λ‹ˆλ‹€. κ°μ²΄λ‚˜ λ°°μ—΄μ˜ 경우
μŠ€ν”„λ ˆλ“œ μ—°μ‚°μžλ‚˜ map, filter 등을 μ‚¬μš©ν•΄μ„œ
μƒˆλ‘œμš΄ μ°Έμ‘°λ₯Ό λ§Œλ“€μ–΄μ•Ό Reactκ°€ λ³€ν™”λ₯Ό κ°μ§€ν•˜κ³ 
λ¦¬λ Œλ”λ§ν•  수 μžˆμŠ΅λ‹ˆλ‹€."

싀무 κ²½ν—˜ μ–΄ν•„ 포인트

"Next.js의 파일 기반 λΌμš°νŒ…μ„ μ‚¬μš©ν•΄μ„œ
SEO μΉœν™”μ μΈ 동적 λΌμš°νŒ… ꡬ쑰λ₯Ό μ„€κ³„ν–ˆκ³ ,
λΆˆλ³€μ„± 원칙을 μ§€μΌœμ„œ μƒνƒœ 관리 μ΅œμ ν™”λ₯Ό κ²½ν—˜ν–ˆμŠ΅λ‹ˆλ‹€."

πŸ’‘ κ²°λ‘ : 였늘 배운 κ²ƒμ˜ 싀무 κ°€μΉ˜
πŸ”₯ 맀일 μ“°λŠ” 것듀 (ν•„μˆ˜)

  • λΆˆλ³€μ„± μ—…λ°μ΄νŠΈ: μƒνƒœ κ΄€λ¦¬ν•˜λŠ” λͺ¨λ“  κ³³
  • 동적 λΌμš°νŒ…: 상세 νŽ˜μ΄μ§€ μžˆλŠ” λͺ¨λ“  ν”„λ‘œμ νŠΈ
  • 파일 기반 λΌμš°νŒ…: Next.js μ“°λŠ” λͺ¨λ“  ν”„λ‘œμ νŠΈ
profile
βœ¨πŸŒν™•μž₯ν•΄ λ‚˜κ°€λŠ” ν”„λ‘ νŠΈμ—”λ“œ κ°œλ°œμžμž…λ‹ˆλ‹€βœοΈ

0개의 λŒ“κΈ€