계정 조회하기
.GET
GET/:id엔드포인트는 다음과 같은 기능들을 제공한다.
1. URL파라미터 유효성 검증은 id가 유요한 형식인지 확인한다.
2. 요청자가 유효한 사용자인지 확인한다.
3. 데이터베이스를 조회하여 특정 사용자가 소유한 계정을 검색한다.
4. 요청 결과에 따라 적절한 HTTP응답을 반환한다.
URL파라미터 검증
.get(
"/:id",
zValidator(
"param",
z.object({
id: z.string().optional(),
})
),
clerkMiddleware(),
async (c) => {
const auth = getAuth(c);
const { id } = c.req.valid("param");
- zValidator는 Zod라이브러리를 사용하여 요청된 파라미터를 검증한다.
- z.object({ id: z.string().optional()})는 id는 선택적인 문자열로 정의되어 있고 만약 id가 제공되지 않았거나 잘못된 형식이라면 요청은 실패하게 된다.
- c.req.valid("param")은 검증된 파라미터를 반환한다.
if (!id) {
return c.json({ error: "Missing id" }, 400);
}
- id가 없으면 400에러를 반환한다.
인증처리 (ClekrMiddleware와 getAuth)
const auth = getAuth(c);
if (!auth?.userId) {
return c.json({ error: "Unauthorized" }, 401);
}
- clerkMiddleware는 요청에 포함된 인증 정보를 처리한다.
- getAuth는 인증 정보를 가져온다. auth?.userID가 존재하지 않으면 401응답을 반환한다.
- 이를 통해 사용자 인증 여부를 확인한다.
데이터 베이스 조회
const [data] = await db
.select({
id: accounts.id,
name: accounts.name,
})
.from(accounts)
.where(and(eq(accounts.userId, auth.userId), eq(accounts.id, id)));
- db.select는 데이터베이스에서 계정 정보를 조회한다. 선택한 필드는 id와 name
- where조건은 eq(accounts.userId, auth.userId)는 요청한 사용자가 소유한 계정인지 확인하고 eq(accounts.id, id)는 요청된 id와 일치하는 계정을 검색하낟.
- and조건은 두 조건을 동시에 만족하는 데이터를 필터링한다.
검색결과처리
- data가 없는 경우는 요청한 계정이 존재하지 않거나 사용자가 소유하지 않은경우이다.
- 클라이언트에 404에러를 반환한다.
useGetAccount
React Query 기반으로 만들어진 이 훅은 특정 계정의 정보를 가져올 수 있도록 도와준다. 이 훅은 서버에서 데이터를 비동기적으로 가져오고 React Query의 강력한 기능을 활용해 상태를 관리하며 간단한 API호출을 가능하게 해준다.
useQuery설정
const query = useQuery({
enabled: !!id,
queryKey: ["account", { id }],
queryFn: async () => {
const response = await client.api.accounts[":id"].$get({
param: { id },
});
- useQuery는 ReactQeury에서 데이터 fetching과 캐싱을 관리하는 훅이ㅏㄷ.
- queryKey는 쿼리를 식별하는 키로 캐싱과 데이터 리패칭을 관리한다. ["account",{id}]는 계정 데이터를 특정 ID로 구분하도록 설정한다.
- enabled:!!id는 id가 존재할때만 쿼리를 활성화한다. !!id는 id가 null또는 undefined인 경우 쿼리를 비활성화하여 불필요한 요청을 방지한다.
API호출
const response = await client.api.accounts[":id"].$get({
param: { id },
});
- client.api.accounts[":id"].$get에서 :id는 동적 경로를 나타내며 API요청시 {id}를 URL에 동적으로 주입한다.
- param:{id}는 API요청에서 필요한 URL파라미터를 설정한다.
응답처리
if (!response.ok) {
throw new Error("Failed to fetch accounts");
}
const { data } = (await response.json()) as {
data: { id: string; name: string };
};
return data;
- HTTP상태는 response.ok코드로 확인한다. 응답상태가 200~299범위에 속하지 않으면 오류를 발생시킨다. 이를 통해 API호출 실패 시 적절한 예외 처리가 가능하다.
- response.json()은 응답 데이터를 JSON으로 변환하여 사용한다.
- as { data: { id: string; name: string } }를 통해 응답 데이터 구조를 명확하게 지정한다.
( 내가 여기서 좀 애를 좀 먹었다.. 타입 지정을 못한 탓에 data에서 계속 오류가 났었다)
동작흐름
1. 쿼리 활성화 여부 확인 : id가 없는 경우, AP호출이 비활성화된다.
2. API호출: id를 사용해 서버에서 특정 계정 정보를 요청한하고 응답 데이터를 JSON으로 변환하고, 타입 단언을 통해 데이터 구조를 명확히 지정한다.
3. React Qeury상태 관리: 데이터 로드 성공 실패 상태를 관리하며 이를 반환한다.
계정 수정하기
.PATCH
이 코드는 patch 요청을 처리하는 엔드포인트를 구현하여 클라이언트가 특정 계정 정보를 업데이터 할 수 있도록 하는것이다.
전체적인 맬락은 다른 메소드와 비슷하다.
데이터 베이스 업데이트
const [data] = await db
.update(accounts)
.set(values)
.where(and(eq(accounts.userId, auth.userId), eq(accounts.id, id)))
.returning();
데이터 베이스를 다루는 부분에서 업데이트 기능을 가져올 수 있다.
- db.update()는 accounts테이블에서 데이터를 업데이트하는 SQL쿼리를 실행한다.
- set(values)는 요청 본문에서 받은 값을 업데이트한다.
- 해당 내용의 조건은 userId와 id가 모두 일치하는 계정을 찾아 업데이트 하는 것이다.
- returning()을 통해서 업데이트된 데이터를 반환한다.
- 그리고 반환된 데이터의 첫번째 항목을 data에 저장한다.
useEditAccount
타입정의
type ResponseType = InferResponseType<
(typeof client.api.accounts)[":id"]["$patch"]
>;
type RequestType = InferRequestType<typeof client.api.accounts.$post>["json"];
ResponseType
- InferResponseType을 사용해 서버에서 반환할 응답 타입을 추론한다.
- client.api.account..[$patch]는 PATCH요청에 대한 타입 정보를 제공한다.
- ResponseType은 서버에서 반환되는 데이터 형식을 나타낸다.
RequestType
- InferRequestType을 사용해 클라이언트에서 보낼 요청 데이터 타입을 추론한다.
- client.api.accounts.$post의 요청 타입을 기반으로 설정하며, json 형태로 전송된다.
- 이 타입은 업데이트 요청에서 필요한 필드들(예: { name: string; })을 정의한다.
ReactQeury 클라이언트 초기화
const queryClient = useQueryClient();
- queryClient는 React Qeury의 캐시관리를 담당하는 객체이다.
- 이를 통해 기존 데이터를 갱신하거나 삭제할 수 있다.
useMutation훅
const mutation = useMutation<ResponseType, Error, RequestType>({
mutationFn: async (json) => {
const response = await client.api.accounts[":id"]["$patch"]({
param: { id },
json,
});
return await response.json();
},
...
});
- useMutation: React Query에서 데이터를 수정하거나 삭제할 때 사용되는 훅이다. useMutation은 데이터를 변경하는 비동기 작업을 처리하고, 성공/실패 후 작업을 정의할 수 있게 해준다.
- 제네릭타입 : responsetype은 서버 응답, error는 에러타입, requesttype은 요청 데이터 타입
- mutationFn은 실제 데이터를 수정하는 비동기 함수이다. client.api.accounts...을 호출해서 서버에 PATCH요청을 보낸다. param은 경로 파라미터로 계정 ID를 전달한다. json은 요청 본문으로 수정하려는 데이터를 전달한다.
- 요청 후에는 response.json()을 호출해 응답 데이터를 반환한다.
성공시 동작
onSuccess: () => {
toast.success("Account updated");
queryClient.invalidateQueries({ queryKey: ["accounts", { id }] });
queryClient.invalidateQueries({ queryKey: ["accounts"] });
},
- 성공 알림 : toast.success("Account updated")는 수정 성공 메시지를 사용자에게 표시한다.
- 캐시 무효화는 queryClient.invalidateQueries를 호출하여 React Qeury의 캐시를 무효화한다.
또 "accounts",{id}는 수정된 특정 계정 데이터를 다시 가져오고 accounts는 계정 목록 데이터를 다시가져온다.
실패시 동작
onError: () => {
toast.error("Failed to edit account");
},
- 실패하면 toast를 통해 에러메시지가 표시된다.
반환값
return mutation;
mutation 객체는 useMutation에서 제공하는 메서드와 상태 값을 포함한다.
- mutation.mutate(data): 데이터를 수정하는 요청을 트리거한다.
- mutation.isLoading: 요청이 진행 중인지 확인한다.
- mutation.isError: 요청이 실패했는지 확인한다.
- mutation.isSuccess: 요청이 성공했는지 확인한다.
계정 삭제하기
.delete
.delete(
"/:id",
clerkMiddleware(),
zValidator(
"param",
z.object({
id: z.string().optional(),
})
),
zValidator(
"json",
insertAccountSchema.pick({
name: true,
})
),
async (c) => {
const auth = getAuth(c);
const { id } = c.req.valid("param");
if (!id) {
return c.json({ error: "Missing id" }, 400);
}
if (!auth?.userId) {
return c.json({ error: " Unauthorized" }, 401);
}
const [data] = await db
.delete(accounts)
.where(and(eq(accounts.userId, auth.userId), eq(accounts.id, id)))
.returning({
id: accounts.id,
});
if (!data) {
return c.json({ error: "Not Found" }, 404);
}
return c.json({ data });
}
);
export default app;
이것 도 결국 위에서 한 내용과 똑같다.
db에서 .delete하면 된다.
useDeleteAccount
import { toast } from "sonner";
import { InferResponseType } from "hono";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { client } from "@/lib/hono";
type ResponseType = InferResponseType<
(typeof client.api.accounts)[":id"]["$delete"]
>;
export const useDeleteAccount = (id?: string) => {
const queryClient = useQueryClient();
const mutation = useMutation<ResponseType, Error>({
mutationFn: async () => {
const response = await client.api.accounts[":id"]["$delete"]({
param: { id },
json: { name: "" },
});
return await response.json();
},
onSuccess: () => {
toast.success("Account deleted");
queryClient.invalidateQueries({ queryKey: ["account", { id }] });
queryClient.invalidateQueries({ queryKey: ["accounts"] });
},
onError: () => {
toast.error("Failed to delete account");
},
});
return mutation;
};
이것도 path에서 했던 방식과 동일하게 적용되었다.