colums

새로운 타입 정의 

export type ResponseType = InferResponseType<
  typeof client.api.accounts.$get,
  200
>["data"][0];

Account.ts에서는 타입이 따로 정해져 있지 않았으나 ResponseType을 따로 정의를 해주었다. 

- InferResponseType을 사용해서 API응답 타입을 정의했다.

- client.api.accounts.$get의 응답 데이터에서 200 상태의 데이터를 기반으로 첫번째 객채타입을 추출하도록 했다. 

- 타입 명시로 인해 정확한 데이터 타입 추론 및 코드의 안전성이 강화되었다. 

체크박스 기능 확장 

이전에는 단일 체크 박스 기능만 가지고 있었다. 

header: ({ table }) => (
  <Checkbox
    checked={
      table.getIsAllPageRowsSelected() ||
      (table.getIsSomePageRowsSelected() && "indeterminate")
    }
    onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
    aria-label="Select all"
  />
),

- 이 코드를 통해 헤더 체크 박스에서 테이블의 모든 행을 선택 및 해제할 수 있도록 하였다.

- 전체선택은 table.getIsAllPageRowsSelected로 모든 행이 선택되었는지 확인한다. 

- 부분선택은 table.getIsSomePageRowsSelected로 일부만 선택되었는지 확인한다. 

- 체크상태에 따라 전체선택 부분 선택 또는 전체 해체로 상태를 토글할 수 있다. 

cell: ({ row }) => (
  <Checkbox
    checked={row.getIsSelected()}
    onCheckedChange={(value) => row.toggleSelected(!!value)}
    aria-label="Select row"
  />
),

개별 박스 cell은 각 행의 개별 선택 및 해제가 가능하다. 

- row.getIsSelected로 현재 행이 선택되었는지 확인할 수 있다. 

- 체크박스의 변경상태에 따라 row.toggleSelected로 선택 상태를 토글할 수 있다. 

 

AccountPage

Skeleton추가 

<CardHeader>
          <Skeleton className="h-8 w-48" />
        </CardHeader>

- accountsQuery.isLoading으로 상태를 확인하여 데이터를 로드 중일 때 로더와 스켈레톤 UI를 보여준다. 

- 이는 사용자 경험이 개선되어 데이터 로드 중에도 상태를 명확하게 전달해준다. 

삭제기능 및 상태관리 

const isDisabled = accountsQuery.isLoading || deleteAccounts.isPending;

<DataTable
  ...
  onDelete={(row) => {
    const ids = row.map((r) => r.original.id);
    deleteAccounts.mutate({ ids });
  }}
  disabled={isDisabled}
/>

- deleteAccounts.isPending상태를 추가해서 삭제 작업 중에도 버튼과 Datatable의 상호작용을 비활성화 시킨다. 

- onDelete핸들러에서 삭제하려는 계정 ID목록을 추출해서 삭제 API를 호출한다. 

 

- 이는 동시에 여러 작업 (로딩/삭제)로 인해 발생할 수 있는 UI비정상 작동을 방지한다. 

- 코드의 상태 관리를 명확히 해서 유지 보수성을 높인다. 

 

Account API  bulk-delete

인증 미들웨어를 통해 사용자 검증 

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

- Clerkmiddleware를 통해 인증 정보를 미리 확인한다. 

- getAuth(c)로 인증 정보를 가져오고 auth.userId를 통해 요청 사용자를 식별한다. 

- 인증되지 않은 사용자는 401에러로 응답한다. 

요청 데이터 검증

zValidator(
  "json",
  z.object({
    ids: z.array(z.string()),
  })
);
const values = c.req.valid("json");

- zvalidator는 요청 데이터가 JSON형식인지를 검증한다. 

- ids가 반드시 문자열 배열이어야 함을 명시

- 검증에 실패하면 400에러 

삭제 조건 작성(and 와 inArray)

const data = await db
  .delete(accounts)
  .where(
    and(
      eq(accounts.userId, auth.userId),
      inArray(accounts.id, values.ids)
    )
  );

- 현재 사용자와 연관된 계정만 삭제하고 이를 통해 다른 사용자 데이터에 접근하거나 삭제하는 것을 방지한다. 

- 요청 데이터에 포함된 계정 ID만 삭제한다. 요청에 포함되지 않은 데이터는 삭제되지 않는다. 

삭제 결과 반환 

.returning({
  id: accounts.id,
});
return c.json({ data });

- 삭제된 레코드의 ID값을 반환한다. 

- 클라이언트는 이 데이터를 통해 성공적으로 삭제된 항목을 확인할 수 있다. 

 

갑자기 궁금한게 왜 .delete가 아니라 .post를 사용한걸까?

RESTful제약을 완화한 설계 

- Delete메서드는 리소스 삭제를 위해 설계되었지만 HTTP표준에서 Delete요청 본문에 데이터를 포함하는 것은 권장하지 않는다. 

- 브라우저나 일부 클라이언트 환경에서 Delete요청의 본문지원이 제한적일 수 있기 때문에 데이터를 안전하게 전달하기 위해 POST를 선택했다. 

클라이언트 제약 

일부 클라이언트나 API게이트웨이에서는 DELETE요청의 본문을 제대로 처리하지 못하는 경우가 있어 대안으로 POST를 사용하는 경우가 많다. 

 

  POST DELETE
사용 사례 본문에 데이터를 포함해야 할 때, 혹은 클라이언트 제약 시 삭제할 리소스의 식별자가 URL로 전달 가능할 때
RESTful 원칙 준수 여부 다소 제한적 RESTful 설계 원칙을 충실히 따름
클라이언트 호환성 DELETE 본문을 지원하지 않는 환경에서도 안전함 일부 환경에서 DELETE 본문 처리에 제약이 있을 수 있음
복잡한 데이터 삭제 여러 ID를 삭제하거나, 추가적인 정보가 필요한 경우 적합 단일 리소스 삭제나 URL로 간단히 식별 가능한 경우 적합

use-delete-account

request와 reponse타입

type ResponseType = InferResponseType<
  (typeof client.api.accounts)["bulk-delete"]["$post"]
>;
type RequestType = InferRequestType<
  (typeof client.api.accounts)["bulk-delete"]["$post"]
>["json"];
  • InferRequestType와 InferResponseType은 Hono 클라이언트를 활용해 서버 API의 요청(Request)과 응답(Response) 타입을 추론한다.
  • 이를 통해 서버와의 데이터 구조 불일치를 방지하며, TypeScript의 타입 안전성을 유지한다.

useMutation

const mutation = useMutation<ResponseType, Error, RequestType>({
  mutationFn: async (json) => {
    const response = await client.api.accounts["bulk-delete"]["$post"]({
      json,
    });
    return await response.json();
  },
  onSuccess: () => {
    toast.success("Account deleted");
    queryClient.invalidateQueries({ queryKey: ["accounts"] });
  },
  onError: () => {
    toast.error("Failed to delete account");
  },
});

useMutation은 서버에 데이터를 전송하거나 변경할 때 사용하는 React Query훅이다. 

 

  • mutationFn: 서버에 삭제 요청을 보내는 비동기 함수이다. 여기서는 Hono 클라이언트를 통해 /bulk-delete API를 호출한다.
  • onSuccess: 요청 성공 시 호출된다. 성공 알림을 표시하고, ["accounts"]라는 키를 가진 데이터를 무효화해 계정 목록을 최신 상태로 유지한다.
  • onError: 요청 실패 시 호출되고 실패 알림을 표시한다.

Query Client

const queryClient = useQueryClient();

 

 

  • queryClient는 React Query에서 제공하는 전역 클라이언트로, 캐싱된 데이터를 관리한다.
  • 여기서는 invalidateQueries를 사용해 특정 쿼리 키를 무효화하여 삭제 작업 이후 계정 목록이 다시 로드되도록 처리한다.

Toast알림

toast.success("Account deleted");
toast.error("Failed to delete account");

 

- sooner라이브러리를 사용해 사용자에게 성공 및 실패 메시지를 표시한다. 

- 사용자는 작업 결과를 직관적으로 확인할 수 있다.

 

useBulkDeleteAccounts 훅은 서버와의 데이터 통신, 상태 관리, 사용자 피드백이라는 주요 문제를 효율적으로 해결한다. React Query와 Hono 클라이언트를 활용한 이 접근 방식은 간결하면서도 타입 안전성을 보장하며, 확장 가능한 코드 구조를 제공한다.

 

useConfirm

promise상태 관리 

const [promise, setPromise] = useState<{
  resolve: (value: boolean) => void;
} | null>(null);

- promise는 현재 모달 상태를 관리한다. 모달이 열리면 resolve함수를 저장해서 사용자가 확인 또는 취소 버튼을 눌렀을 때 Promise를 완료한다. 

confirm함수 

const confirm = () =>
  new Promise((resolve, reject) => {
    setPromise({ resolve });
  });

- 새로운 promise를 반환하여 사용자가 선택할 때까지 대기한다. promise가 완료되면 확인 또는 취소값을 반환한다. 

 

handle함수들 

- handleClose는 모달을 닫고 상태를 초기화한다. 

- handleConfirm은 사용자가 확인 버튼을 클릭했을 때 호출된다. 

- handleCancel은 사용자가 취소 버튼을 클릭했을 때 호출되고 promise를 false로 완료한 뒤 모달을 닫는다. 

+ Recent posts