이번 내용은 Shadcn의 데이터 테이블 컴퍼넌트를 설정하고 만드는 방법에 대해 공부했다. 

 

Shadcn의 data-table은 테이블마다 특정한 정렬이나 필터링 요구사항들이 있고 다양한 데이터 소스로 작동하기 때문에 하나의 구성요소로 설정하기 보다는 유연하게 설정할 수 있는 가이드를 제공한다고 한다. 

 

https://ui.shadcn.com/docs/components/data-table

 

Data Table

Powerful table and datagrids built using TanStack Table.

ui.shadcn.com

전체적인 가이드는 다음 내용을 따라서 진행이 되고 자신의 요구사항에 맞추어서 설정을 진행하면된다. 

 

완성된 Account Page

나는 계정들에 대한 정보들을 탐색하는 Account Table을 완성 했다. 

 

이 테이블을 만들기 위해서는 다음 구조들을 통해서 작업한다. 

- columns.tsx는 columns의 정의를 포함한다.

- data-table.tsx는 <DataTable>을 포함한다.

- page.tsx는 table에 대한 렌더링을 진행한다.

 

상황에 맞게 적절하게 잘 조정하면서 만들면됨 

플랫폼을 개발하면서 React Query라는 라이브러리에 대해 알게 되었다. 

https://tanstack.com/query/v5/docs/framework/react/overview#motivation

 

Overview | TanStack Query React Docs

TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes fetching, caching, synchronizing and updating serve...

tanstack.com

 

이 React Query는 React애플리케이션에서 서버 상태를 관리하고 데이터를 효율적이고 안정적으로 fetching, caching, synchronizing, update하는 것을 도와주는 데이터 관리 라이브러리이다. 서버와으이 데이터를 다룰 때 발생할 수 있는 복잡한 로직을 단순화하고 사용자 경험을 개선하는 데 초점을 맞춘 도구이다. 

 

React Querysms Rest API뿐만 아니라 GraphQL, GRPC등 다양한 데이터 소스를 지원하며, 클라이언트 상태 관리와 서버 상태관리를 분리함으로써 React 애플리케이션의 구조를 깔끔하게 유지할 수 있도록 도와준다. 

 

React Query의 주요 개념

서버 상태(Server State)란?

서버 상태는 클라이언트 상태와 달리 서버에서 관리되는 데이터로 클라이언트가 이를 가져와 사용한다.

서버 상태는 다음과 같은 특징이 있다. 

- 클라이언트가 직접 수정할 수 없고 서버에서만 변경 가능하다.

- 서버와 클라이언트 간 동기화를 관리해야 한다.

- 네트워크 요청을 통해 데이터를 가져와야 하며 요청 과정에서 로딩, 에러 처리 등이 필요하다.

React Query는 이러한 서버 상태를 효율적으로 관리하기 위한 도구이다.

React Query의 핵심 기능

- 데이터 페칭(Fetching): 서버 데이터를 가져오고 React 컴포넌트에 바로 연결

- 캐싱(Caching): 가져온 데이터를 메모리에 저장하여 재요청을 줄임

- 자동 갱신(Refetching): 데이터가 변경되었거나 오래되었을 때 자동으로 최신 상태 유지

- 동기화(Synchronization): 서버와 클라이언트 간 데이터 동기화

- 로딩 및 에러 상태 관리: 비동기 요청 상태(로딩, 에러, 성공 등)를 쉽게 처리

React Query의 주요 구성 요소

useQuery

useQuery는 데이터를 가져오기(fetching) 위한 훅이다.

import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

const fetchTodos = async () => {
  const { data } = await axios.get('/api/todos');
  return data;
};

function Todos() {
  const { data, isLoading, isError } = useQuery(['todos'], fetchTodos);

  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Error occurred!</p>;

  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

 

- ['todos']: 쿼리 키(Query Key)로, 요청 데이터를 고유하게 식별한다.

- fetchTodos: 데이터를 가져오는 비동기 함수다.

- isLoading, isError, data: 요청 상태를 React Query가 자동으로 관리한다.

useMutation

useMutation은 데이터를 변경(Post, Put, Delete)하는 데 사용된다.

import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

const addTodo = async (newTodo) => {
  const { data } = await axios.post('/api/todos', newTodo);
  return data;
};

function AddTodo() {
  const queryClient = useQueryClient();

  const mutation = useMutation(addTodo, {
    onSuccess: () => {
      // 'todos' 쿼리를 무효화하여 최신 데이터로 갱신
      queryClient.invalidateQueries(['todos']);
    },
  });

  const handleAddTodo = () => {
    mutation.mutate({ title: 'New Todo' });
  };

  return (
    <button onClick={handleAddTodo}>
      {mutation.isLoading ? 'Adding...' : 'Add Todo'}
    </button>
  );
}

 

 

- useMutation: 데이터 변경 로직을 처리한다.

- onSuccess: 성공 시 후속 작업(예: 쿼리 무효화)을 처리한다.

- invalidateQueries: 특정 쿼리를 무효화하여 데이터를 다시 가져오도록 요청한다.

QueryClient와 QueryClientProvider

React Query를 애플리케이션에서 사용하려면 QueryClient를 생성하고 QueryClientProvider로 감싸야 합니다.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Todos from './Todos';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  );
}

 

- QueryClient: React Query의 모든 상태와 동작을 관리합니다.

- QueryClientProvider: React Query의 컨텍스트를 애플리케이션에 제공합니다.

 

React Query의 특징

자동 데이터 갱신

React Query는 서버 데이터가 변경되었을 때 이를 자동으로 감지하고 최신 데이터를 가져온다. 이를 통해 클라이언트와 서버 간 동기화 문제를 해결한다.

캐싱

React Query는 데이터를 캐싱하여 동일한 데이터를 반복적으로 요청하는 것을 방지한다. 이를 통해 성능을 크게 개선할 수 있다.

옵션 제공

React Query는 다양한 옵션으로 동작을 세밀하게 제어할 수 있다.

- staleTime: 데이터가 '오래된' 상태로 간주되기까지의 시간이다.

- refetchOnWindowFocus: 창이 다시 포커스되었을 때 데이터를 다시 가져올지 여부

const { data } = useQuery(['todos'], fetchTodos, {
  staleTime: 5000, // 5초 동안 데이터가 '신선'한 상태로 유지
  refetchOnWindowFocus: true, // 창 포커스 시 다시 가져오기
});

React Query의 장점

로딩 및 에러 상태 관리 간소화: 데이터 가져오기와 상태 관리를 쉽게 처리할 수 있다.

  1. 데이터 캐싱 및 최적화: 동일한 데이터를 반복적으로 가져오지 않아 네트워크 성능을 개선한다.
  2. 자동 갱신 및 동기화: 서버 상태와 클라이언트 상태를 일치시켜 신뢰할 수 있는 데이터를 제공한다.
  3. 유연성: REST API, GraphQL 등 다양한 데이터 소스에서 작동한다.

React Query의 단점

  1. 러닝 커브: React Query의 개념(쿼리 키, 캐싱, mutation 등)을 처음 배우는 데 시간이 필요할 수 있다.
  2. 초기 설정: 작은 프로젝트에서는 설정과 사용법이 다소 과하게 느껴질 수 있다.

React Query를 사용해야 하는 이유

React Query는 클라이언트 상태와 서버 상태를 분리하여 애플리케이션 구조를 단순화하고, 서버 데이터를 효율적으로 다룰 수 있는 도구를 제공한다. 특히 서버 데이터가 자주 변경되거나 데이터 동기화가 중요한 애플리케이션에서 유용하다.

이번에는 GET API에 이어서 POST API를 만들어 보았습니다. 

지난번에 만들었던 account.tsx파일에 이어서 작성을 합니다. 

 

POST API

라우트 정의 및 미들웨어 추가 

.post(
  "/",
  clerkMiddleware(), // 인증 미들웨어
  zValidator(         // 데이터 검증 미들웨어
    "json",
    insertAccountSchema.pick({
      name: true,
    })
  ),
  async (c) => {      // 요청을 처리하는 핸들러 함수

- clerkMiddleware()는 인증을 담당하는 미들웨어입니다. 사용자의 인증 상태를 확인하고 인증되지 않은 요청을 거부합니다. 

- zValidator는 클라이언트에서 전달된 JSON데이터를 검증하는 미들웨어입니다. 이 코드에서는 insertAccountSchema를 기반으로 데이터를 검증하며 사용자의 name필드만 전달하도록 제한합니다. 

- "json"은 요청 본문이 JSON형식임을 나타냅니다. 

사용자 인증 정보 가져오기 

const auth = getAuth(c);
if (!auth?.userId) {
  return c.json({ error: "Unauthorized" }, 401);
}

- getAuth(c)는 clerkMiddleware를 통해 인증된 사용자의 정보를 가져옵니다. 

- auth?.userId는 사용자의 고유 ID입니다. 인증되지 않은 사용자라면 401응답을 반환합니다. 

요청 데이터 검증 및 데이터 베이스 삽입 

const values = c.req.valid("json");

- c.req.valud("json")은 검증된 요청 데이터를 가져옵니다. zValidator가 검증을 통과한 데이터만 반환하므로 추가적인 검증이 필요하지 않습니다. 

const [data] = await db
  .insert(accounts) // 데이터베이스 테이블에 데이터 삽입
  .values({
    id: createId(),      // 고유 ID 생성
    userId: auth.userId, // 현재 사용자의 ID
    ...values,           // 검증된 데이터 (name 필드)
  })
  .returning();

 

- db.insert(accounts): drizzle-orm을 사용하여 accounts 테이블에 데이터를 삽입합니다.

- createId(): 고유 ID를 생성하는 @paralleldrive/cuid2 라이브러리를 사용합니다.

- auth.userId: 현재 인증된 사용자의 ID를 userId 필드에 저장합니다.

- ...values: 요청에서 검증된 데이터 (name 필드)를 삽입합니다.

- .returning(): 삽입된 데이터의 일부(혹은 전체)를 반환합니다. 여기서는 삽입된 데이터의 첫 번째 항목을 반환합니다.

응답 반환 

return c.json({});

 

- 데이터베이스에 성공적으로 저장되면 빈 JSON응답을 반환합니다. 필요에 따라 클라이언트가 활용할 데이터를 응답에 포함할 수 있습니다. 

동작 흐름 요약

1. 클라이언트가 POST 요청을 통해 데이터를 전송한다. 

2. 서버는 clekrMiddleware로 인증 상태를 확인하고, 인증되지 않으면 401에러를 반환한다. 

3. 요청데이터는 zValidator를 통해 검증되며 유효하지 않은 데이터는 서버로 전달되지 않는다. 

4. 검증된 데이터와 사용자 ID를 데이터베이스에 저장한다.


API를 만들었으니 UI를 수정해보자 

이번에 만든 건 버튼을 눌렀을 때 계정을 만드는 창이 생기는 페이지다. 

useNewAccount훅

이 훅은 zustand를 활용해 상태를 관리하기 위해 사용되는 훅이다. 

 

zustand란? 

zusatnad는 React의 상태 관리 라이브러리로 간결하고 직관적인 API를 통해 상태를 관리할 수 있다. redux와 비교했을 때 설정이 간단하며, 불필요한 보일러플레이트 코드를 줄여준다. 

import { create } from "zustand";

- create는 zustand의 핵심 함수로 상태를 정의하고 사용할 수 있는 훅을 생성한다. 

타입 정의

type NewAccountState = {
  isOpen: boolean; // 모달의 열림 여부를 나타내는 상태
  onOpen: () => void; // 모달을 여는 함수
  onClose: () => void; // 모달을 닫는 함수
};

- isOpen은 모달이 열려있는지 닫혀있는지를 나타낸다.

- onOpen은 상태를 열림으로 변경하는 함수이다. 

- onClose는 상태를 닫힘으로 변경하는 함수이다. 

useNewAccount

export const useNewAccount = create<NewAccountState>((set) => ({
  isOpen: false, // 초기 상태는 모달이 닫혀 있음
  onOpen: () => set({ isOpen: true }), // 상태를 true로 설정
  onClose: () => set({ isOpen: false }), // 상태를 false로 설정
}));

- set은 상태를 업데이트 하기 위한 함수이다. 

- 초기 상태에서 isOpen은 false로 설정되어 모달이 닫혀 있는 상태로 시작한다. 

- onOpen과 onClose함수는 각각 상태를 열림/닫힘으로 전환한다. 

 

이번에는 useCreateAccount 훅에 대해서 알아보자 

useCreateAccount훅 

이 훅은 새로운 계정을 생성하는 API 요청을 처리하고 성공 및 실패 시 사용자에게 알림을 제공한다. 

import { toast } from "sonner";
import { InferRequestType, InferResponseType } from "hono";
import { useMutation, useQueryClient } from "@tanstack/react-query";

import { client } from "@/lib/hono";

- toast: 사용자에게 API 요청 성공 및 실패 여부를 알리기 위해 사용하는 알림 라이브러리

- InferRequestType 및 InferResponseType: hono 라이브러리의 타입 추론 도구로, 요청(Request)과 응답(Response)의 타입을 자동으로 추론한다.

- useMutation: React Query의 비동기 요청 처리를 위한 훅으로, 데이터 생성, 수정, 삭제와 같은 작업을 처리한다.

- useQueryClient: React Query의 캐싱 및 데이터 동기화를 관리하는 클라이언트이다. 

타입 정의 

type ResponseType = InferResponseType<typeof client.api.accounts.$post>;
type RequestType = InferRequestType<typeof client.api.accounts.$post>;

- ResponseType: client.api.accounts.$post로부터 응답 데이터의 타입을 추론.

- RequestType: client.api.accounts.$post로부터 요청 데이터의 타입을 추론.

코드 설명

export const useCreateAccount = () => {
  const queryClient = useQueryClient();

- queryClient는 ReactQuery에서 제공하는 캐시 관리 도구이다. API호출 후 데이터 동기화나 캐시  무효화를 처리할 때 사용된다. 

const mutation = useMutation<ResponseType, Error, RequestType>({
    mutationFn: async ({ json }) => {
      const response = await client.api.accounts.$post({ json });
      return await response.json();
    },

 

- useMutation은 API호출을 처리하는 메인 함수이다. 

- mutaitonFn은 실제 API요청을 처리하는 비동기 함수이다. 

- client.api.accounts.$post... 은 hono클라이언트를 사용하여 계정을 생성하는 API를 호출한다. 

onSuccess: () => {
      toast.success("Account created");
      queryClient.invalidateQueries({ queryKey: ["accounts"] });
    },

- 이 함수는 요청이 성공했을 때 실행되는 콜백함수이다. 

- 요청이 성공하면 toast를 통해 Account created알림이 표시된다. 

- queryClient.invalidateQueries({ queryKey: ["accounts"] });는 accounts와 관련된 캐시 데이터를 무효화하여 최신 데이터를 가져오도록 설정한다. 

onError: () => {
      toast.error("Failed to create account");
    },
  });

- 요청이 실패했을 때 실행되는 콜백함수이다. 

- toast.error를 통해 실패시 알람이 발생한다. 

작동과정

사용자가 폼에 데이터를 입력하고 제출 버튼을 누르게 되면 mutate함수가 호출되서 mutationFn이 실행된다. 

API요청이 성공하면 성공메시지가 표시되고 관련 캐시 데이터가 무효화되어 최신 데이터로 갱신된다. 실패하면 실패 메시지가 뜨겠죠?

왜 이코드가 유용할까요?

 

  1. 효율적인 서버 상태 관리
    React Query와 useMutation을 사용하여 비동기 데이터 생성 및 에러 처리를 간단히 관리할 수 있습니다.
  2. 자동화된 캐싱
    queryClient.invalidateQueries를 통해 데이터 동기화 과정을 자동화할 수 있어, 추가적인 데이터 페칭 코드를 작성할 필요가 없습니다.
  3. 알림 메시지 통합
    toast를 사용하여 사용자 경험(UX)을 개선합니다. 성공 및 실패 시 사용자에게 즉각적인 피드백을 제공합니다.

 

NewAccountSheet

이 컴포넌트는 React와 typescript를 활용하여 모달 형식으로 새로운 계정을 생성하는 UI를 제공한다. 주요 기능과 구현 방법을 이해하면서  컴포넌트 설계와 상태 관리를 어떻게 효율적으로 처리했는지 알아 볼 수 있다. 

코드 구조 분석 

import { z } from "zod";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
} from "@/components/ui/sheet";
import { useNewAccount } from "../hooks/use-new-account";
import { AccountForm } from "@/components/account-form";
import { insertAccountSchema } from "@/db/schema";
import { useCreateAccount } from "../api/use-create-account";

- zod: 폼 데이터의 유효성 검증을 위한 스키마 정의 라이브러리.

- Sheet: 모달 UI를 제공하는 컴포넌트.

- useNewAccount: 모달의 열림/닫힘 상태를 제어하는 커스텀 훅.

- useCreateAccount: 계정을 생성하는 API 요청을 처리하는 커스텀 훅.

- AccountForm: 계정 생성 폼 컴포넌트.

유효성 검증 스키마 정의 

const formSchema = insertAccountSchema.pick({
  name: true,
});
type FormValues = z.input<typeof formSchema>;

- insertAccountSchema: 데이터베이스와 연동된 전체 스키마에서 필요한 필드만 선택한다. 여기서는 계정이름필드만 사용한다. 

- FormValues: zod의 스키마를 기반으로 타입을 생성한다. 이는 폼에서 입력받는 데이터의 타입으로 사용된다. 

API요청 처리

const mutation = useCreateAccount();
const onSubmit = (values: FormValues) => {
  mutation.mutate(
    { json: values },
    {
      onSuccess: () => {
        onClose();
      },
    }
  );
};

- 아까설명한 useCreateAccount를 통해 서버와 통신하여 계정을 생성한다. 

- onSubmit을 통해 AccountForm에서 폼 데이터를 제출하면 호출된다. 

- 데이터를 API에 전달하고 요청이 성공하면 모달을 닫는다. 

 

렌더링 과정은 생략할래 ... 

 

작동과정 

1. 모달을 useNewaccoiunt훅을 통해 sheet컴포넌트를 렌더링 한다. 

2. AccountForm에 계정을 입력하고 제출 버튼을 누르면 onSubmit함수가 호출된다. 

3. API요청을 처리한다. 

- useCreateAccount의 mutation.mutate가 실행되어 서버에 데이터를 전송한다. 

- 요청 성공 시에는 성공 메시지가 표시되고 이후에 닫힌다. 

4. 모달 닫기 

 

이 코드의 유용성 

  1. 컴포넌트 재사용성
    NewAccountSheet는 계정 생성 모달이라는 특정 역할에 집중되어 있어 다른 프로젝트에서도 쉽게 재사용할 수 있습니다.
  2. 깔끔한 상태 관리
    • useNewAccount 훅을 통해 상태를 분리하여 유지보수를 간소화했습니다.
    • useCreateAccount 훅은 API 요청 로직을 캡슐화하여 재사용성을 높였습니다.
  3. 입력 데이터 유효성 검증
    zod를 활용해 폼 데이터를 강력하게 검증하여 오류를 사전에 방지합니다.
  4. UX 강화
    • 폼이 제출 중일 때 비활성화 상태로 전환하여 중복 요청을 방지합니다.
    • 성공 및 실패 시 적절한 알림을 제공하여 사용자 경험을 개선했습니다.

 

Accounts API 

Route.ts

Hono와 라우터 설정 

import { Hono } from "hono";
import accounts from "./accounts";

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

- basePath: 모든 라우터의 기본 경로를 /api로 설정한다. 예를들어 accounts라우터는 /api/accounts로 접근 가능하다. 이를통해 일관된 경로 구조를 유지할 수 있다. 

라우터 파일 분리 및 통합 

import accounts from "./accounts";
const routes = app.route("/accounts", accounts);

- 모듈화된 라우터: accounts라우터를 별도의 파일로 분리했다. 라우터 파일에는 /accounts경로와 관련된 모든 API로직이 포함된다. 이렇게 모듈화하면 각 경로의 기능을 독립적으로 관리할 수 있어 유지보수와 확장이 용이하다. 

전역에러 처리 

app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return err.getResponse();
  }
  return c.json({ error: "Internal server error" }, 500);
});

 - 에러 처리 : onError는 애플리케이션에서 발생하는 모든 에러를 전역적으로 처리한다. 

- HTTPException: Hono에서 제공하는 에러 클래스로 HTTP상태코드와 함께 클라이언트에 적절한 응답을 보낸다. 

- 예상치 못한 에러 : 다른 에러는 500 상태 코드와 함께 internal server error메시지를 반환한다. 

- 이방식은 API전체에서 일관성 있는 에러 응답을 보장하고 코드 중복을 줄일 수 있다. 

Vercel 과의 통합 

import { handle } from "hono/vercel";

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

- Vercel핸들러에서 Hono앱을 Vercel의 서버리스 환경에서 실행할 수 있도록 설정한다. 

- handle함수는 Hono앱을 요청 처리 하는 방식에 맞게 변환한다. 

- Get과 Post요청을 각각 지원하도록 설정되어 있다. 

- 이 설정은 Vercel베포를 간소화하며 엣지 환경에서 빠르게 동작하도록 최적화한다. 

동작원리 

1. 클라이언트가 /api/accounts경로로 요청을 보낸다. 

2. 요청이 accounts라우터로 전달된다. 

3. 에러가 발생하면 HTTPException일 경우, 정의된 에러 응답이 반환된다. 기타의 에러의 경우 interval servererror와 함께 500상태 코드로 처리된다. 

Account.ts

Hono 라우터 설정 

const app = new Hono().get("/", clerkMiddleware(), async (c) => {
  const auth = getAuth(c);

- 라우터 등록 : Hono().get()은 GET요청을 처리하는 라우트를 등록한다. 

여기서 경로는 / 이다. 

- 미들웨어 등록: ClerkMiddleware()는 인증 미들웨어로 인증되지 않은 요청을 자동으로 차단한다. 

- getAuth(c): 현재 요청의 인증 정보를 가져온다. 

 예: auth.userID -> 요청한 사용자의 고유 ID

인증 실패 처리 

if (!auth?.userId) {
  throw new HTTPException(401, {
    res: c.json({ error: "Unauthorized" }, 401),
  });
}

- 인증 정보가 없거나, auth.userId가 없는 경우 401Unauthorized에러를 반환한다. 

- HTTPException은 hono에서 제공하는 HTTP에러 객체로, 응답 코드와 메시지를 포함한다. 

데이터베이스 쿼리 

const data = await db
  .select({
    id: accounts.id,
    name: accounts.name,
  })
  .from(accounts)
  .where(eq(accounts.userId, auth.userId));

- db.select():데이터베이스에서 데이터를 조회하여 id와 name열만 선택해 반환한다. 

- from(accounts): 열만 선택해 반환한다. 

- where(eq(accounts.userId, auth.userId)): 테이블의 userId가 인증된 사용자의 userId와 동일한 레코드를 필터링한다. 

- 결과반환 : 반환된 데이터는 data객체에 저장된다. 예: [{ id: "123", name: "John Doe" }]

응답반환 

return c.json({ data });

조회된 데이터는 JSON형식으로 반환된다. 

 

React Query와 Provider설정

이 코드는 next.jsdml app/providers.tsx 파일 구조를 기반으로 작성된 React Query Provider를 설정하는 코드이다. @tanstack/react-query를 활용해서 클라이언트와 서버 간의 효율적인 데이터 관리를 가능하게 한다. 이를 통해 데이터 요청, 캐싱, 동기화, 그리고 서버 상태 관리가 훨씬 간편해진다. 

주요기능 

reactQuery와 QueryClientProvider

- QueryClientProvuder는 react query의 핵심 컴포넌트로 앱의 전역 상태 관리를 담당한다. 

- 이 provider는 react query의 queryclient를 받아 애플리케이션 전체에서 데이터 캐싱과 동기화를 제공한다. 

QueryClient 생성 

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000, // 1분 동안 데이터를 신선하다고 간주
      },
    },
  });
}

- makeQueryClient함수는 새로운 queryclien객체를 생성한다. 

- staleTime옵션 

- 데이터를 fresh하다고 간주하는 시간을 설정한다. 

- 60초로 설정되어 있어 데이터가 캐시된 후 1분 동안 재요청 없이 사용할 수 있다. 

- SSR에서 초기 데이터를 다시 가져오는 불필요한 요청을 방지한다. 

클라이언트와 서버 환경에 따른 QueryClient관리 

let browserQueryClient: QueryClient | undefined = undefined;

function getQueryClient() {
  if (isServer) {
    return makeQueryClient(); // 서버에서는 항상 새로운 QueryClient 생성
  } else {
    if (!browserQueryClient) browserQueryClient = makeQueryClient(); // 브라우저에서는 QueryClient 재사용
    return browserQueryClient;
  }
}

서버환경

- 서버에서는 매 요청마다 새로운 QueryClient를 생성한다. 

- 이는 서버가 상태를 재사용하지 않고 각 요청마다 독립적인 처리를 보장하기 위함이다. 

브라우저 환경 

- 브라우저에서는 queryClient를 한번만 생성하여 재사용한다. 

- React의 suspense가 초기 렌더링 중 다시 렌더링을 트리거해도, 새로운 queryClient가 생성되지 않도록 방지한다. 

Providers컴포넌트 

export default function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient();

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}

providers 컴포넌트 

- 이 컴포넌트는 queryClientProvider를 사용하여 React Query를 애플리케이션의 컨텍스트로 제공한다. 

- 자식 컴포넌트들은 모두 queryClient를 활용할 수 있게된다. 

children속성 

- Providers컴포넌트의 자식 컴포넌트는 children으로 전달된다. 

- 이 구조를 통해 React Query의 데이터 관리 기능을 전체 애플리케이션에서 사용할 수 있다. 

사용처 

앱 전체 데이터 관리 

- provider는 Next.js의 lauout.tsx나 최상위 컴포넌트에서 사용되어, 애플리케이션 전체에 React Qeury의 기능을 제공한다. 

 

클라이언트와 서버 데이터 연동 

- SSR및 CSR동시 지원 : next.js의 getserversideProps나 useQuery를 통해 서버와 클라이언트에서 데이터를 가져온다. 

- React Query의 캐싱덕분에 서버에서 가져온 데이터를 클라이언트에서 효율적으로 재사용할 수 있다. 

주요 특징 

1. 서버와 클라이언트 환경에 따라 최적화된 QueryClient관리 

- 서버에서는 요청마다 새로운 queryclient를 생헝한다. 

- 브라우저에서는 queryclient를 재사용하여 불필요한 리소스 낭비를 방지한다. 

2. suspense와의 호환성 

- react의 suspense가 초기 렌더링에서 중단되더라도 동일한 queryclient를 재사용하여 데이터 요청 문제를 방지한다. 

3. 설정 가능한 Staletiem

- staleTiem을 설정해 데이터 캐싱 기간을 조절 가능하다. 

4. 확장성과 모듈화

- providers컴포넌트를 사용하여 애플리케이션의 전역 데이터 관리가 간소화된다. 

useGetAccounts 훅

useGetAccounts는 React query를 활용하여 서버에서 계정 데이터를 가져오는 Custom hook이다. 이 훅은 api요청과 관련된 로직을 캡슐화 하여 React컴포넌트에서 쉽게 데이터를 가져오고 상태를 관리할 수  있도록 돕는다. 

주요 역할 

- API호출관리: 서버의 GET/ Accounts API를 호출하여 데이터를 가져온다. 

- React Query를 통한 상태 관리 : 데이터를 캐싱하고 로딩/에러 상태를 관리한다. 

- 코드 재사용성 제공: 동일한 API호출 로직을 여러 컴포넌트에서 재사용할 수 있도록 Hook형태로 제공된다. 

 

useQuery사용 

const query = useQuery({
  queryKey: ["account"],
  queryFn: async () => {
    const response = await client.api.accounts.$get();
    if (!response.ok) {
      throw new Error("Failed to fetch accounts");
    }
    const { data } = await response.json();
    return data;
  },
});

- useQuery : react query의 핵심 함수로, 서버에서 데이터를 가져오고 상태를 관리한다. 

- 주요 옵션으로 queyKey는 캐싱과 상태 관리를 위한 키로 여기서는 ["account"]를 사용한다.

- queryFn: 데이터를 가져오는 비동기 함수로 API호출 로직이 포함된다. 

QueryFn 동작 

const response = await client.api.accounts.$get();
if (!response.ok) {
  throw new Error("Failed to fetch accounts");
}
const { data } = await response.json();
return data;

API호출 

-  client.api.accounts.$get()는 GET/accounts엔드포인트에 요청을 보낸다. 

- 요청이 실패하면 Error를 throw하여 React Query가 이를 에러 상태로 처리하도록 하게 한다. 

응답처리 

- 성공적인 응답(response.ok)이 확이되면 JSON데이터를 파싱하고, data만 반환한다. 

- 이 반환된 데이터는 React Query가 캐싱 및 상태 관리에 활용된다. 

hook 반환값 

return query;

- 이 hook은 useQuery의 반환값을 그대로 반환한다. 

- 이 반환값에는 다음 상태들이 포함된다. 

data, isLoading, isError, error

 

장점 

1. 코드 간소화 

- API호출 로직이 캡슐화되어, 컴포넌트에서 데이터 로직이 제거된다. 

- 컴포넌트는 데이터 상태를 신경 쓰는데 집중할 수 있다. 

2. 재사용성 

- 동일한 API호출이 필요한 곳에서 useGetAccounts를 재사용할 수 있다. 

3. React Query의 강력한 기능 활용 

- 캐싱, 자동 리패치, 로딩/에러 상태 관리 등 React Query의 모든 기능을 쉽게 적용가능하다. 

4. 유지 보수에 용이하다. 

- API로직 변경 시 Hook 내부만 수정하면 된다. 

- 컴포넌트에 흩어진 호출 코드를 관리할 필요가 없다. 

 

hono.ts

import { hc } from "hono/client";

import { AppType } from "@/app/api/[[...route]]/route";

export const client = hc<AppType>(process.env.NEXT_PUBLIC_APP_URL!);

이 코드는 Hono프레임워크를 클라이언트 측에서 사용하기 위한 설정 파일이다. API클라이언트를 생성해서 서버와의 통신을 간편하게 수행할 수 있도록 돕는다. 

 

주요역할 

1. Hono 클라이언트 생성 : 서버의 API를 클라이언트 측에서 호출할 수 있도록 도와준다. 

2. 타입 안전성 제공: 서버에서 정의한 API 타입을 기반으로 클라이언트에서 타입 안전성을 확보한다. 

3. 환경 변수 기반 URL설정 : 서버 URL을 동적으로 설정하여 개발 환경과 프로덕션 환경에서 동일한 코드를 사용할 수 있다. 

 

동작원리 

이 설정 파일은 클라이언트 측에서 hono로 작성된 API를 호출할 때 사용된다. 

1. 서버의 API경로와 메서드 정보가 AppType에 정의되어 있다. 

2. 이 정보를 기반으로 타입 안전한 클라이언트를 생성한다. 

3. 클라리언트는 client.api.<endpoint> 형태로 API호출을 수행한다. 

 

개발을 할 때 초기 환경 설정들이 나를 힘들게 하는 경우가 참 많은 거 같다. 

어렵다. 어려워 .. 

 

이번 프로젝트에서 사용된 데이터 베이스 도구들은 Neon과 Drizzle이다. 

Neon 

Neon은 PostgreSQL클라우드 서비스로, 서버리스 PostgreSQL 데이터베이스를 제공하는 플랫폼이다. pg를 기반으로 하되, 기존 데이터베이스의 단점을 보완하고 현대적인 개발자 경험을 제공한다. 

Neon의 주요 특징 

1. 서버리스 

- 인프라를 관리할 필요 없이, 사용량에 따라 확장되고 비용이 최적화된다. 

- 필요할 때만 리소스를 사용하고 불필요한 비용을 줄여준다. 

2. 분리된 스토리지와 컴퓨팅 

- 네온은 스토리지와 컴퓨팅을 분리하여 필요에 따라 독립적으로 확장할 수 있다. 

- 이로 인해 데이터를 처리하거나 성능을 높이기 쉬워진다. 

3. 자동 스냅샷과 백업 

- 데이터베이스의 스냅샷과 백업이 자동으로 이루어져 데이터 손실의 위험을 줄인다. 

4. PostgreSQL호환성 

- 네이티브 PostgreSQL을 사용하므로 기존 PostgreSQL클라이언트 및 도구와 완벽히 호환된다. 

5. 분산형 아키텍쳐 

- 데이터베이스 리소스를 유연하게 분산하고 관리하여 더 높은 가용성을 보장한다.

Drizzle 

Drizzle은 Typescript/Javascript환경에서 사용할 수 있는 경량 SQL ORM이다. 데이터베이스 쿼리를 더 안전하고 깔끔하게 작성할 수 있도록 도와준다. 

Drizzle의 주요 특징 

1. 타입 안전성 

- typescript의 강력한 타입  시스템을 활용해 타입 안전한 쿼리를 작성할 수 있다. 

- 쿼리 작성 시 오류를 컴파일 타입에 잡을 수 있다. 

2. SQL친화적 

- Drizzle은 SQL을 추상화하는 대신, 개발자가 SQL쿼리를 더 효율적으로 작성할 수 있도록 돕는다. 

- 복잡한 SQL쿼리도 손쉽게 작성할 수 있다. 

3. 경량 및 모듈화

- 불필요한 기능이 없고 매우 가벼우며, 필요한 기능만 모듈처럼 가져와 사용할 수 있다. 

- 런타입 성능에 부담을 주지 않는다. 

4. 마이그레이션 지원 

- Drizzle은 데이터베이스 스키마 마이그레이션 기능을 제공하여 데이터베이스 변경 사항을 효율적으로 관리할 수 있게 한다. 

5. PostgreSQL, MySQL, SQLite 지원

- 다양한 데이터베이스를 지원하며 특히 PostgreSQL과 함께 자주 사용된다.

6. CLI와 코드 기반 설정

- CLI를 통해 데이터베이스 스키마를 생성하고 관리할 수 있으며, 코드 기반으로 테이블 스키마를 정의한다.

Neon과 Drizzle의 관계 

- neon은 postgreSQL클라우드 플랫폼이며, Drizzle은 PostgreSQL과 같은 데이터베이스를 더 쉽게 사용할 수 있게 도와주는 ORM이다. 

- 함께 사용하면 서버리스 PostgreSQL환경에서 타입 안전한 쿼리를 작성하고 데이터베이스 스키마를 관리하는 모던한 개발 경험을 얻을 수 있다.

Drizzle과 Neon으로 마이그레이션 설정하기 

https://orm.drizzle.team/docs/get-started/neon-new

 

Drizzle ORM - PostgreSQL

Drizzle ORM is a lightweight and performant TypeScript ORM with developer experience in mind.

orm.drizzle.team

 

그럼 Drizzle과 neon으로 데이터베이스 스키마를 생성하고 마이그레이션하는 과정에 대해서 살펴보자 

먼저 프로젝트 구조는 다음과 같다. 

.
├── db/
│   ├── drizzle.ts       
│   └── schema.ts        
├── scripts/
│   └── migrate.ts       
├── drizzle/             
│   ├── 0000_***.json
│   ├── 0001_***.json
│   ├── _journal.json
│   ├── 0000_snapshot.json
│   └── 0001_snapshot.json
└── package.json

 

DB폴더 

Schema.ts

db/schema.ts는 테이블 스키마를 정의한다. 여기서는 accounts테이블이 정의되어 있다. 

import { pgTable, text } from "drizzle-orm/pg-core";

export const accounts = pgTable("accounts", {
  id: text("id").primaryKey(),      
  plaidId: text("plaid_id"),        
  name: text("name").notNull(),     
  userId: text("user_id").notNull() 
});

 

  • pgTable: PostgreSQL 테이블을 정의하는 함수이다.
  • text(): 각 필드의 데이터 타입을 text로 지정한다.
  • primaryKey(): id 필드를 기본 키로 설정한다.
  • notNull(): name과 userId 필드는 null 값을 허용하지 않음을 명시한다.

drizzle.ts

db/drizzle.ts에서는 Neon클라이언트와 Drizzle ORM을 연결한다. 

import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";

export const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

 

  • neon: Neon의 서버리스 PostgreSQL 클라이언트를 생성한다.
  • drizzle: Drizzle ORM과 schema.ts 파일을 연결한다.
  • DATABASE_URL: 데이터베이스 연결 문자열이 환경 변수에 저장되어야 한다.

Scripts폴더 

migrate.ts

migrate.ts파일은 Drizzle ORM의 마이그레이션을 실행하는 코드이다. 

import { migrate } from "drizzle-orm/neon-http/migrator";
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import { config } from "dotenv";

config({ path: ".env.local" }); // 환경 변수 불러오기

const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql);

const main = async () => {
  try {
    await migrate(db, { migrationsFolder: "drizzle" });
    console.log("Migration completed successfully!");
  } catch (error) {
    console.error("Error during migration:", error);
    process.exit(1);
  }
};

main();

 

  • config(): .env.local에서 환경 변수를 불러온다.
  • migrate(): db 연결 인스턴스를 통해 drizzle 폴더에 있는 마이그레이션 파일들을 실행한다.
  • migrationsFolder: 자동 생성된 마이그레이션 파일들이 저장될 폴더이다.

명령어 추가 

package.json에는 명령어를 추가해야한다. 

"scripts": {
  "db:generate": "drizzle-kit generate:pg --schema db/schema.ts --out ./drizzle",
  "db:migrate": "tsx ./scripts/migrate.ts",
  "db:studio": "drizzle-kit studio"
}

 

  1. npm run db:generate
    • schema.ts를 바탕으로 마이그레이션 파일을 자동 생성한다.
    • --out 옵션으로 출력될 폴더를 ./drizzle로 지정한다.
  2. npm run db:migrate
    • scripts/migrate.ts 파일을 실행해 마이그레이션을 수행한다.
    • 마이그레이션은 drizzle 폴더에 저장된 JSON 파일을 기반으로 진행된다.
  3. npm run db:studio
    • Drizzle Studio를 실행해 데이터베이스 스키마와 데이터를 시각적으로 확인할 수 있다

 

 

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메서드를 지원하도록 설정 

 

프로젝트를 시작했다. 

bun을 사용해 볼까 했지만 그냥 npm을 사용하기로 했다. 이게 아직은 익숙하기때문이다.

npx create-next-app my-app

 

shadcn ui

그리고 이번 프로젝트에서는 UI 컴포넌트에서 shadcn UI를 사용할 예정이다. 

https://dozistory.tistory.com/164

 

[기타]shadcn ui에 대해서 알아보자

shadcn uishadcn/ui는 React를 기반으로 Tailwind CSS를 활용해 빠르고 효율적으로 UI 컴포넌트를 구축할 수 있도록 돕는 오픈소스 UI 라이브러리이다. 이 라이브러리는 일반적인 UI 키트와는 다르게 컴포

dozistory.tistory.com

다음 명령어를 통해 shadcn을 설치해주자 

npx shadcn@latest init

 

그리고 버튼 컴포넌트들을 추가하려면 다음 명령어를 작성해주면 된다.

npx shadcn@latest add button

 

 

components/ui에 생성된 button.tsx파일을 확인 할 수 있다. 


폴더 구조 

왜 이렇게 설정하는 걸까요 ? 

1. 폴더이름 (auth)와 (dashboard)

괄호로 묶인 폴더 이름Routing Group을 나타낸다.

  • 이는 UI에서 특정 경로를 그룹화하지만, URL 경로에는 반영되지 않습니다.
  • 예를 들어, app/(auth)/sign-in/page.tsx는 URL 경로 /sign-in에 대응됩니다.
  • 목적: 인증 경로와 비인증 경로를 명확히 분리하고 코드 구조를 깔끔하게 유지한다.

2. [[...segment]]: Dynamic and Catch-all Routes

  • [[...sign-in]]은 선택적 캐치-올 라우트(Optional Catch-All Route)이다.
    • /sign-in 경로뿐 아니라 /sign-in/other-path도 처리 가능.
    • 목적: Clerk.js에서 인증 흐름 중 리다이렉션이나 쿼리 파라미터를 다룰 때 유연하게 대응.

3. 클라이언트와 서버의 역할 분리

  • app/(auth) 아래에 인증 관련 UI를 모아둠으로써 Clerk.js 컴포넌트(SignIn, SignUp)를 로드하는 경량 페이지를 구성.
  • 반대로, app/(dashboard)는 인증 이후 대시보드 데이터를 불러오고 렌더링하는 비즈니스 로직 중심 페이지를 구성.

4. Lazy Loading과 성능 최적화

  • (auth)와 (dashboard)는 별도 경로 그룹으로 분리되기 때문에, Next.js는 사용자가 해당 경로로 이동할 때만 관련 코드를 Lazy Loading합니다.
    • 인증이 필요한 경로만 로드하고, 불필요한 코드는 로드하지 않아 초기 렌더링 성능을 향상.

5. Clerk.js와 인증 보호

  • Clerk.js는 MiddlewareProtected Routes 기능을 통해 특정 경로에 대한 인증을 강제.
  • app/(auth)는 인증이 필요 없는 Public Routes,app/(dashboard)는 로그인한 사용자만 접근 가능한 Protected Routes로 나뉘어야 하기 때문에, Next.js 라우팅 그룹으로 관리

Clerk사용 

Clerk를 사용하려면 당연히 api 설정을 해주어된다. 

가서 계정 로그인 하고 Key값을 받아와서 .env파일에 해당 내용을 작성해준다. 그리고 꼭 잊지말고 이 .env파일은 gitignore에 설정해주어야 한다. 

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=********************
CLERK_SECRET_KEY=********************

NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

미들웨어 

이 컴포넌트들을 사용하기 위해서 Middleware를 설정해주어야 한다. 

roo폴더에 middleware.ts를 만들어주고 다음 공식 문서의내용을 따라서 진행 해주면 된다. 

https://clerk.com/docs/references/nextjs/custom-signup-signin-pages?_gl=1*gn2a5a*_gcl_au*MjEwNjI1OTYxOC4xNzMyNzAwMDU1*_ga*NTQ1ODkzNjc3LjE3MzM5MzMxMjg.*_ga_1WMF5X234K*MTczMzkzMzEyNy4xLjEuMTczMzkzMzM1Mi4wLjAuMA

 

Next.js: Build your own sign-in and sign-up pages for your Next.js app with Clerk

Learn how to add custom sign-in and sign-up pages to your Next.js app with Clerk's prebuilt components.

clerk.com

 

import { NextResponse } from "next/server";
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

const isProtectedRoute = createRouteMatcher(["/"]);

export default clerkMiddleware(async (auth, request) => {
  if (isProtectedRoute(request)) {
    await auth.protect();
  }
  return NextResponse.next();
});

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
    // Always run for API routes
    "/(api|trpc)(.*)",
  ],
};

 

이 미들웨어 설정하는데 계속 오류가 발생했었는데 경로 설정에서의 문제가 있었다. 

<Signin/>과 <Signup/>에 path를 통해서 경로설정을 해주어야 오류가 생기지 않는다.  

Sign-In, Sign-Up

import Image from "next/image";
import { Loader2 } from "lucide-react";
import { SignIn, ClerkLoaded, ClerkLoading } from "@clerk/nextjs";

export default function Page() {
  return (
    <div className="min-h-screen grid grid-cols-1 lg:grid-cols-2">
      <div className="h-full lg:flex flex-col items-center justify-center px-4">
        <div className="text-center space-y-4 pt-16">
          <h1 className="font-bold text-3xl text-[#2E2A47]">Welcome Back!</h1>
          <p className="text-base text-[#7E8CA0]">
            Log in or Create account to get back to your dashboard!
          </p>
        </div>
        <div className="flex items-center justify-center mt-8">
          <ClerkLoaded>
            <SignIn path="/sign-in" />;
          </ClerkLoaded>
          <ClerkLoading>
            <Loader2 className="animate-spin text-muted-foreground" />
          </ClerkLoading>
        </div>
      </div>
      <div className="h-full bg-blue-600 hidden lg:flex items-center justify-center">
        <Image src="/logo.svg" height={100} width={100} alt="Logo" />
      </div>
    </div>
  );
}

- signin은 이렇게 UI가 구성되었다. signout도 똑같다. 

그리고 완성된 페이지 

 

Clerk.js사용자 인증 및 사용자 관리(User Management)를 간편하게 구현할 수 있도록 돕는 개발자 도구이다. Clerk는 주로 React, Next.js, Vue, Node.js 등 다양한 프레임워크와 통합되어 작동하며, 인증 및 사용자 관련 기능을 직접 구현할 필요 없이 빠르게 구축할 수 있게 도와주는 역할을 한다. 

Clerk.js 주요 특징

  1. 다양한 인증 방식 지원
    • 이메일/비밀번호 기반 로그인
    • 소셜 로그인 (Google, Facebook, GitHub 등)
    • OTP 기반 인증 (SMS, 이메일)
    • 패스워드 없는 인증 (Magic Link, WebAuthn 등)
  2. 사용자 관리(User Management)
    • 사용자 프로필 관리
    • 사용자 설정 페이지(User settings UI) 제공
    • 조직 관리(팀 및 조직 기반 워크플로우 지원)
  3. Next.js와의 강력한 통합
    • Middleware를 활용해 특정 페이지를 보호(Protected Routes)
    • API 라우트 보호를 위해 auth 객체 제공
    • Server-side Rendering(SSR) 및 **Static Site Generation(SSG)**에서 유연하게 인증 정보 사용 가능.
  4. 커스터마이징 가능한 UI
    • 기본 제공되는 Sign-in, Sign-up, User Profile 컴포넌트를 그대로 사용할 수 있음.
    • Tailwind CSS 등으로 스타일 커스터마이징 가능.
  5. 보안과 규정 준수
    • WebAuthn 지원을 통해 보안 인증 구현 가능.
    • GDPR, CCPA, SOC 2 등 주요 데이터 보호 규정을 준수.
  6. 다중 플랫폼 지원
    • React, React Native, Vue, Angular, Node.js 등 다양한 프레임워크와 통합 가능.
    • REST API, GraphQL API를 사용하여 프론트엔드와 백엔드 모두 쉽게 연결 가능.

Clerk.js 의 장점

  1. 빠른 개발
    • 인증 및 사용자 관리에 필요한 시간을 크게 절약할 수 있음.
    • API 호출만으로 복잡한 인증 로직을 구현 가능.
  2. 확장성
    • 단순 로그인 기능뿐만 아니라, 사용자 프로필, 팀, 조직 등 더 복잡한 사용자 관리 기능도 제공.
  3. 보안 강화
    • WebAuthn, Magic Link 등 강력한 보안 인증 방식 내장.
  4. SSR/SSG 지원
    • Next.js 등에서 서버 사이드 렌더링과 정적 페이지 생성에 필요한 인증 정보를 제공.
  5. 손쉬운 커스터마이징
    • 기본 제공되는 UI 컴포넌트를 Tailwind CSS 등으로 자유롭게 수정 가능.

Clerk.js 주요 구성 요소

  1. Auth Components
    • <SignIn />: 로그인 UI 컴포넌트
    • <SignUp />: 회원가입 UI 컴포넌트
    • <UserProfile />: 사용자 프로필 관리 UI 컴포넌트
    • <SignOutButton />: 로그아웃 버튼
  2. API 및 Hook
    • useAuth: 현재 로그인 상태와 인증 정보를 가져옴.
    • useUser: 현재 사용자(User) 정보를 가져옴.
    • authMiddleware: 특정 라우트를 보호하는 데 사용.
    • getAuth: 서버에서 인증 정보에 접근하기 위한 함수.
  3. Middleware
    • Next.js에서 특정 페이지나 API 라우트를 보호할 수 있도록 Clerk의 미들웨어를 제공.
  4. Dashboard
    • Clerk 대시보드를 통해 사용자, 팀, 조직 등을 관리 가능.

Clerk.js와 기존 인증 시스템 비교

Clerk.js 기존 인증 구현 (직접 개발)
빠른 설정 및 통합 가능 개발자가 모든 인증 로직을 직접 구현해야 함
다양한 인증 방식 기본 제공 특정 인증 방식을 구현하려면 추가 작업 필요
커스터마이징 가능한 UI 컴포넌트 제공 UI를 직접 설계하고 개발해야 함
데이터 보호 및 보안 규정을 자동 준수 규정 준수를 위해 별도 검토 및 구현 필요

 

사용 예시 (Next.js)

import { SignIn } from "@clerk/nextjs";

export default function SignInPage() {
  return <SignIn />;
}

API Route보호 

import { authMiddleware } from "@clerk/nextjs";

export default authMiddleware({
  publicRoutes: ["/public-page"],
});

export const config = {
  matcher: "/((?!_next|static|favicon.ico).*)",
};

공식 사이트 및 문서

https://clerk.com/

 

Clerk | Authentication and User Management

The easiest way to add authentication and user management to your application. Purpose-built for React, Next.js, Remix, and “The Modern Web”.

clerk.com

더 궁금한 내용이 있으면 다음 내용을 살펴 보자 

+ Recent posts