← 목록으로

챕터 24: Layout 컴포넌트로 일관성 유지하기

서론 웹사이트의 모든 페이지가 각각 다른 구조를 가진다면 사용자는 혼란스러워할 것입니다. 일관된 레이아웃은 사용자가 웹사이트를 편안하게 탐색할 수 있도록 도와줍니다. Next.js의 Layout 컴포넌트는 이러한 일관성을 유지하면서도 페이지별 커스터마이징을 가능하게 하는 강력한 도구입니다.

이번 챕터에서는 Layout 컴포넌트의 개념을 이해하고, 다양한 레이아웃 패턴을 구현하며, 중첩 레이아웃을 활용하는 방법까지 자세히 알아보겠습니다. 실제 프로젝트에서 바로 활용할 수 있는 실용적인 레이아웃 시스템을 구축해보겠습니다.

본론 기본 레이아웃 구조 이해하기 Next.js App Router에서 layout.js는 특별한 파일입니다:

jsx // app/layout.js - 루트 레이아웃 export const metadata = { title: 'My Website', description: 'Welcome to my awesome website' }

export default function RootLayout({ children }) { return ( {/* 모든 페이지에 공통으로 적용되는 요소들 */}

{children}
) }

// 헤더 컴포넌트 function Header() { return (

) }

// 푸터 컴포넌트 function Footer() { return (

© 2024 My Website. All rights reserved.

) } 다양한 레이아웃 패턴 실제 프로젝트에서 자주 사용되는 레이아웃 패턴들을 구현해봅시다:

jsx // components/layouts/DefaultLayout.jsx import Link from 'next/link'

export default function DefaultLayout({ children }) { return (

{/* 네비게이션 */}

  {/* 메인 콘텐츠 */}
  <main className="flex-1 container mx-auto px-4 py-8">
    {children}
  </main>
  
  {/* 푸터 */}
  <footer className="bg-gray-100 border-t">
    <div className="container mx-auto px-4 py-6">
      <p className="text-center text-gray-600">
        © 2024 Company Name. All rights reserved.
      </p>
    </div>
  </footer>
</div>

) }

// components/layouts/SidebarLayout.jsx export default function SidebarLayout({ children, sidebar }) { return (

{/* 사이드바 */}

  {/* 메인 영역 */}
  <div className="flex-1 flex flex-col">
    {/* 탑바 */}
    <header className="bg-white shadow-sm h-16 flex items-center px-6">
      <h1 className="text-xl font-semibold">페이지 제목</h1>
    </header>
    
    {/* 콘텐츠 */}
    <main className="flex-1 p-6 bg-gray-50">
      {children}
    </main>
  </div>
</div>

) }

function DefaultSidebar() { const menuItems = [ { icon: '📊', label: '대시보드', href: '/dashboard' }, { icon: '👥', label: '사용자', href: '/users' }, { icon: '📦', label: '제품', href: '/products' }, { icon: '📈', label: '분석', href: '/analytics' }, { icon: '⚙️', label: '설정', href: '/settings' } ]

return (

) }

// components/layouts/GridLayout.jsx export default function GridLayout({ children, columns = 3 }) { return ( <div className={grid grid-cols-1 md:grid-cols-${columns} gap-6}> {children}

) } 중첩 레이아웃 활용하기 Next.js는 레이아웃을 중첩하여 사용할 수 있습니다:

jsx // app/(marketing)/layout.js - 마케팅 섹션 레이아웃 export default function MarketingLayout({ children }) { return (

{/* 마케팅 페이지 전용 네비게이션 */}

  {/* CTA 배너 */}
  <div className="bg-blue-50 border-b">
    <div className="container mx-auto px-4 py-3 text-center">
      <p className="text-blue-800">
        🎉 특별 할인 진행중! 지금 가입하면 50% 할인
      </p>
    </div>
  </div>
  
  {children}
  
  {/* 마케팅 푸터 */}
  <footer className="bg-gray-900 text-white py-12">
    <div className="container mx-auto px-4">
      <div className="grid grid-cols-4 gap-8">
        <div>
          <h3 className="font-bold mb-4">제품</h3>
          <ul className="space-y-2 text-gray-400">
            <li>기능</li>
            <li>가격</li>
            <li>통합</li>
          </ul>
        </div>
        <div>
          <h3 className="font-bold mb-4">회사</h3>
          <ul className="space-y-2 text-gray-400">
            <li>소개</li>
            <li>블로그</li>
            <li>채용</li>
          </ul>
        </div>
        <div>
          <h3 className="font-bold mb-4">지원</h3>
          <ul className="space-y-2 text-gray-400">
            <li>도움말</li>
            <li>문의</li>
            <li>상태</li>
          </ul>
        </div>
        <div>
          <h3 className="font-bold mb-4">법적고지</h3>
          <ul className="space-y-2 text-gray-400">
            <li>개인정보처리방침</li>
            <li>이용약관</li>
            <li>쿠키정책</li>
          </ul>
        </div>
      </div>
    </div>
  </footer>
</div>

) }

// app/(admin)/layout.js - 관리자 섹션 레이아웃 'use client'

import { useState } from 'react' import Link from 'next/link'

export default function AdminLayout({ children }) { const [sidebarOpen, setSidebarOpen] = useState(true)

return (

{/* 관리자 헤더 */}
<button onClick={() => setSidebarOpen(!sidebarOpen)} className="p-2 hover:bg-gray-800 rounded" > ☰ Admin Panel
관리자님

  <div className="flex pt-16">
    {/* 사이드바 */}
    <aside className={`bg-gray-800 text-white w-64 min-h-screen transition-all duration-300 ${
      sidebarOpen ? 'translate-x-0' : '-translate-x-full'
    } fixed lg:relative lg:translate-x-0`}>
      <nav className="p-4">
        <Link href="/admin" className="block py-2 px-4 hover:bg-gray-700 rounded">
          대시보드
        </Link>
        <Link href="/admin/users" className="block py-2 px-4 hover:bg-gray-700 rounded">
          사용자 관리
        </Link>
        <Link href="/admin/products" className="block py-2 px-4 hover:bg-gray-700 rounded">
          상품 관리
        </Link>
        <Link href="/admin/orders" className="block py-2 px-4 hover:bg-gray-700 rounded">
          주문 관리
        </Link>
        <Link href="/admin/settings" className="block py-2 px-4 hover:bg-gray-700 rounded">
          설정
        </Link>
      </nav>
    </aside>
    
    {/* 메인 콘텐츠 */}
    <main className="flex-1 p-6">
      {children}
    </main>
  </div>
</div>

) } 동적 레이아웃 전환 조건에 따라 다른 레이아웃을 사용하는 방법입니다:

jsx 'use client'

import { useState, useEffect } from 'react' import DefaultLayout from './DefaultLayout' import CompactLayout from './CompactLayout' import WideLayout from './WideLayout'

export default function DynamicLayout({ children, layoutType = 'default' }) { const [currentLayout, setCurrentLayout] = useState(layoutType) const [isMobile, setIsMobile] = useState(false)

useEffect(() => { const checkMobile = () => { setIsMobile(window.innerWidth < 768) }

checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)

}, [])

// 모바일에서는 항상 CompactLayout 사용 const activeLayout = isMobile ? 'compact' : currentLayout

const layouts = { default: {children}, compact: {children}, wide: {children} }

return (

{/* 레이아웃 선택기 */}
<button onClick={() => setCurrentLayout('default')} className={px-3 py-1 rounded ${ currentLayout === 'default' ? 'bg-blue-600 text-white' : 'bg-gray-200' }} > 기본 <button onClick={() => setCurrentLayout('compact')} className={px-3 py-1 rounded ${ currentLayout === 'compact' ? 'bg-blue-600 text-white' : 'bg-gray-200' }} > 컴팩트 <button onClick={() => setCurrentLayout('wide')} className={px-3 py-1 rounded ${ currentLayout === 'wide' ? 'bg-blue-600 text-white' : 'bg-gray-200' }} > 와이드

  {layouts[activeLayout]}
</div>

) } 실습: 완성도 높은 레이아웃 시스템 모든 기능을 통합한 레이아웃 시스템을 구축해봅시다:

jsx // components/layouts/MasterLayout.jsx 'use client'

import { createContext, useContext, useState } from 'react' import Link from 'next/link' import { usePathname } from 'next/navigation'

// 레이아웃 컨텍스트 const LayoutContext = createContext()

export function useLayout() { return useContext(LayoutContext) }

export default function MasterLayout({ children, config = {} }) { const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [theme, setTheme] = useState('light') const pathname = usePathname()

const defaultConfig = { showHeader: true, showSidebar: false, showFooter: true, maxWidth: 'container', padding: 'normal', ...config }

const value = { sidebarCollapsed, setSidebarCollapsed, theme, setTheme, config: defaultConfig }

return ( <LayoutContext.Provider value={value}> <div className={min-h-screen ${theme === 'dark' ? 'dark bg-gray-900' : 'bg-gray-50'}}> {defaultConfig.showHeader &&

}

    <div className="flex">
      {defaultConfig.showSidebar && <Sidebar />}
      
      <main className={`flex-1 ${
        defaultConfig.maxWidth === 'container' ? 'container mx-auto' : 'w-full'
      } ${
        defaultConfig.padding === 'normal' ? 'p-6' : defaultConfig.padding === 'compact' ? 'p-3' : 'p-0'
      }`}>
        {/* 브레드크럼 */}
        <Breadcrumbs />
        
        {/* 페이지 콘텐츠 */}
        <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
          {children}
        </div>
      </main>
    </div>
    
    {defaultConfig.showFooter && <Footer />}
  </div>
</LayoutContext.Provider>

) }

function Header() { const { theme, setTheme } = useLayout()

return (

MyApp

    <nav className="flex items-center space-x-6">
      <Link href="/dashboard" className="text-gray-600 dark:text-gray-300 hover:text-blue-600">
        대시보드
      </Link>
      <Link href="/projects" className="text-gray-600 dark:text-gray-300 hover:text-blue-600">
        프로젝트
      </Link>
      <Link href="/team" className="text-gray-600 dark:text-gray-300 hover:text-blue-600">
        팀
      </Link>
      
      {/* 테마 토글 */}
      <button
        onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
        className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
      >
        {theme === 'light' ? '🌙' : '☀️'}
      </button>
    </nav>
  </div>
</header>

) }

function Sidebar() { const { sidebarCollapsed, setSidebarCollapsed } = useLayout() const pathname = usePathname()

const menuItems = [ { icon: '🏠', label: '홈', href: '/' }, { icon: '📊', label: '대시보드', href: '/dashboard' }, { icon: '📁', label: '프로젝트', href: '/projects' }, { icon: '👥', label: '팀', href: '/team' }, { icon: '⚙️', label: '설정', href: '/settings' } ]

return ( <aside className={bg-gray-900 text-white transition-all duration-300 ${ sidebarCollapsed ? 'w-20' : 'w-64' }}>

<button onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="w-full text-left mb-6" > {sidebarCollapsed ? '→' : '←'}

    <nav className="space-y-2">
      {menuItems.map(item => (
        <Link
          key={item.href}
          href={item.href}
          className={`flex items-center space-x-3 px-3 py-2 rounded transition ${
            pathname === item.href
              ? 'bg-blue-600'
              : 'hover:bg-gray-800'
          }`}
        >
          <span className="text-xl">{item.icon}</span>
          {!sidebarCollapsed && <span>{item.label}</span>}
        </Link>
      ))}
    </nav>
  </div>
</aside>

) }

function Breadcrumbs() { const pathname = usePathname() const paths = pathname.split('/').filter(Boolean)

if (paths.length === 0) return null

return (

) }

function Footer() { return (

© 2024 MyApp. All rights reserved.

) } 결론 Layout 컴포넌트는 웹 애플리케이션의 일관성과 유지보수성을 크게 향상시킵니다. 이번 챕터에서 우리는 기본 레이아웃 구조부터 중첩 레이아웃, 동적 레이아웃 전환, 그리고 완성도 높은 레이아웃 시스템까지 구현해보았습니다.

잘 설계된 레이아웃 시스템은 개발 생산성을 높이고, 사용자 경험을 일관되게 유지하며, 새로운 기능을 추가할 때도 쉽게 확장할 수 있습니다. Next.js의 파일 기반 레이아웃 시스템을 활용하면 이 모든 것을 효율적으로 구현할 수 있습니다.

다음 챕터에서는 이미지 최적화와 처리에 대해 알아보겠습니다. Next.js의 Image 컴포넌트를 활용하여 성능을 최적화하는 방법을 배워보겠습니다!