웹 개발

Next.js 홈페이지 만들기 - 기본 레이아웃 (3/3)

깨비아빠0 2023. 12. 21. 21:00
728x90
반응형

Next.js 프로젝트 생성 글에서 만든 Next.js 보일러플레이트를 개인 홈페이지로 바꾸기 위해 몇 가지 기본적인 내용을 적용했다.

 

  1. Next.js 홈페이지 만들기 - 한글 폰트 (1/3)
  2. Next.js 홈페이지 만들기 - 다크 모드 (2/3)
  3. Next.js 홈페이지 만들기 - 기본 레이아웃 (3/3)

 

 

 

웹사이트 구조

홈페이지를 포트폴리오 사이트스럽게 만들기 위해 웹사이트 상단에 제목과 네비게이션 바를 포함하는 헤더를 배치하려고 한다. 이를 위해 header.tsx 파일을 추가하였다.

 

세부 페이지는 dynamic route를 사용하지 않고, app 폴더 밑에 직접 페이지용 폴더와 page.tsx 파일을 생성하였다.

About 페이지를 위한 about 폴더 하나만 추가했는데, 추후에 Nextra, gray-matter 같은 프레임워크 혹은 CMS를 사용하게 되면 프로젝트 구조가 크게 바뀌므로, 우선은 about 하나만 만들었다.

 

현재 프로젝트 구조와 추가된 파일은 다음과 같다.

 

기존에 만들어져 있는 다음 코드들도 약간의 수정이 있을 예정이다.

 

  1. app/layout.tsx, page.tsx
  2. global.css
  3. tailwind.config.ts

 

웹사이트 헤더

 

웹사이트 상단에 배치할 Header 컴포넌트는 다음 내용을 포함한다.

 

  1. 홈페이지 이름
  2. 네비게이션 바
  3. 다크 모드 토글 버튼 (이전 다크 모드 글에서 추가한 토글 버튼을 헤더로 옮김)

위 요소들의 배치는 responsive 디자인을 위해 flexbox를 사용했고, 네비게이션 바에는 loop 방식과 active link 스타일이 쓰였다.

 

flexbox로 responsive 디자인 적용

헤더에는 아래와 같이 제목과 네비게이션 바(+ 토글 버튼)의 두 영역이 있으며, 화면 폭에 따라 양쪽으로 나뉘어 배치된다. 

 

또한, 화면 폭이 줄어들 때에는 네비게이션 바 영역은 제목 아래로 내려온다.

 

이러한 디자인은 flex, flex-wrap, justify-between과 ml-auto 클래스로 쉽게 만들 수 있다.

아래와 같이 최상단 div에 flex, flex-wrap, justify-between을 넣고, 우측 div(네비게이션 바)에 ml-auto를 추가하면 위 애니메이션과 같이 평소에는 양쪽으로 나뉘어 있다가 화면이 좁아지면 우측 영역이 아래로 내려온다.

    <div className="flex flex-wrap items-end justify-between ...">
      <h1 className="flex-shrink-0 ...">
        Portfolio&nbsp;
        <span className="...">
          under construction
        </span>
      </h1>
      <div className="ml-auto mt-4">
        ...

 

Reusing Styles (loop)

tailwind를 사용하면 class를 지정하는 코드가 길어질 수밖에 없는데, 네비게이션 링크들처럼 태그가 반복될 때에는 수정사항을 모든 태그의 className에 적용해야 하므로 작업이 더욱 귀찮아진다.

이런 문제를 해결하기 위해 tailwind에서 제안하는 방법들 중 loop 방식을 사용하여 링크 목록을 구현했다. Link 태그를 여러 개 사용하는 대신에, 아래와 같이 [title, url] 배열을 만들어서 map 함수를 사용하는 방식이다.

export function Header() {
  const navItems = [
    ['Home', '/'],
    ['Portfolio 1', '/portfolio1'],
    ['Portfolio 2', '/portfolio2'],
    ['Portfolio 3', '/portfolio3'],
    ['About', '/about'],
  ]

  return (
    ...
        <nav>
          {navItems.map(([title, url], index) => (
            <Link href={url} key={index}>{title}</Link>
          ))}
        </nav>
    ...
  )
}

 

Active Link

활성화된 페이지 링크에 스타일을 적용하려면 usePathname 함수를 사용한다.

다음은 Next.js 공식 문서의 active link 예제이다.

'use client'
 
import { usePathname } from 'next/navigation'
import Link from 'next/link'
 
export function Links() {
  const pathname = usePathname()
 
  return (
    <nav>
      <ul>
        <li>
          <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
            Home
          </Link>
        </li>
        <li>
          <Link
            className={`link ${pathname === '/about' ? 'active' : ''}`}
            href="/about"
          >
            About
          </Link>
        </li>
      </ul>
    </nav>
  )
}

 

위 예제를 참고하여 활성화된 링크일 경우의 폰트 색상과 weight를 별도로 지정했고, hover 스타일도 추가했다.

export function Header() {
  const pathname = usePathname()
  const isActive = (url: string) => pathname === url
  ...
  
  return (
    ...
            <Link
              className={
                `... hover:bg-theme-active/30
                ${isActive(url) ? 'text-theme-active font-bold' : ''}`
              }
              href={url}
              key={index}
            >{title}</Link>
    ...
  )
}

 

theme-active 컬러는 다크 모드 글  "light/dark 모드 공통 색상 class 정의"에 정리한 방식으로 추가한 커스텀 컬러이다.

// global.css
...

@layer base {
  :root {
    ...
    --theme-active: 48 112 236;
  }

  :root.dark {
    ...
    --theme-active: 55 126 255;
  }
}

...


// tailwind.config.css
import type { Config } from 'tailwindcss'

const config: Config = {
  ...
  theme: {
    extend: {
      colors: {
        ...
        'theme-active': "rgb(var(--theme-active) / <alpha-value>)",
      },
    },
  },
}
export default config

 

전체 Header 코드

완성된 Header 컴포넌트 코드는 다음과 같다. (app/header.tsx)

최상단 div의 header-layout은 global.css에 추가한 커스텀 클래스인데, 아래 "레이아웃을 위한 커스텀 클래스" 섹션에 정리
'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'
import { ThemeMode } from '@/components/theme-mode'

export function Header() {
  const pathname = usePathname()
  const isActive = (url: string) => pathname === url
  const navItems = [
    ['Home', '/'],
    ['Portfolio 1', '/portfolio1'],
    ['Portfolio 2', '/portfolio2'],
    ['Portfolio 3', '/portfolio3'],
    ['About', '/about'],
  ]

  return (
    <div className="header-layout flex flex-wrap items-end justify-between">
      <h1 className="flex-shrink-0 pr-6 text-4xl font-extrabold italic">
        Portfolio&nbsp;
        <span className="text-3xl font-medium">
          under construction
        </span>
      </h1>
      <div className="ml-auto mt-4">
        <nav className="inline text-sm font-medium">
          {navItems.map(([title, url], index) => (
            <Link
              className={
                `py-1 px-2 rounded-sm hover:bg-theme-active/30
                ${isActive(url) ? 'text-theme-active font-bold' : ''}`
              }
              href={url}
              key={index}
            >{title}</Link>
          ))}
        </nav>
        <div className="inline ml-4">
          <ThemeMode />
        </div>
      </div>
    </div>
  )
}

 

전체 레이아웃

main 영역 100vh 이슈

Header 컴포넌트는 app/layout.tsx에 간단하게 추가할 수 있지만, 헤더로 인해 기존 레이아웃에 이슈가 발생했다.

보일러플레이트의 main 태그에는 min-h-screen 클래스가 쓰이는데, min-height에 100vh(창 전체 크기)를 설정하는 클래스이다. 여기에 flexbox justify-between을 함께 사용하여 아래와 같이 하위 div 2개가 전체 창 상/하단으로 나뉘도록 하고 있다.

 

그런데, 헤더를 추가함에 따라 메인 영역 크기가 화면을 넘어가면서, 아래처럼 스크롤되게 레이아웃이 바뀌었다. 이것은 원래의 디자인 의도가 아니다.

 

이를 해결하기 위해 전체 디자인을 flexbox에 넣는 방법을 사용했다.

우선 body에 다음과 같이 flexbox 및 min-h-screen 클래스를 적용한다.

      <body className={`flex flex-col min-h-screen ${notoSansKr.className}`}>

 

그리고, main 태그에서 min-h-screen을 지우고 flex-grow를 적용하면, 아래와 같이 밑으로 넘치는 영역 없이 화면에 딱 맞게 레이아웃이 적용된다.

 

전체 레이아웃 코드

app/layout.tsx의 html 부분 코드는 다음과 같다.

app/page.tsx 전체를 감싸던 <main> 태그를 layout.tsx로 옮겨오는 변화도 있다.
...

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ko">
      <body className={`flex flex-col min-h-screen ${notoSansKr.className}`}>
        <Providers>
          <Header />
          <main className="flex-grow flex flex-col items-center">
            {children}
          </main>
        </Providers>
      </body>
    </html>
  )
}

 

레이아웃을 위한 커스텀 클래스

About 페이지 등의 세부 페이지들에 공통된 레이아웃이 적용되어야 한다. (너비, 여백 등)

그런데, 너비나 여백 스타일을 app/layout.tsx 같은 상위 레이아웃에 적용하게 되면, 세부 페이지에서 상위 프레임보다 큰 영역을 사용하고 싶을 때 음수 마진을 사용해야하는 번거로움이 있다. 따라서, 아래와 같이 세부 페이지 레이아웃을 위한 main-layout 클래스를 만들고, 각각의 페이지에서 사용하도록 하였다.

헤더 영역의 레이아웃도 함께 보기 위해 header-layout 클래스도 추가함
@layer components {
  .header-layout {
    @apply flex-shrink-0 mx-[5%] p-2
  }

  .main-layout {
    @apply max-w-5xl w-full p-4
  }
}

 

about 페이지에 다음과 같이 main-layout 클래스를 사용하면, 1024px 너비(max-w-5xl)와 16px 여백(p-4)이 적용된다.

export default function About() {
  return (
    <div className="main-layout">
      <span>About...</span>
    </div>
  )
}

 

결과

위의 내용이 모두 적용된 모습은 다음과 같다. (iframe)

 

 

반응형