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로 완료한 뒤 모달을 닫는다.