← 목록으로

챕터 18: 라우팅의 개념 이해하기

서론

웹사이트는 단일 페이지가 아닌 여러 페이지로 구성됩니다. 홈페이지, 소개 페이지, 상품 페이지, 연락처 페이지 등 각각의 페이지는 고유한 주소(URL)를 가지고 있습니다. 사용자가 특정 URL을 입력하거나 링크를 클릭했을 때 해당하는 페이지를 보여주는 것, 이것이 바로 라우팅(Routing)입니다.

Next.js는 파일 시스템 기반의 직관적인 라우팅을 제공합니다. 폴더와 파일을 만드는 것만으로도 자동으로 라우트가 생성되는 마법 같은 시스템입니다. 이번 챕터에서는 라우팅의 기본 개념부터 Next.js의 App Router 시스템까지 차근차근 알아보겠습니다.

본론

라우팅이란 무엇인가

라우팅은 웹 애플리케이션에서 URL과 페이지를 연결하는 메커니즘입니다:

'use client'

import { useState } from 'react'

export default function RoutingConcept() {
  const [currentPath, setCurrentPath] = useState('/')
  
  const pages = {
    '/': { title: '홈', content: '홈페이지 콘텐츠입니다' },
    '/about': { title: '소개', content: '회사 소개 페이지입니다' },
    '/products': { title: '제품', content: '제품 목록 페이지입니다' },
    '/contact': { title: '연락처', content: '연락처 페이지입니다' }
  }
  
  return (
    <div className="p-8 max-w-4xl mx-auto">
      <h2 className="text-2xl font-bold mb-6">라우팅 개념 이해하기</h2>
      
      {/* URL 바 시뮬레이션 */}
      <div className="mb-8">
        <div className="bg-gray-100 rounded-lg p-4">
          <div className="flex items-center gap-2">
            <div className="bg-white rounded px-4 py-2 flex-1 font-mono text-sm">
              https://mywebsite.com{currentPath}
            </div>
            <button className="bg-blue-500 text-white px-4 py-2 rounded">
              이동
            </button>
          </div>
        </div>
      </div>
      
      {/* 네비게이션 */}
      <nav className="mb-8">
        <div className="flex gap-2">
          {Object.keys(pages).map(path => (
            <button
              key={path}
              onClick={() => setCurrentPath(path)}
              className={`px-4 py-2 rounded transition-colors ${
                currentPath === path
                  ? 'bg-blue-500 text-white'
                  : 'bg-gray-200 hover:bg-gray-300'
              }`}
            >
              {pages[path].title}
            </button>
          ))}
        </div>
      </nav>
      
      {/* 페이지 콘텐츠 */}
      <div className="bg-white border rounded-lg p-8">
        <h1 className="text-3xl font-bold mb-4">
          {pages[currentPath].title}
        </h1>
        <p className="text-gray-600">
          {pages[currentPath].content}
        </p>
      </div>
      
      {/* 라우팅 설명 */}
      <div className="mt-8 bg-blue-50 rounded-lg p-6">
        <h3 className="font-bold text-blue-900 mb-2">라우팅이란?</h3>
        <ul className="space-y-2 text-blue-800">
          <li>• URL 경로와 페이지 컴포넌트를 연결하는 시스템</li>
          <li>• 사용자가 다른 페이지로 이동할 수 있게 해주는 메커니즘</li>
          <li>• 브라우저의 뒤로가기/앞으로가기 버튼과 연동</li>
          <li>• SEO와 사용자 경험에 중요한 역할</li>
        </ul>
      </div>
    </div>
  )
}

Next.js의 파일 기반 라우팅

Next.js App Router는 폴더 구조가 곧 URL 구조가 됩니다:

'use client'

export default function FileBasedRouting() {
  const fileStructure = [
    { path: 'app/', type: 'folder', indent: 0 },
    { path: 'page.js', type: 'file', indent: 1, url: '/' },
    { path: 'layout.js', type: 'file', indent: 1, url: null },
    { path: 'about/', type: 'folder', indent: 1 },
    { path: 'page.js', type: 'file', indent: 2, url: '/about' },
    { path: 'products/', type: 'folder', indent: 1 },
    { path: 'page.js', type: 'file', indent: 2, url: '/products' },
    { path: '[id]/', type: 'folder', indent: 2 },
    { path: 'page.js', type: 'file', indent: 3, url: '/products/[id]' },
    { path: 'blog/', type: 'folder', indent: 1 },
    { path: 'page.js', type: 'file', indent: 2, url: '/blog' },
    { path: '[slug]/', type: 'folder', indent: 2 },
    { path: 'page.js', type: 'file', indent: 3, url: '/blog/[slug]' }
  ]
  
  return (
    <div className="p-8 max-w-5xl mx-auto">
      <h2 className="text-2xl font-bold mb-6">Next.js 파일 기반 라우팅</h2>
      
      <div className="grid grid-cols-2 gap-8">
        {/* 파일 구조 */}
        <div>
          <h3 className="font-bold mb-4">📁 폴더 구조</h3>
          <div className="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm">
            {fileStructure.map((item, index) => (
              <div key={index} className="py-1">
                <span className="text-gray-600">
                  {'  '.repeat(item.indent)}
                </span>
                {item.type === 'folder' ? '📁' : '📄'}
                {' '}
                <span className={item.type === 'folder' ? 'text-yellow-400' : 'text-green-400'}>
                  {item.path}
                </span>
              </div>
            ))}
          </div>
        </div>
        
        {/* URL 매핑 */}
        <div>
          <h3 className="font-bold mb-4">🔗 생성되는 URL</h3>
          <div className="space-y-2">
            {fileStructure
              .filter(item => item.url)
              .map((item, index) => (
                <div key={index} className="bg-white border rounded-lg p-3">
                  <div className="flex items-center justify-between">
                    <span className="text-sm text-gray-600">
                      {item.path}
                    </span>
                    <span className="text-blue-600 font-medium">
                      {item.url}
                    </span>
                  </div>
                </div>
              ))}
          </div>
        </div>
      </div>
      
      {/* 규칙 설명 */}
      <div className="mt-8 grid grid-cols-2 gap-4">
        <div className="bg-blue-50 rounded-lg p-4">
          <h4 className="font-bold text-blue-900 mb-2">📝 page.js</h4>
          <p className="text-sm text-blue-800">
            각 라우트의 UI를 정의합니다. 이 파일이 있어야 해당 경로에 접근 가능합니다.
          </p>
        </div>
        
        <div className="bg-green-50 rounded-lg p-4">
          <h4 className="font-bold text-green-900 mb-2">🎨 layout.js</h4>
          <p className="text-sm text-green-800">
            여러 페이지가 공유하는 UI를 정의합니다. 중첩 가능합니다.
          </p>
        </div>
        
        <div className="bg-purple-50 rounded-lg p-4">
          <h4 className="font-bold text-purple-900 mb-2">[ ] 동적 세그먼트</h4>
          <p className="text-sm text-purple-800">
            대괄호로 감싸면 동적 라우트가 됩니다. 예: [id], [slug]
          </p>
        </div>
        
        <div className="bg-yellow-50 rounded-lg p-4">
          <h4 className="font-bold text-yellow-900 mb-2">(...) 그룹</h4>
          <p className="text-sm text-yellow-800">
            괄호로 감싸면 URL에 영향을 주지 않는 폴더 그룹을 만들 수 있습니다.
          </p>
        </div>
      </div>
    </div>
  )
}

라우트 구조 예제

실제 프로젝트에서 자주 사용되는 라우트 구조:

'use client'

import { useState } from 'react'

export default function RouteStructureExample() {
  const [selectedRoute, setSelectedRoute] = useState(null)
  
  const routes = [
    {
      path: '/',
      name: '홈',
      description: '메인 페이지',
      code: `// app/page.js
export default function HomePage() {
  return <h1>홈페이지</h1>
}`
    },
    {
      path: '/about',
      name: '소개',
      description: '정적 페이지',
      code: `// app/about/page.js
export default function AboutPage() {
  return <h1>소개 페이지</h1>
}`
    },
    {
      path: '/products',
      name: '상품 목록',
      description: '상품 리스트 페이지',
      code: `// app/products/page.js
export default function ProductsPage() {
  return (
    <div>
      <h1>상품 목록</h1>
      <ul>
        <li>상품 1</li>
        <li>상품 2</li>
      </ul>
    </div>
  )
}`
    },
    {
      path: '/products/[id]',
      name: '상품 상세',
      description: '동적 라우트 (상품 ID)',
      code: `// app/products/[id]/page.js
export default function ProductPage({ params }) {
  return <h1>상품 ID: {params.id}</h1>
}`
    },
    {
      path: '/blog/[...slug]',
      name: '블로그 포스트',
      description: 'Catch-all 라우트',
      code: `// app/blog/[...slug]/page.js
export default function BlogPost({ params }) {
  // params.slug는 배열
  // /blog/2024/01/my-post → ['2024', '01', 'my-post']
  return <h1>블로그: {params.slug.join('/')}</h1>
}`
    }
  ]
  
  return (
    <div className="p-8 max-w-6xl mx-auto">
      <h2 className="text-2xl font-bold mb-6">라우트 구조 예제</h2>
      
      <div className="grid grid-cols-3 gap-6">
        {/* 라우트 목록 */}
        <div className="col-span-1">
          <h3 className="font-bold mb-3">라우트 목록</h3>
          <div className="space-y-2">
            {routes.map((route, index) => (
              <button
                key={index}
                onClick={() => setSelectedRoute(route)}
                className={`w-full text-left p-3 rounded-lg transition-all ${
                  selectedRoute?.path === route.path
                    ? 'bg-blue-500 text-white'
                    : 'bg-gray-100 hover:bg-gray-200'
                }`}
              >
                <div className="font-medium">{route.name}</div>
                <div className={`text-sm ${
                  selectedRoute?.path === route.path ? 'text-blue-100' : 'text-gray-600'
                }`}>
                  {route.path}
                </div>
              </button>
            ))}
          </div>
        </div>
        
        {/* 상세 정보 */}
        <div className="col-span-2">
          {selectedRoute ? (
            <div>
              <div className="bg-white border rounded-lg p-6 mb-4">
                <h3 className="text-xl font-bold mb-2">{selectedRoute.name}</h3>
                <p className="text-gray-600 mb-4">{selectedRoute.description}</p>
                
                <div className="bg-gray-100 rounded p-3 mb-4">
                  <span className="text-sm font-medium text-gray-700">URL 패턴: </span>
                  <code className="text-blue-600">{selectedRoute.path}</code>
                </div>
                
                <div>
                  <h4 className="font-medium mb-2">코드 예제:</h4>
                  <pre className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
                    <code>{selectedRoute.code}</code>
                  </pre>
                </div>
              </div>
              
              {/* URL 예시 */}
              {selectedRoute.path.includes('[') && (
                <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
                  <h4 className="font-medium text-yellow-900 mb-2">URL 예시:</h4>
                  <ul className="space-y-1 text-sm text-yellow-800">
                    {selectedRoute.path === '/products/[id]' && (
                      <>
                        <li>• /products/1</li>
                        <li>• /products/abc123</li>
                        <li>• /products/laptop-dell-xps</li>
                      </>
                    )}
                    {selectedRoute.path === '/blog/[...slug]' && (
                      <>
                        <li>• /blog/hello-world</li>
                        <li>• /blog/2024/01/15/my-post</li>
                        <li>• /blog/category/tech/article</li>
                      </>
                    )}
                  </ul>
                </div>
              )}
            </div>
          ) : (
            <div className="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
              라우트를 선택하면 상세 정보가 표시됩니다
            </div>
          )}
        </div>
      </div>
    </div>
  )
}

라우트 그룹과 레이아웃

라우트를 논리적으로 그룹화하고 레이아웃을 적용하는 방법:

'use client'

import { useState } from 'react'

export default function RouteGroupsAndLayouts() {
  const [activeSection, setActiveSection] = useState('marketing')
  
  return (
    <div className="p-8 max-w-6xl mx-auto">
      <h2 className="text-2xl font-bold mb-6">라우트 그룹과 레이아웃</h2>
      
      {/* 라우트 그룹 설명 */}
      <div className="mb-8">
        <h3 className="font-bold mb-4">라우트 그룹 (Route Groups)</h3>
        <div className="grid grid-cols-2 gap-6">
          <div className="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm">
            <div className="text-yellow-400 mb-2">app/</div>
            <div className="ml-4">
              <div className="text-blue-400">(marketing)/</div>
              <div className="ml-4">
                <div>page.js</div>
                <div>about/page.js</div>
                <div>blog/page.js</div>
              </div>
              <div className="text-blue-400 mt-2">(shop)/</div>
              <div className="ml-4">
                <div>page.js</div>
                <div>products/page.js</div>
                <div>cart/page.js</div>
              </div>
              <div className="text-blue-400 mt-2">(admin)/</div>
              <div className="ml-4">
                <div>dashboard/page.js</div>
                <div>users/page.js</div>
              </div>
            </div>
          </div>
          
          <div>
            <h4 className="font-medium mb-3">장점:</h4>
            <ul className="space-y-2">
              <li className="flex items-start">
                <span className="text-green-500 mr-2">✓</span>
                <span>URL 구조에 영향 없이 파일 구조 정리</span>
              </li>
              <li className="flex items-start">
                <span className="text-green-500 mr-2">✓</span>
                <span>그룹별로 다른 레이아웃 적용 가능</span>
              </li>
              <li className="flex items-start">
                <span className="text-green-500 mr-2">✓</span>
                <span>코드 구성과 유지보수 용이</span>
              </li>
            </ul>
            
            <div className="mt-4 p-3 bg-blue-50 rounded">
              <p className="text-sm text-blue-800">
                <strong>참고:</strong> 괄호로 감싼 폴더명은 URL에 포함되지 않습니다.
                (marketing)/about → /about
              </p>
            </div>
          </div>
        </div>
      </div>
      
      {/* 레이아웃 중첩 */}
      <div>
        <h3 className="font-bold mb-4">레이아웃 중첩 (Nested Layouts)</h3>
        
        {/* 탭 네비게이션 */}
        <div className="flex gap-2 mb-4">
          {['marketing', 'shop', 'admin'].map(section => (
            <button
              key={section}
              onClick={() => setActiveSection(section)}
              className={`px-4 py-2 rounded-lg transition-colors ${
                activeSection === section
                  ? 'bg-blue-500 text-white'
                  : 'bg-gray-200 hover:bg-gray-300'
              }`}
            >
              {section === 'marketing' ? '마케팅 사이트' :
               section === 'shop' ? '쇼핑몰' : '관리자'}
            </button>
          ))}
        </div>
        
        {/* 레이아웃 미리보기 */}
        <div className="border rounded-lg overflow-hidden">
          {/* Root Layout */}
          <div className="bg-gray-800 text-white p-2 text-sm">
            Root Layout (모든 페이지 공통)
          </div>
          
          {/* Group Layout */}
          {activeSection === 'marketing' && (
            <div className="bg-blue-100 p-4">
              <div className="text-sm font-medium text-blue-800 mb-2">
                Marketing Layout
              </div>
              <nav className="flex gap-4 mb-4">
                <span className="text-blue-600">홈</span>
                <span className="text-blue-600">소개</span>
                <span className="text-blue-600">블로그</span>
              </nav>
              <div className="bg-white rounded p-6">
                <h1 className="text-xl font-bold mb-2">마케팅 페이지 콘텐츠</h1>
                <p className="text-gray-600">깔끔한 디자인과 CTA 중심</p>
              </div>
            </div>
          )}
          
          {activeSection === 'shop' && (
            <div className="bg-green-100 p-4">
              <div className="text-sm font-medium text-green-800 mb-2">
                Shop Layout
              </div>
              <div className="flex gap-4 mb-4">
                <nav className="flex gap-4">
                  <span className="text-green-600">상품</span>
                  <span className="text-green-600">카테고리</span>
                </nav>
                <div className="ml-auto">
                  <span className="bg-green-600 text-white px-3 py-1 rounded">
                    장바구니 (3)
                  </span>
                </div>
              </div>
              <div className="bg-white rounded p-6">
                <h1 className="text-xl font-bold mb-2">쇼핑몰 페이지</h1>
                <p className="text-gray-600">상품 중심의 레이아웃</p>
              </div>
            </div>
          )}
          
          {activeSection === 'admin' && (
            <div className="bg-red-100 p-4">
              <div className="text-sm font-medium text-red-800 mb-2">
                Admin Layout
              </div>
              <div className="flex">
                <aside className="bg-red-200 p-3 rounded mr-4">
                  <div className="space-y-2 text-sm">
                    <div>대시보드</div>
                    <div>사용자 관리</div>
                    <div>설정</div>
                  </div>
                </aside>
                <div className="bg-white rounded p-6 flex-1">
                  <h1 className="text-xl font-bold mb-2">관리자 페이지</h1>
                  <p className="text-gray-600">사이드바가 있는 관리자 레이아웃</p>
                </div>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  )
}

실습: 간단한 라우팅 시스템 구현

라우팅의 동작 원리를 이해하기 위한 미니 라우터:

'use client'

import { useState } from 'react'

export default function MiniRouter() {
  const [currentPath, setCurrentPath] = useState('/')
  const [history, setHistory] = useState(['/'])
  const [historyIndex, setHistoryIndex] = useState(0)
  
  // 페이지 컴포넌트들
  const pages = {
    '/': {
      title: '홈',
      component: () => (
        <div>
          <h1 className="text-3xl font-bold mb-4">홈페이지</h1>
          <p className="mb-4">웹사이트에 오신 것을 환영합니다!</p>
          <div className="space-x-2">
            <button 
              onClick={() => navigate('/about')}
              className="bg-blue-500 text-white px-4 py-2 rounded"
            >
              소개 페이지로
            </button>
            <button 
              onClick={() => navigate('/products')}
              className="bg-green-500 text-white px-4 py-2 rounded"
            >
              상품 페이지로
            </button>
          </div>
        </div>
      )
    },
    '/about': {
      title: '소개',
      component: () => (
        <div>
          <h1 className="text-3xl font-bold mb-4">회사 소개</h1>
          <p className="mb-4">우리는 최고의 서비스를 제공합니다.</p>
          <button 
            onClick={() => navigate('/contact')}
            className="bg-purple-500 text-white px-4 py-2 rounded"
          >
            연락처 보기
          </button>
        </div>
      )
    },
    '/products': {
      title: '상품',
      component: () => (
        <div>
          <h1 className="text-3xl font-bold mb-4">상품 목록</h1>
          <div className="grid grid-cols-2 gap-4">
            {['상품 A', '상품 B', '상품 C', '상품 D'].map(product => (
              <div key={product} className="border p-4 rounded">
                <h3 className="font-bold">{product}</h3>
                <button 
                  onClick={() => navigate(`/products/${product.toLowerCase().replace(' ', '-')}`)}
                  className="text-blue-500 mt-2"
                >
                  자세히 보기 →
                </button>
              </div>
            ))}
          </div>
        </div>
      )
    },
    '/contact': {
      title: '연락처',
      component: () => (
        <div>
          <h1 className="text-3xl font-bold mb-4">연락처</h1>
          <p>이메일: contact@example.com</p>
          <p>전화: 02-1234-5678</p>
        </div>
      )
    }
  }
  
  // 동적 라우트 처리
  const getDynamicPage = (path) => {
    if (path.startsWith('/products/')) {
      const productId = path.replace('/products/', '')
      return {
        title: `상품: ${productId}`,
        component: () => (
          <div>
            <h1 className="text-3xl font-bold mb-4">상품 상세</h1>
            <p className="mb-4">상품 ID: {productId}</p>
            <button 
              onClick={() => navigate('/products')}
              className="bg-gray-500 text-white px-4 py-2 rounded"
            >
              ← 목록으로
            </button>
          </div>
        )
      }
    }
    return null
  }
  
  // 네비게이션 함수
  const navigate = (path) => {
    const newHistory = history.slice(0, historyIndex + 1)
    newHistory.push(path)
    setHistory(newHistory)
    setHistoryIndex(newHistory.length - 1)
    setCurrentPath(path)
  }
  
  const goBack = () => {
    if (historyIndex > 0) {
      setHistoryIndex(historyIndex - 1)
      setCurrentPath(history[historyIndex - 1])
    }
  }
  
  const goForward = () => {
    if (historyIndex < history.length - 1) {
      setHistoryIndex(historyIndex + 1)
      setCurrentPath(history[historyIndex + 1])
    }
  }
  
  // 현재 페이지 가져오기
  const currentPage = pages[currentPath] || getDynamicPage(currentPath) || pages['/']
  
  return (
    <div className="p-8 max-w-4xl mx-auto">
      <h2 className="text-2xl font-bold mb-6">미니 라우터 실습</h2>
      
      {/* 브라우저 UI */}
      <div className="bg-gray-100 rounded-lg p-4 mb-6">
        <div className="flex items-center gap-2 mb-3">
          <button 
            onClick={goBack}
            disabled={historyIndex === 0}
            className="p-2 rounded hover:bg-gray-200 disabled:opacity-50"
          >
            ←
          </button>
          <button 
            onClick={goForward}
            disabled={historyIndex === history.length - 1}
            className="p-2 rounded hover:bg-gray-200 disabled:opacity-50"
          >
            →
          </button>
          <div className="flex-1 bg-white rounded px-3 py-2 font-mono text-sm">
            localhost:3000{currentPath}
          </div>
        </div>
        
        {/* 네비게이션 바 */}
        <nav className="flex gap-2">
          {Object.keys(pages).map(path => (
            <button
              key={path}
              onClick={() => navigate(path)}
              className={`px-3 py-1 rounded transition-colors ${
                currentPath === path
                  ? 'bg-blue-500 text-white'
                  : 'bg-white hover:bg-gray-50'
              }`}
            >
              {pages[path].title}
            </button>
          ))}
        </nav>
      </div>
      
      {/* 페이지 콘텐츠 */}
      <div className="bg-white border rounded-lg p-8">
        {currentPage.component()}
      </div>
      
      {/* 히스토리 */}
      <div className="mt-6 bg-gray-50 rounded-lg p-4">
        <h3 className="font-bold mb-2">브라우저 히스토리</h3>
        <div className="flex gap-2 text-sm">
          {history.map((path, index) => (
            <div
              key={index}
              className={`px-2 py-1 rounded ${
                index === historyIndex
                  ? 'bg-blue-500 text-white'
                  : 'bg-white text-gray-600'
              }`}
            >
              {path}
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

결론

라우팅은 웹 애플리케이션의 핵심 기능입니다. 이번 챕터에서 우리는 라우팅의 기본 개념, Next.js의 파일 기반 라우팅 시스템, 라우트 그룹과 레이아웃, 그리고 미니 라우터 구현을 통해 라우팅의 동작 원리를 깊이 있게 이해했습니다.

Next.js App Router의 핵심 개념:

  1. 파일 시스템 기반: 폴더 구조가 URL 구조를 결정
  2. page.js: 각 라우트의 UI를 정의
  3. layout.js: 공통 UI와 상태 유지
  4. 동적 라우트: [param] 형식으로 동적 경로 생성
  5. 라우트 그룹: (group) 형식으로 논리적 구조화

다음 챕터에서는 정적 라우팅을 실제로 구현하면서 페이지 간 이동과 네비게이션을 만드는 방법을 배워보겠습니다!