Dashboard의 Layout에 해당하는 Header 컴포넌트를 만들어야 한다. 

이 Header에는 다른 페이지로 이동 할 수 있는 Navigation의 기능도 포함한다.

import { Header } from "@/components/ui/header";

type Props = {
  children: React.ReactNode;
};

const DashboardLayout = ({ children }: Props) => {
  return (
    <>
      <Header />
      <main className="px-3 lg:px-14">{children}</main>
    </>
  );
};
export default DashboardLayout;

 

Header.tsx

import { ClerkLoaded, ClerkLoading, UserButton } from "@clerk/nextjs";
import { HeaderLogo } from "./header-logo";
import { Navigation } from "./navigation";
import { Loader2 } from "lucide-react";
import { WelcomeMsg } from "./welcome-msg";

export const Header = () => {
  return (
    <header className="bg-gradient-to-b from-blue-700 to-blue-500 px-4 py-8 lg:px-14 pb-36">
      <div className="max-w-screen-2xl mx-auto">
        <div className="w-full flex itmes-center justify-between mv-14">
          <div className="flex items-center lg:gap-x-16">
            <HeaderLogo />
            <Navigation />
          </div>
          <ClerkLoaded>
            <UserButton afterSignOutUrl="/" />
          </ClerkLoaded>
          <ClerkLoading>
            <Loader2 className="size-8 animate-spin text-slate-400" />
          </ClerkLoading>
        </div>
        <WelcomeMsg />
      </div>
    </header>
  );
};

Header의 전체 형태를 보면 

HeaderLogo와 Navigation이 묶여있는 하나의 div와 Clerk auth컴포넌트가  justify-between으로 나란히 배열되어 있다. 

그리고 그 아래에 WecomeMessage가 나열되어 있다. 

HeaderLogo.tsx

import Link from "next/link";
import Image from "next/image";
export const HeaderLogo = () => {
  return (
    <Link href="/">
      <div className="items-center hidden lg:flex">
        <Image src="/logo.svg" alt="Logo" height={28} width={28} />
        <p className="font-semibold text-white text-2xl ml-2.5">Finance</p>
      </div>
    </Link>
  );
};

Navigation.tsx

네비게이션 컴포넌트는 반응형 네비게이션 메뉴를 구현한 React함수형 컴포넌트이다. Next.js와 라이브러리를 활용해 화면 크기와 사용자의 인터렉션에 따라 다른 UI를 렌더링 하는 특징이 있다. 

 

먼저 사용된 라이브러리들을 먼저 확인해보자 

1. next/navigation

- useRouter: 페이지의 이동을 처리하는 훅이다. 

- usePathname:현재 페이지의 URL경로를 가져온다. 

2. react-use

- useMedia: 현재 화면ㅇ 크기를 감지해 모바일 환경인지 판별할 수 있게 한다 

3. lucide-react 

- 아이콘을 렌더링 한다. 

 

컴포넌트 로직

1. routes배열 

const routes = [
  { href: "/", label: "Overview" },
  { href: "/transactions", label: "Transactions" },
  { href: "/accounts", label: "Accounts" },
  { href: "/categories", label: "Categories" },
  { href: "/settings", label: "Settings" },
];

- href : 각 메뉴가 이동할 URL이다. 

- label: 각 메뉴의 텍스트 

 

2. isMobile반응형동작처리 

- useMedia("(max-width:1024px)",false)를 이용해 화면 크기가 1024px이하인지 판별한다. 

- 모바일 환경(isMobile===true)과 데스크탑 환경에서 각각 다른 UI를 반환한다. 

모바일 환경 (1024px 이하)

1. sheet컴포넌트

- sheet은 좌측에서 슬라이드로 나타나는 메뉴다.

- isOpen상태를 통해 메뉴가 열리거나 닫힌다. 

<Sheet open={isOpen} onOpenChange={setIsOpen}>

- sheetTrigger: 메뉴를 열기 위한 버튼 

- sheetContent: 메뉴가 열렸을 때 표시되는 항목들 

 

2. Button과 Menu아이콘 

- SheetTrigger내부에 버튼을 렌더링하며 Menu아이콘으로 네비게시연 트리거 역할을 수행한다. 

<Button
  variant="outline"
  size="sm"
  className="font-normal bg-white/10 hover:bg-white/20 ..."
>
  <Menu className="size-4" />
</Button>

 

3. 메뉴 항목 렌더링  

- routes배열을 기반으로 메뉴 버튼을 렌더링한다. 

{routes.map((route) => (
  <Button
    key={route.href}
    variant={route.href === pathname ? "secondary" : "ghost"}
    onClick={() => onClick(route.href)}
    className="w-full justify-start"
  >
    {route.label}
  </Button>
))}

- 현재 경로 (pathname)과 route.href가 같다면 "secondary"스타일로 강조한다. 

- 버튼 클릭 시 onclick을 호출해 페이지 이동과 메뉴 닫기를 처리한다.

데스크탑환경 (1023px 초과)

1. navButton컴포넌트

- 데스크탑 환경에서는 routes배열을 기반으로 각 메뉴 버튼을 렌더링한다. 

<nav className="hidden lg:flex items-center gap-x-2 overflow-x-auto">
  {routes.map((route) => (
    <NavButton
      key={route.href}
      href={route.href}
      label={route.label}
      isActive={pathname === route.href}
    />
  ))}
</nav>

- 현재 URL경로(pathname)와 메뉴 항목의 href를 비교해 활성 상태를 나타낸다. 

- NavButton은 각 메뉴를 정렬하고 스타일링한다. 

2. 반응형 처리 

- className="hidden lg:flex"로 모바일에서는 숨기고 데스크탑에서만 보이도록 설정한다. 

 

핵심함수들 

1. onClick함수 

- href로 페이지를 이동시키고, 모바일 환경에서 메뉴를 닫느다. 

const onClick = (href: string) => {
  router.push(href); // 페이지 이동
  setIsOpen(false); // 메뉴 닫기
};

2. usePathname과 useRouter활용 

- 현재 경로(pathname)를 가져와 활성 메뉴를 결정한다. 

- router.push를 통해 Next.js라우팅한다. 


Hono를 사용해 API구축하기 

간단한 Next.js와 Clerk통합 

추가적으로 이번 프로젝트에서는 hono를 사용하여 api를 구현하게된다. 

1. HONO: 빠르고 간결한 API 라우팅을 제공하는 초경량 웹 프레임워크이다. Next.js와 함께 사용할 때 Edge Runtime과도 호환되어 성능에 민감한 프로젝트에서 적합하다. 

2. Clerk : Clerk는 사용자 인증 및 관리를 간단하게 처리할 수 있도록 도와주는 서비스이다.

clerkMiddleware와 getAuth를 통해 사용자 인증 상태를 확인할 수 있다.

3. Zod : zod는 데이터 스키마 정의 및 검증을 위한 라이브러리이다. 타입스크립트와 긴밀하게 통합되어 API입력 데이터의 안정성과 타입 안정성을 보장한다. 

 

Hono인스턴스 생성 및 기본 경로 설정 

const app = new Hono().basePath("/api");

- hono인스턴스를 생성하고 .basePath("/api")로 기본경로를 /api로 설정한다. 이를통해 모든 엔드 포인트가 /api로 시작한다. 

Clerk미들웨어를 이용한 인증 처리 

app.get("/hello", clerkMiddleware(), (c) => {
  const auth = getAuth(c); // Clerk로부터 인증 정보 가져오기
  if (!auth?.userId) {
    return c.json({ error: "Unauthorized" }); // 인증되지 않은 요청 처리
  }

  return c.json({
    message: "Hello Next.js!",
    userId: auth.userId,
  });
});

- /api/hello 엔드포인트는 Clerk미들웨어를 통해 인증된 사용자만 접근할 수 있다. 

  - clerkMiddleware()는 사용자 인증 상태를 확인하는 역할을 한다. 

  - getAuth(c)는 userId와 같은 인증 정보를 가져온다. 

- 인증되지 않은 사용자는 401unauthorized 응답을 받는다. 

- 인증된 사용자에게는 userId와 함께 간단한 환영 메시지를 반환한다. 

 

GET과 POST핸들러 

export const GET = handle(app);
export const POST = handle(app);

- handle(app)을 통해 Hono앱을 Vercel의 Edge Runtime에서 실행할 수 있는 핸들러로 변환한다.

- GET과 POST 두가지 HTTP메서드를 지원하도록 설정 

 

+ Recent posts