ππ» μ¬κΈ°μλ μΉμ¬μ΄νΈλ₯Ό λ§λ€λ μλ¨ λ©λ΄ λΆλΆμ λ§λλλ€.
Here, we will create the top menu section for the website.
ππ» μ¬λ¬κ°μ§ νμ
μ΄ μμ§λ§ μ€λμ μν λ€λΉκ²μ΄μ
λ©λ΄λ₯Ό λ§λλλ€.(Horizontal Nav Menu)
There are various types, but today we will create a horizontal navigation menu.
ππ» λ©λ΄λ₯Ό μ»΄ν¬λνΈ νμμΌλ‘ λ§λ€μκ³ tailwindcssλ₯Ό μ¬μ©νμ΅λλ€.
I created the menu as a component and used Tailwind CSS.
ππ» λ무 μ½λκ° κΈΈμ΄μ Έμ μ¬κΈ°μλ tailwindcssλ‘λ§ μ§νν©λλ€.
Since the code has become too long, I will proceed using only Tailwind CSS here.
π λλ ν 리 ꡬ쑰 / Directory structure
tailwindcss-nextjs/
βββ app/
βββ layout.tsx
βββ page.tsx (루νΈ)
βββ globals.css
βββ favicon.ico
β
βββ ui/ β components
β βββ page.tsx
β βββ _components/
β βββ button.tsx β Button
β βββ dropdown.tsx
β βββ navigation/
β βββ HorizontalNavMenu.tsx β NavMenu
β
βββ hmenu/ β Menu test route
β βββ layout.tsx β layout
β βββ page.tsx β /hmenu
β βββ game/ -> page.tsx β /hmenu/game
β βββ music/ -> page.tsx β /hmenu/music
β βββ profile/ -> page.tsx β /hmenu/profile
β βββ help/ -> page.tsx β /hmenu/help
β
βββ button-css/ , button-tailwind/ , card-css/ ... (μμ νμ΄μ§λ€/Example pages)
βββ form-css/ , form-tailwind/ ...
π μμ΄νΌμ£Όμνμ© / Allow IP address
βοΈ next.config.tsμ μ½λλ₯Ό μλμ²λΌ μμ ν©λλ€.
Modify the code in next.config.ts as shown below.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
allowedDevOrigins: ['192.168.0.10', '192.168.0.*'],
};
export default nextConfig;
1)192.168.0.10μ μ¬λ¬λΆμ μμ΄νΌ μ£Όμλ₯Ό μ
λ ₯νλ©΄λ©λλ€.
You should enter your IP address where 192.168.0.10 is shown.
2)μ΄ λΆλΆμ μμ νμ§ μμΌλ©΄ λͺ¨λ°μΌ λΈλΌμ°μ μμ http://your ip address/hmenu μ΄λ κ² μ€ννλ©΄ μλ°μ€ν¬λ¦½νΈκ° λμνμ§ μμ΅λλ€.
If you do not modify this part, the JavaScript will not function when you access http://your ip address/hmenu from a mobile browser.
3)μμ μμ΄νΌλ₯Ό νμ©νκ³ μ μνλ©΄ μ μμ μΌλ‘ λͺ¨λ°μΌμμλ λ©λ΄λ₯Ό ν
μ€νΈ ν μ μμ΅λλ€.
Once you allow the IP address mentioned above and connect, you will be able to test the menu on mobile devices as expected.
π λ©λ΄ μ»΄ν¬λνΈ / Menu Component
βοΈ λ©λ΄λ μ»΄ν¬λνΈ ννλ‘ λ§λ€μμ΅λλ€.
The menu was created as a component.
βοΈ app/ui/_components/navigation/HoriztalNavMenu.tsx
βοΈ λ©λ΄λΌλ²¨ λ° λΌμ°νΈ
Menu Labels and Routes
const LINKS = [
{ label: 'κ²μ/Game', href: '/hmenu/game' },
{ label: 'μμ
/Music', href: '/hmenu/music' },
{ label: 'νλ‘ν/Profile', href: '/hmenu/profile' },
{ label: 'λ¬Έμ/Help', href: '/hmenu/help' },
]
1) labelμ λ©λ΄μμ 보μ¬μ€ ν
μ€νΈμ΄κ³ hrefλ μ€μ μ΄λν νμ΄μ§ λΌμ°νΈ μ
λλ€.
‘label’ is the text to be displayed in the menu, and ‘href’ is the actual page route to navigate to.
βοΈ ν λλ ν 리 λ§ν¬,PCμ© λ©λ΄
Home directory link, menu for PC
<a href="/hmenu" className="text-xl font-black">freelifemakers</a>
{/** PC */}
<nav className="hidden md:flex gap-6">
{LINKS.map(l => (
<Link
key={l.href}
href={l.href}
className={`text-sm font-medium ${pathname === l.href ? 'text-black font-bold underline' : 'text-gray-600'}`}
>
{l.label}
</Link>
))}
</nav>
1)λ©λ΄μμ freelifemakersλ₯Ό ν΄λ¦νλ©΄ νλλ ν λ¦¬λ‘ μ΄λν©λλ€.
Clicking “freelifemakers” in the menu will take you to the home directory.
2)<nav> … </nav>μ μ½λλ PCμμ 보μ¬μ§λ λ©λ΄μ
λλ€.
The code within <nav> … </nav> represents the menu displayed on PCs.
3) <nav className=”hidden md:flex gap-6″>
μ΄ μ½λλ λͺ¨λ°μΌμμ μ¨κΈ°κ³ , PC(md μ΄μ)μμλ§ λ³΄μ΄λλ‘ μ€μ ν©λλ€.
This code is set to be hidden on mobile and visible only on PC (md size and up).
4)mdλ 768px md:hidden mdκ°λ³΄λ€ νλ©΄μ΄ ν¬λ©΄ νλ©΄μ λ©λ΄λ₯Ό 보μ¬μ€λλ€.
The md breakpoint is 768px; md:hidden means the menu will be displayed on screen whenever the screen size is larger than this md value.
βοΈ λͺ¨λ°μΌμ© λ©λ΄ / Menu for mobile
const [mobileOpen, setMobileOpen] = useState(false)
... ...
<div className="flex items-center gap-3">
<button
type="button"
onPointerUp={() => setMobileOpen(v => !v)} // onClick λμ onPointerUp(λͺ¨λ°μΌμ μ΅μ ) / Use onPointerUp instead of onClick (optimized for mobile)
// mobile
className="md:hidden w-11 h-11 flex items-center justify-center rounded-full bg-gray-100 active:bg-gray-200 touch-manipulation"
>
<span className="text-xl">{mobileOpen ? 'β' : 'β°'}</span>
</button>
</div>
</div>
{mobileOpen && (
<div className="md:hidden absolute top-16 left-0 w-full bg-white border-t px-6 py-6 flex flex-col gap-2 shadow-xl">
<div className="text-xs text-green-600 bg-green-50 p-2 rounded">Menu</div>
{LINKS.map(l => (
<Link key={l.href} href={l.href} onClick={() => setMobileOpen(false)} className="py-3 font-medium">
{l.label}
</Link>
))}
</div>
)}
1)md:hidden μ md(768px)λ³΄λ€ νλ©΄μ΄ ν¬λ©΄ λ©λ΄λ₯Ό μ¨κΈ°κ³ 768pxλ³΄λ€ μμμ§λ©΄ 보μ¬μ€λλ€.(λͺ¨λ°μΌμ©)md:hidden hides the menu when the screen is larger than md (768px) and displays it when the screen is smaller than 768px (for mobile use).
2) {mobileOpen && ( … ) } μ΄ λΆλΆμ λͺ¨λ°μΌ μ©μΌλ‘ νλ²κ±° λ©λ΄λ₯Ό λλ₯΄λ©΄ λνλλ λ©λ΄μ
λλ€.
The {mobileOpen && ( ... )} section represents the menu that appears when the hamburger menu is clicked on mobile devices.
3)mobileOpenμ κ°μ true,falseλ‘ λ°μ μμΌμ λ©λ΄λ₯Ό open,closeν©λλ€.
Toggling the value of mobileOpen between true and false opens or closes the menu.
4)λ©λ΄ μ΄λ¦Ό,λ«ν λ‘μ§μ μλμ κ°μ΅λλ€.
The logic for opening and closing the menu is as follows.
1. λ³μμ€μ / Variable setting
const [mobileOpen, setMobileOpen] = useState(false)
2.λ©λ΄ μ΄λ¦Ό,λ«ν λ²νΌ / Menu open/close button
<button type="button" onPointerUp={() => setMobileOpen(v => !v)} > ... </button>
3.mobileOPenμ μνμ λ°λΌ 보μ¬μ€ λ©λ΄, λ§ν¬ ν΄λ¦νλ©΄ λ©λ΄ λ«κΈ°
Display the menu based on the state of `mobileOpen`; close the menu when a link is clicked.
{mobileOpen && ( ... ...
<Link key={l.href} href={l.href} onClick={() => setMobileOpen(false)}
className="py-3 font-medium">{l.label}
</Link>
)
π λ©λ΄ μ μ© / Apply Menu
βοΈ Layout.tsxνμΌμμ μ 체 νμ΄μ§μ λ©λ΄κ° μ μ©λ μ μλλ‘ ν©λλ€.
In the Layout.tsx file, ensure that the menu is applied to the entire page.
import { NavMenu } from '../ui/_components/navigation/HorizontalNavMenu'
export default function HmenuLayout({ children }: { children: React.ReactNode }) {
return (
<>
<NavMenu />
<main className="min-h-screen bg-gray-50">
<div className="mx-auto max-w-6xl px-6 py-20">
{children}
</div>
</main>
</>
)
}
π λ©μΈνμ΄μ§ / Main Page
βοΈ λ©λ΄λ₯Ό Layout.tsxμ μ μ©νκΈ°λλ¬Έμ μ¬κΈ°μλ λ©λ΄λ₯Ό λ°λ‘ μ μ©νμ§ μμ΅λλ€.
Since the menu has been applied in Layout.tsx, it is not applied separately here.
'use client'
import { Button } from '../ui/_components/button'
export default function MainPage() {
return (
<>
<main className="min-h-screen bg-gray-50">
<div className="mx-auto max-w-6xl px-6 py-20">
<h1 className="text-4xl font-bold">HorizontalNavMenu</h1>
<p className="text-gray-500 mt-2">κΈ°λ³Έ λ€λΉκ²μ΄μ
λ©λ΄μ
λλ€.</p>
<p className="text-gray-500 mt-2">This is the default navigation menu.</p>
<div className="flex gap-x-4 m-4">
<div>
<Button onClick={() => alert('button test')} >Button test</Button>
</div>
<div>
<Button onClick={() => console.log('Game Start')}>Game STart</Button>
</div>
</div>
</div>
</main>
</>
)
}
π μλΈνμ΄μ§ / Subpage
βοΈ λ©λ΄μ μλ²νμ΄μ§λ μ½λκ° λͺ¨λ λμΌν©λλ€.
The server pages for the menu all contain identical code.
βοΈ νμ΄μ§λ₯Ό ꡬλ³νκΈ°μν ν
μ€νΈλ§ λ€λ¦
λλ€. μλλ /game λΌμ°νΈμ νμ΄μ§ μ½λμ
λλ€.
Only the text used to distinguish the pages differs. Below is the page code for the /game route.
'use client'
export default function MainPage() {
return (
<>
<h1 className="text-4xl font-bold">κ²μ νμ΄μ§</h1>
<p className="text-gray-500 mt-2">Game Page</p>
</>
)
}
π λ²νΌ μ»΄ν¬λνΈ μμ / Modify button component
βοΈ κΈ°μ‘΄μ λ²νΌ λ‘μ§μ onClickλ±μ νλΌλ©ν°(Props)λ₯Ό λ°μ μ μμ΄μ μμ νμ΅λλ€.
I modified the existing button logic because it could not accept parameters (props) such as onClick.
'use client'
type ButtonProps = {
children: React.ReactNode
onClick?: () => void
} & React.ButtonHTMLAttributes<HTMLButtonElement>
export function Button({ children, onClick, ...props }: ButtonProps) {
return (
<button
onClick={onClick}
{...props}
className="px-4 py-2 bg-blue-600 text-white rounded-xl text-sm font-bold
active:bg-blue-700 touch-manipulation">
{children}
</button>
)
}
π ν μ€νΈ / Test
βοΈ PC
1) λ©μΈνμ΄μ§ / Main Page

2) μλΈ νμ΄μ§ / Subpage


βοΈ Mobile

