Sonamu๊ฐ ๋ฐฑ์๋ API๋ก๋ถํฐ ํ์
์์ ํ ํด๋ผ์ด์ธํธ Service๋ฅผ ์๋ ์์ฑํ๋ ๋ฐฉ์์ ์ดํดํฉ๋๋ค.
์๋ ์์ฑ Service ๊ฐ์
ํ์
์์ ์ฑ
๋ฐฑ์๋์ ๋๊ธฐํ์ปดํ์ผ ํ์ ๊ฒ์ฆ
Namespace ๊ธฐ๋ฐ
์ ์ ํจ์๊ฐ๊ฒฐํ ํธ์ถ
TanStack Query
React Hook ์๋ ์์ฑ์บ์ฑ๊ณผ ์ฌ๊ฒ์ฆ
Subset ์ง์
ํ์ํ ํ๋๋ง์ฑ๋ฅ ์ต์ ํ
์ ์๋ ์์ฑ์ธ๊ฐ?
๋ฌธ์ : ์๋ API ํด๋ผ์ด์ธํธ์ ํ๊ณ
์ ํต์ ์ธ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์์๋ ๋ฐฑ์๋ API๋ฅผ ํธ์ถํ๊ธฐ ์ํด ์๋์ผ๋ก ํด๋ผ์ด์ธํธ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.
์๋ ํด๋ผ์ด์ธํธ ์์:
// โ ์๋ ์์ฑ - ๋ฌธ์ ๊ฐ ๋ง์
async function getUser(userId: number) {
const response = await axios.get(`/api/user/${userId}`);
return response.data;
}
async function updateUser(userId: number, data: any) {
const response = await axios.put(`/api/user/${userId}`, data);
return response.data;
}
์ด ๋ฐฉ์์ ๋ฌธ์ ์ :
- ํ์
์์ ์ฑ ๋ถ์ฌ:
any ํ์
๋จ๋ฐ, ๋ฐํ์ ์๋ฌ ๋ฐ์
- ๋ฐฑ์๋ ๋ณ๊ฒฝ ์ถ์ ๋ถ๊ฐ: API๊ฐ ๋ณ๊ฒฝ๋์ด๋ ํ๋ก ํธ์๋๋ ๋ชจ๋ฆ
- ์ค๋ณต ์ฝ๋: ๋ชจ๋ API๋ง๋ค ๋น์ทํ ์ฝ๋ ๋ฐ๋ณต
- ์ค์ ๊ฐ๋ฅ์ฑ: URL ์คํ, ์๋ชป๋ ํ๋ผ๋ฏธํฐ ๋ฑ
- ์ ์ง๋ณด์ ์ด๋ ค์: API ๋ณ๊ฒฝ ์ ๋ชจ๋ ํธ์ถ ์ง์ ์์ ํ์
ํด๊ฒฐ: ์๋ ์์ฑ์ ์ฅ์
Sonamu๋ ๋ฐฑ์๋์ @api ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ถ์ํ์ฌ ํ์
์์ ํ ํด๋ผ์ด์ธํธ๋ฅผ ์๋ ์์ฑํฉ๋๋ค.
์๋ ์์ฑ ํด๋ผ์ด์ธํธ ์์:
// โ
์๋ ์์ฑ - ํ์
์์ (Namespace ๊ธฐ๋ฐ)
export namespace UserService {
// Subset์ผ๋ก ํ์ํ ํ๋๋ง ์กฐํ
export async function getUser<T extends UserSubsetKey>(
subset: T,
id: number
): Promise<UserSubsetMapping[T]> {
return fetch({
method: "GET",
url: `/api/user/findById?${qs.stringify({ subset, id })}`,
});
}
export async function updateUser(
id: number,
params: { username?: string; email?: string }
): Promise<User> {
return fetch({
method: "PUT",
url: `/api/user/update`,
data: { id, ...params },
});
}
}
์ฅ์ :
- โจ ์์ ํ ํ์
์์ ์ฑ: ๋ฐฑ์๋ ํ์
์ด ํ๋ก ํธ์๋์ ๊ทธ๋๋ก ๋ฐ์
- โจ ์ฆ๊ฐ์ ์ธ ์๋ฌ ๋ฐ๊ฒฌ: ์ปดํ์ผ ํ์์ API ๋ณ๊ฒฝ ๊ฐ์ง
- โจ ์๋ ์์ฑ: IDE๊ฐ API ํ๋ผ๋ฏธํฐ์ ์๋ต ํ์
์๋ ์ ์
- โจ Namespace ๊ธฐ๋ฐ: ๊น๋ํ ๊ตฌ์กฐ, import ๊ฐํธ
- โจ ๋จ์ผ ์ง์ค ๊ณต๊ธ์: ๋ฐฑ์๋๊ฐ API ๋ช
์ธ์ ์ ์ผํ ์์ค
**๋จ์ผ ์ง์ค ๊ณต๊ธ์ (Single Source of Truth)**์ด๋ ์์คํ
์ ๋ชจ๋ ์ ๋ณด๊ฐ ํ๋์ ์์ค์์ ํ์๋๋ ์์น์
๋๋ค. Sonamu์์๋ ๋ฐฑ์๋์ @api ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ์ ์ผํ API ๋ช
์ธ์ด๋ฉฐ, ํ๋ก ํธ์๋๋ ์ด๋ฅผ ๋ฐ๋ผ๊ฐ๋๋ค.
Service ์์ฑ ๊ณผ์
1๋จ๊ณ: ๋ฐฑ์๋ API ์ ์
๋ฐฑ์๋์์ @api ๋ฐ์ฝ๋ ์ดํฐ๋ก API๋ฅผ ์ ์ํฉ๋๋ค.
// backend/models/user.model.ts
import { BaseModelClass, api } from "sonamu";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
}
/**
* ์ฌ์ฉ์ ํ๋กํ ์กฐํ
*/
@api({ httpMethod: "GET" })
async getProfile(userId: number): Promise<{
user: {
id: number;
email: string;
username: string;
createdAt: Date;
};
}> {
const rdb = this.getPuri("r");
const user = await rdb
.table("users")
.where("id", userId)
.first();
return { user };
}
/**
* ์ฌ์ฉ์ ํ๋กํ ์์
*/
@api({ httpMethod: "PUT", guards: ["user"] })
async updateProfile(params: {
username?: string;
bio?: string;
}): Promise<{
user: {
id: number;
username: string;
bio: string;
};
}> {
// ๊ตฌํ...
}
}
์ด API ์ ์๊ฐ ๋ชจ๋ ๊ฒ์ ์์์ ์
๋๋ค. ํ์
์ ๋ณด, ํ๋ผ๋ฏธํฐ, ์๋ต ํ์ ๋ชจ๋ ์ฌ๊ธฐ์ ์ ์๋์ด ์์ต๋๋ค.
2๋จ๊ณ: TypeScript AST ํ์ฑ
Sonamu๋ TypeScript ์ปดํ์ผ๋ฌ API๋ฅผ ์ฌ์ฉํ์ฌ ์ฝ๋๋ฅผ ๋ถ์ํฉ๋๋ค.
๋ถ์ ๊ณผ์ :
// Sonamu์ ๋ด๋ถ ๋์ (์์ฌ ์ฝ๋)
const sourceFile = ts.createSourceFile(
"user.model.ts",
fileContent,
ts.ScriptTarget.Latest
);
// @api ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ์๋ ๋ฉ์๋ ์ฐพ๊ธฐ
const apiMethods = findDecorators(sourceFile, "api");
for (const method of apiMethods) {
const apiInfo = {
name: method.name.text, // "getProfile"
httpMethod: getDecoratorOption("httpMethod"), // "GET"
path: `/api/user/${method.name.text}`, // "/api/user/getProfile"
parameters: extractParameters(method), // [{ name: "userId", type: "number" }]
returnType: extractReturnType(method), // Promise<{ user: User }>
};
// Service ์ฝ๋ ์์ฑ
generateServiceMethod(apiInfo);
}
ํต์ฌ ๊ฐ๋
:
- AST (Abstract Syntax Tree): ์ฝ๋๋ฅผ ํธ๋ฆฌ ๊ตฌ์กฐ๋ก ํํํ ๊ฒ
- ํ์
์ถ์ถ: TypeScript์ ํ์
์์คํ
์์ ์ ํํ ํ์
์ ๋ณด ํ๋
- ๋ฉํ๋ฐ์ดํฐ ์์ง: ๋ฐ์ฝ๋ ์ดํฐ ์ต์
, Guards, ์ฃผ์ ๋ฑ ๋ชจ๋ ์ ๋ณด ์์ง
3๋จ๊ณ: Namespace Service ์์ฑ
์์ง๋ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก Namespace ๊ธฐ๋ฐ Service๋ฅผ ์์ฑํฉ๋๋ค.
์์ฑ๋๋ ์ฝ๋ (services.generated.ts):
import qs from "qs";
// Subset ํ์
์ ์
export type UserSubsetKey = "A" | "B" | "C";
// Subset ๋ณ ํ์
๋งคํ
export type UserSubsetMapping = {
A: { id: number; email: string; username: string }; // ๊ธฐ๋ณธ ํ๋
B: { id: number; email: string; username: string; bio: string }; // + bio
C: User; // ์ ์ฒด ํ๋ (createdAt, updatedAt ๋ฑ ํฌํจ)
};
/**
* User Service Namespace
*
* ๋ชจ๋ User ๊ด๋ จ API ํธ์ถ์ ๋ด๋นํ๋ namespace์
๋๋ค.
*/
export namespace UserService {
/**
* ์ฌ์ฉ์ ์กฐํ (Subset ์ง์)
*
* @param subset - ์กฐํํ ํ๋ ๋ฒ์ ("A" | "B" | "C")
* @param id - ์ฌ์ฉ์ ID
* @returns Subset์ ๋ฐ๋ฅธ ํ์
์์ ํ ์ฌ์ฉ์ ์ ๋ณด
*/
export async function getUser<T extends UserSubsetKey>(
subset: T,
id: number
): Promise<UserSubsetMapping[T]> {
// qs.stringify๋ก ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ์ง๋ ฌํ
return fetch({
method: "GET",
url: `/api/user/findById?${qs.stringify({ subset, id })}`,
});
}
/**
* ์ฌ์ฉ์ ํ๋กํ ์์
*
* @param params - ์์ ํ ํ๋
* @returns ์์ ๋ ์ฌ์ฉ์ ์ ๋ณด
*/
export async function updateProfile(params: {
username?: string;
bio?: string;
}): Promise<{
user: {
id: number;
username: string;
bio: string;
};
}> {
return fetch({
method: "PUT",
url: "/api/user/updateProfile",
data: params, // POST/PUT์ body๋ก ์ ์ก
});
}
}
Namespace ๊ตฌ์กฐ์ ์ฅ์ :
- ๊ฐ๊ฒฐํจ: ํด๋์ค๋ณด๋ค ๊ฐ๋จ (new ๋ถํ์)
- ์ ์ ๋ฉ์๋: ์ํ ๊ด๋ฆฌ ๋ถํ์
- Tree-shaking: ์ฌ์ฉํ์ง ์๋ ํจ์๋ ๋ฒ๋ค์์ ์ ์ธ
- Import ๊ฐํธ:
import { UserService } from "./services.generated"
4๋จ๊ณ: TanStack Query Hook ์์ฑ
React์์ ๋ฐ๋ก ์ฌ์ฉํ ์ ์๋ Hook๋ ์๋ ์์ฑ๋ฉ๋๋ค.
// services.generated.ts (๊ณ์)
import { useQuery, queryOptions } from "@tanstack/react-query";
export namespace UserService {
// ... ์์ ํจ์๋ค
/**
* TanStack Query Options
*
* queryKey์ queryFn์ ํฌํจํ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ต์
์
๋๋ค.
*/
export const getUserQueryOptions = <T extends UserSubsetKey>(
subset: T,
id: number
) =>
queryOptions({
queryKey: ["User", "getUser", subset, id],
queryFn: () => getUser(subset, id),
});
/**
* React Hook (TanStack Query)
*
* ์๋ ์บ์ฑ, ์ฌ๊ฒ์ฆ, ๋ก๋ฉ ์ํ๋ฅผ ์ ๊ณตํ๋ Hook์
๋๋ค.
*/
export const useUser = <T extends UserSubsetKey>(
subset: T,
id: number,
options?: { enabled?: boolean }
) =>
useQuery({
...getUserQueryOptions(subset, id),
...options,
});
}
TanStack Query ํตํฉ์ ์ฅ์ :
- ์๋ ์บ์ฑ
- ์๋ ์ฌ๊ฒ์ฆ
- ๋ก๋ฉ/์๋ฌ ์ํ ์๋ ๊ด๋ฆฌ
- ์กฐ๊ฑด๋ถ ํ์นญ ์ง์
- ๋๊ด์ ์
๋ฐ์ดํธ ์ง์
์์ฑ๋ Service์ ๊ตฌ์กฐ
fetch ์ ํธ๋ฆฌํฐ ํจ์
๋ชจ๋ Service๊ฐ ์ฌ์ฉํ๋ ๊ณตํต fetch ํจ์์
๋๋ค.
// sonamu.shared.ts
import axios, { AxiosRequestConfig } from "axios";
import { z } from "zod";
/**
* ๊ณตํต fetch ํจ์
*
* ๋ชจ๋ API ํธ์ถ์ด ์ด ํจ์๋ฅผ ํตํด ์ด๋ฃจ์ด์ง๋๋ค.
* Axios๋ฅผ ๋ํํ์ฌ ์๋ฌ ์ฒ๋ฆฌ์ ์๋ต ๋ณํ์ ๋ด๋นํฉ๋๋ค.
*/
export async function fetch(options: AxiosRequestConfig) {
try {
const res = await axios({
...options,
});
return res.data;
} catch (e: unknown) {
// Axios ์๋ฌ๋ฅผ SonamuError๋ก ๋ณํ
if (axios.isAxiosError(e) && e.response && e.response.data) {
const d = e.response.data as {
message: string;
issues: z.ZodIssue[];
};
throw new SonamuError(e.response.status, d.message, d.issues);
}
throw e;
}
}
/**
* Sonamu ์๋ฌ ํด๋์ค
*
* HTTP ์ํ ์ฝ๋์ Zod ์ ํจ์ฑ ๊ฒ์ฌ ์ด์๋ฅผ ํฌํจํฉ๋๋ค.
*/
export class SonamuError extends Error {
isSonamuError: boolean;
constructor(
public code: number, // HTTP ์ํ ์ฝ๋ (401, 403, 422 ๋ฑ)
public message: string, // ์๋ฌ ๋ฉ์์ง
public issues: z.ZodIssue[] // Zod ์ ํจ์ฑ ๊ฒ์ฌ ์ด์
) {
super(message);
this.isSonamuError = true;
}
}
/**
* ์๋ฌ ํ์
๊ฐ๋
*/
export function isSonamuError(e: any): e is SonamuError {
return e && e.isSonamuError === true;
}
fetch ํจ์์ ์ญํ :
- Axios ํธ์ถ ๋ํ:
options๋ฅผ Axios์ ์ ๋ฌ
- ์๋ ์๋ต ์ถ์ถ:
res.data๋ฅผ ๋ฐ๋ก ๋ฐํ
- ์๋ฌ ๋ณํ: Axios ์๋ฌ โ
SonamuError
- Zod ์ด์ ์ฒ๋ฆฌ: ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ๋ฅผ ํ์
์์ ํ๊ฒ ์ฒ๋ฆฌ
AxiosRequestConfig ํ๋ผ๋ฏธํฐ:
{
method: "GET" | "POST" | "PUT" | "DELETE",
url: string,
params?: Record<string, any>, // GET ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ
data?: any, // POST/PUT body
headers?: Record<string, string>,
}
Subset ์์คํ
Sonamu์ ๋
ํนํ ๊ธฐ๋ฅ์ธ Subset ์์คํ
์
๋๋ค.
Subset์ด๋?
์ํฐํฐ์ ์ฌ๋ฌ ๋ณํ(subset)์ ์ ์ํ์ฌ ํ์ํ ํ๋๋ง ์กฐํํ ์ ์๋ ์์คํ
์
๋๋ค.
// Subset ์ ์ (์๋ ์์ฑ)
export type UserSubsetKey = "A" | "B" | "C";
export type UserSubsetMapping = {
A: { id: number; email: string; username: string }, // ๊ธฐ๋ณธ ์ ๋ณด
B: { id: number; email: string; username: string; bio: string }, // + bio
C: User, // ์ ์ฒด ํ๋ (createdAt, updatedAt, deletedAt ๋ฑ ํฌํจ)
};
// ์ฌ์ฉ ์์
const basicUser = await UserService.getUser("A", 123);
// ํ์
: { id: number; email: string; username: string }
const fullUser = await UserService.getUser("C", 123);
// ํ์
: User (์ ์ฒด ํ๋)
Subset์ ์ฅ์ :
- ์ฑ๋ฅ: ํ์ํ ํ๋๋ง ์กฐํํ์ฌ ๋คํธ์ํฌ ๋น์ฉ ์ ๊ฐ
- ํ์
์์ : ๊ฐ Subset๋ง๋ค ์ ํํ ํ์
๋ฐํ
- ๋ช
์์ฑ: ์ด๋ค ๋ฐ์ดํฐ๊ฐ ํ์ํ์ง ์ฝ๋์์ ๋ช
ํํ ํํ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ต์ ํ: SELECT ์ ์ ํ์ํ ์ปฌ๋ผ๋ง ํฌํจ
Subset ๋ค์ด๋ฐ ๊ท์น:
- A: ๊ธฐ๋ณธ ํ๋ (id, ํต์ฌ ์ ๋ณด)
- B: ์ค๊ฐ ํ๋ (A + ์ถ๊ฐ ์ ๋ณด)
- C: ์ ์ฒด ํ๋ (๋ชจ๋ ์ปฌ๋ผ, ํ์์คํฌํ ํฌํจ)
ํ์
์์ ์ฑ์ ์ค์
์ปดํ์ผ ํ์ ๊ฒ์ฆ
๋ฐฑ์๋ API๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์ปดํ์ผ ํ์์ ์ฆ์ ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค.
๋ฐฑ์๋ ๋ณ๊ฒฝ:
// ๋ฐฑ์๋์์ username -> displayName์ผ๋ก ๋ณ๊ฒฝ
@api({ httpMethod: "PUT" })
async updateProfile(params: {
displayName?: string; // username์์ ๋ณ๊ฒฝ๋จ
bio?: string;
}): Promise<{ user: User }> {
// ...
}
ํ๋ก ํธ์๋ ์๋ฌ:
// pnpm generate ํ ์๋์ผ๋ก Service ํ์
์
๋ฐ์ดํธ๋จ
// โ ์ปดํ์ผ ์๋ฌ ๋ฐ์!
await UserService.updateProfile({
username: "newname", // Error: 'username' does not exist in type
});
// โ
์์ ํ ์ ์ ๋์
await UserService.updateProfile({
displayName: "newname", // OK
});
์ด๋ ๋ฐํ์ ์๋ฌ๋ฅผ ์ปดํ์ผ ํ์์ผ๋ก ๋์ด์ฌ๋ ค ๋ฒ๊ทธ๋ฅผ ์ฌ์ ์ ๋ฐฉ์งํฉ๋๋ค.
IDE ์๋ ์์ฑ
ํ์
์ ๋ณด ๋๋ถ์ IDE๊ฐ ๊ฐ๋ ฅํ ์๋ ์์ฑ์ ์ ๊ณตํฉ๋๋ค.
// ํ์
์ ์
๋ ฅํ๋ฉด...
UserService.up // IDE๊ฐ "updateProfile" ์๋ ์ ์
// ํ๋ผ๋ฏธํฐ๋ฅผ ์
๋ ฅํ๋ฉด...
await UserService.updateProfile({
// IDE๊ฐ ๊ฐ๋ฅํ ํ๋๋ฅผ ๋ชจ๋ ์ ์:
// - displayName?: string
// - bio?: string
});
// Subset๋ ์๋ ์์ฑ
await UserService.getUser(
"A" // IDE๊ฐ "A" | "B" | "C" ์ ์
, 123
);
๊ฐ๋ฐ ์ํฌํ๋ก์ฐ
๋ฐฑ์๋ ์ฐ์ ๊ฐ๋ฐ
Sonamu์ ์๋ ์์ฑ์ ๋ฐฑ์๋ ์ฐ์ (Backend-First) ๊ฐ๋ฐ์ ๊ถ์ฅํฉ๋๋ค.
์ผ๋ฐ์ ์ธ ์ํฌํ๋ก์ฐ:
์ฅ์ :
- ๋ฐฑ์๋์ ํ๋ก ํธ์๋์ ๊ณ์ฝ(Contract)์ด ๋ช
ํ
- ํ์
๋ถ์ผ์น๋ก ์ธํ ๋ฒ๊ทธ ์์ฒ ์ฐจ๋จ
- API ๋ฌธ์ ์๋ ์์ฑ (Service๊ฐ ๊ณง ๋ฌธ์)
- ํ์
ํจ์จ์ฑ ํฅ์
๊ฐ๋ฐ ์ ์ฌ์์ฑ
API๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค Service๋ฅผ ์ฌ์์ฑํด์ผ ํฉ๋๋ค.
# Service ์ฌ์์ฑ
pnpm generate
# ๋๋ watch ๋ชจ๋๋ก ์๋ ์ฌ์์ฑ
pnpm generate:watch
์ฃผ์์ฌํญ:
- ์์ฑ๋ Service ํ์ผ(
services.generated.ts)์ ์ ๋ ์๋ ์์ ๊ธ์ง
- ์์ ์ด ํ์ํ๋ฉด ๋ฐฑ์๋์์ ์์ ํ ์ฌ์์ฑ
- ์์ฑ ํ์ผ์
.gitignore์ ์ถ๊ฐํ ์ง ํ์์ ๊ฒฐ์
- ์ถ๊ฐํ๋ฉด: ๊ฐ์ ๋ก์ปฌ์์ ์์ฑ
- ์ถ๊ฐ ์ํ๋ฉด: Git์ผ๋ก ๊ณต์ (๋น๋ ์๊ฐ ๋จ์ถ)
์ค์ ์ฌ์ฉ ์์
๊ธฐ๋ณธ ์ฌ์ฉ
import { UserService } from "@/services/services.generated";
// Subset "A"๋ก ๊ธฐ๋ณธ ์ ๋ณด๋ง ์กฐํ
const user = await UserService.getUser("A", 123);
console.log(user.username); // OK
console.log(user.bio); // โ ์ปดํ์ผ ์๋ฌ (Subset A์ bio ์์)
// Subset "B"๋ก bio ํฌํจ ์กฐํ
const userWithBio = await UserService.getUser("B", 123);
console.log(userWithBio.bio); // OK
// ํ๋กํ ์์
await UserService.updateProfile({
username: "newname",
bio: "Hello, World!",
});
React์์ ์ฌ์ฉ (TanStack Query Hook)
import { UserService } from "@/services/services.generated";
function UserProfile({ userId }: { userId: number }) {
// ์๋ ์์ฑ๋ Hook ์ฌ์ฉ
const { data: user, isLoading, error } = UserService.useUser("A", userId);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.username}</h1>
<p>{user.email}</p>
</div>
);
}
์กฐ๊ฑด๋ถ ํ์นญ
function UserProfile({ userId }: { userId: number | null }) {
const { data: user } = UserService.useUser(
"A",
userId!, // TypeScript non-null assertion
{ enabled: userId !== null } // userId๊ฐ null์ด๋ฉด ํธ์ถ ์ํจ
);
if (!userId) return <div>Please select a user</div>;
return <div>{user?.username}</div>;
}
๊ณ ๊ธ ๊ธฐ๋ฅ
qs.stringify ์ฌ์ฉ
GET ์์ฒญ์ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ฅผ ์ง๋ ฌํํ ๋ qs ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
import qs from "qs";
// ๋ณต์กํ ๊ฐ์ฒด๋ ์ฟผ๋ฆฌ ์คํธ๋ง์ผ๋ก ๋ณํ
const queryString = qs.stringify({
subset: "A",
id: 123,
filters: { status: "active" }
});
// "subset=A&id=123&filters[status]=active"
// Service์์ ์ฌ์ฉ
export async function getUser<T extends UserSubsetKey>(
subset: T,
id: number
): Promise<UserSubsetMapping[T]> {
return fetch({
method: "GET",
url: `/api/user/findById?${qs.stringify({ subset, id })}`,
});
}
qs๋ฅผ ์ฌ์ฉํ๋ ์ด์ :
- ์ค์ฒฉ ๊ฐ์ฒด ์ง์ (
filters[status]=active)
- ๋ฐฐ์ด ์ง๋ ฌํ ์ง์ (
ids[]=1&ids[]=2)
- ๋ฐฑ์๋์ ํ์ฑ ๋ฐฉ์๊ณผ ์ผ์น
์๋ฌ ์ฒ๋ฆฌ
SonamuError๋ฅผ ํ์
์์ ํ๊ฒ ์ฒ๋ฆฌํฉ๋๋ค.
import { UserService } from "@/services/services.generated";
import { isSonamuError } from "@/lib/sonamu.shared";
try {
await UserService.updateProfile({
username: "newname",
});
} catch (error) {
if (isSonamuError(error)) {
// Sonamu ์๋ฌ
console.log("Status:", error.code);
console.log("Message:", error.message);
console.log("Validation Issues:", error.issues);
// Zod ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ์ฒ๋ฆฌ
error.issues.forEach((issue) => {
console.log(`${issue.path.join(".")}: ${issue.message}`);
});
} else {
// ์ผ๋ฐ ์๋ฌ
console.error(error);
}
}
Query Options ์ฌ์ฌ์ฉ
import { UserService } from "@/services/services.generated";
import { useQueryClient } from "@tanstack/react-query";
function SomeComponent() {
const queryClient = useQueryClient();
async function handleUpdate() {
// ์์ ํ ์บ์ ๋ฌดํจํ
await UserService.updateProfile({ username: "newname" });
// Query Options๋ก ํน์ ์ฟผ๋ฆฌ ๋ฌดํจํ
queryClient.invalidateQueries(
UserService.getUserQueryOptions("A", 123)
);
}
}
Prefetching
import { UserService } from "@/services/services.generated";
import { useQueryClient } from "@tanstack/react-query";
function UserList({ userIds }: { userIds: number[] }) {
const queryClient = useQueryClient();
// ํธ๋ฒ ์ ๋ฏธ๋ฆฌ ๋ก๋
function handleMouseEnter(userId: number) {
queryClient.prefetchQuery(
UserService.getUserQueryOptions("A", userId)
);
}
return (
<ul>
{userIds.map((id) => (
<li
key={id}
onMouseEnter={() => handleMouseEnter(id)}
>
User {id}
</li>
))}
</ul>
);
}
์ฃผ์์ฌํญ
Service ์ฌ์ฉ ์ ์ฃผ์์ฌํญ:
- ์์ฑ๋ Service ํ์ผ(
services.generated.ts)์ ์ ๋ ์๋ ์์ ๊ธ์ง
- Subset ํ๋ผ๋ฏธํฐ ํ์:
getUser("A", id) ์ฒ๋ผ subset ์ง์ ํ์
- Namespace์ด๋ฏ๋ก new ๋ถํ์:
UserService.getUser() ์ง์ ํธ์ถ
- TanStack Query Hook์ ์ปดํฌ๋ํธ ๋ด๋ถ์์๋ง ํธ์ถ
- ์๋ฌ ์ฒ๋ฆฌ ์
isSonamuError() ํ์
๊ฐ๋ ์ฌ์ฉ
qs.stringify()๋ ๋ณต์กํ ๊ฐ์ฒด ์ง๋ ฌํ ์ ์ฌ์ฉ
๋ค์ ๋จ๊ณ