์์ฑ๋ Namespace Service๋ฅผ ์ฌ์ฉํ์ฌ ํ์
์์ ํ๊ฒ API๋ฅผ ํธ์ถํ๋ ๋ฐฉ๋ฒ์ ์์๋ด
๋๋ค.
Service ์ฌ์ฉ ๊ฐ์
Namespace ํธ์ถ
UserService.getUser()๊ฐ๊ฒฐํ ๋ฌธ๋ฒ
ํ์
์์
์๋ ์์ฑ์ปดํ์ผ ๊ฒ์ฆ
Subset ์ง์
ํ์ํ ํ๋๋ง์ฑ๋ฅ ์ต์ ํ
TanStack Query
useUser Hook์๋ ์บ์ฑ
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
Service Import
์์ฑ๋ Service๋ services.generated.ts์์ Namespace๋ก export๋ฉ๋๋ค.
// โ
์ฌ๋ฐ๋ฅธ import ๋ฐฉ์
import { UserService, PostService } from "@/services/services.generated";
๋จ์ผ ํ์ผ import์ ์ฅ์ :
- ์ผ๊ด์ฑ: ๋ชจ๋ Service๊ฐ ํ ๊ณณ์์ ๊ด๋ฆฌ๋จ
- ๊ฐํธํ import: ํ์ผ ๊ฒฝ๋ก ์ฐพ์ ํ์ ์์
- ์๋ ์
๋ฐ์ดํธ:
pnpm generate ์ ์๋์ผ๋ก ๋๊ธฐํ
- ๋ช
ํํ ๋ค์ด๋ฐ: Namespace๋ก ๊ทธ๋ฃนํ๋์ด ์ถฉ๋ ์์
๊ธฐ๋ณธ API ํธ์ถ
Service์ ์ ์ ํจ์๋ฅผ ์ง์ ํธ์ถํฉ๋๋ค.
import { UserService } from "@/services/services.generated";
// ์ฌ์ฉ์ ์กฐํ (Subset "A"๋ก ๊ธฐ๋ณธ ์ ๋ณด๋ง)
const user = await UserService.getUser("A", 123);
console.log(user.username); // ํ์
์์ !
// ์ฌ์ฉ์ ์์
await UserService.updateProfile({
username: "newname",
bio: "Hello, World!",
});
ํน์ง:
await์ผ๋ก ๋น๋๊ธฐ ํธ์ถ
- ์๋ต์ ์๋์ผ๋ก ํ์ฑ๋จ (fetch ํจ์ ๋ด๋ถ ์ฒ๋ฆฌ)
- ํ์
์ด ์์ ํ ๋ณด์ฅ๋จ
- Subset ํ๋ผ๋ฏธํฐ ํ์ (getUser ๋ฑ)
Namespace ๊ธฐ๋ฐ ๊ตฌ์กฐ:
- โ ํด๋์ค ์ธ์คํด์ค:
new UserService() ๋ถํ์
- โ
์ ์ ํจ์:
UserService.getUser() ์ง์ ํธ์ถ
- ๋ชจ๋ ํจ์๊ฐ ๋
๋ฆฝ์ ์ผ๋ก ๋์
Subset ์์คํ
์ดํดํ๊ธฐ
Sonamu์ ํต์ฌ ๊ธฐ๋ฅ์ธ Subset ์์คํ
์
๋๋ค.
// Subset "A": ๊ธฐ๋ณธ ์ ๋ณด
const basicUser = await UserService.getUser("A", 123);
console.log(basicUser.id); // โ
OK
console.log(basicUser.username); // โ
OK
console.log(basicUser.bio); // โ ์ปดํ์ผ ์๋ฌ (Subset A์ ์์)
// Subset "B": bio ํฌํจ
const userWithBio = await UserService.getUser("B", 123);
console.log(userWithBio.bio); // โ
OK
// Subset "C": ์ ์ฒด ํ๋
const fullUser = await UserService.getUser("C", 123);
console.log(fullUser.createdAt); // โ
OK
Subset์ ์ฌ์ฉํ๋ ์ด์ :
- ์ฑ๋ฅ: ํ์ํ ํ๋๋ง ์กฐํํ์ฌ ๋คํธ์ํฌ ๋น์ฉ ์ ๊ฐ
- ํ์
์์ : ๊ฐ Subset๋ง๋ค ์ ํํ ํ์
๋ฐํ
- ๋ช
์์ฑ: ์ด๋ค ๋ฐ์ดํฐ๊ฐ ํ์ํ์ง ์ฝ๋์์ ๋ช
ํํ ํํ
React์์ ์ฌ์ฉํ๊ธฐ
TanStack Query Hook (๊ถ์ฅ)
๊ฐ์ฅ ๊ฐํธํ ๋ฐฉ๋ฒ์ ์๋ ์์ฑ๋ TanStack Query Hook์ ์ฌ์ฉํ๋ ๊ฒ์
๋๋ค.
import { UserService } from "@/services/services.generated";
function UserProfile({ userId }: { userId: number }) {
// ์๋ ์์ฑ๋ Hook ์ฌ์ฉ
const { data: user, error, isLoading } = 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>
);
}
TanStack Query Hook์ ์ฅ์ :
- ์๋ ์บ์ฑ (๊ฐ์ userId ์ฌ์ฌ์ฉ)
- ์๋ ์ฌ๊ฒ์ฆ (ํฌ์ปค์ค ์, ์ฌ์ฐ๊ฒฐ ์)
- ๋ก๋ฉ/์๋ฌ ์ํ ์๋ ๊ด๋ฆฌ
- ์ค๋ณต ์์ฒญ ์๋ ์ ๊ฑฐ
ํจ์ ์ปดํฌ๋ํธ (useEffect)
Hook์ ์ฌ์ฉํ์ง ์๊ณ ์ง์ ํธ์ถํ ์๋ ์์ต๋๋ค.
import { useState, useEffect } from "react";
import { UserService } from "@/services/services.generated";
import type { User } from "@/services/services.generated";
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
// Subset "C"๋ก ์ ์ฒด ํ๋ ์กฐํ
const userData = await UserService.getUser("C", userId);
setUser(userData);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load user");
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.username}</h1>
<p>{user.email}</p>
</div>
);
}
๊ถ์ฅ: ๊ฐ๋ฅํ๋ฉด TanStack Query Hook์ ์ฌ์ฉํ์ธ์. ์๋ ์บ์ฑ, ์ฌ๊ฒ์ฆ, ์ํ ๊ด๋ฆฌ๋ฅผ ๋ชจ๋ ์ ๊ณตํ์ฌ ์ฝ๋๊ฐ ํจ์ฌ ๊ฐ๊ฒฐํด์ง๋๋ค.
์ด๋ฒคํธ ํธ๋ค๋ฌ
์ฌ์ฉ์ ์ก์
์ ๋ฐ๋ผ API๋ฅผ ํธ์ถํฉ๋๋ค.
import { useState } from "react";
import { UserService } from "@/services/services.generated";
function EditProfile({ userId }: { userId: number }) {
const [username, setUsername] = useState("");
const [bio, setBio] = useState("");
const [saving, setSaving] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
try {
setSaving(true);
await UserService.updateProfile({
username,
bio,
});
alert("Profile updated!");
} catch (error) {
alert("Failed to update profile");
console.error(error);
} finally {
setSaving(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
/>
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
placeholder="Bio"
/>
<button type="submit" disabled={saving}>
{saving ? "Saving..." : "Save"}
</button>
</form>
);
}
@sonamu-kit/react-components์ useTypeForm์ ์ฌ์ฉํ๋ฉด ํ์
์์ ํ ํผ์ ๋ ๊ฐํธํ๊ฒ ์์ฑํ ์ ์์ต๋๋ค.
import { useTypeForm } from "@sonamu-kit/react-components/lib";
import { Input, Button } from "@sonamu-kit/react-components/components";
import { UserService } from "@/services/services.generated";
import { UserSaveParams } from "@/services/user/user.types";
function RegisterForm() {
// Zod ์คํค๋ง ๊ธฐ๋ฐ ํ์
์์ ํผ
const { register, submit } = useTypeForm(UserSaveParams, {
email: "",
username: "",
password: "",
});
const saveMutation = UserService.useSaveMutation();
const handleSubmit = submit(async (form) => {
saveMutation.mutate(
{ spa: [form] },
{
onSuccess: ([userId]) => {
console.log("Registered:", userId); // โ
ํ์
์์
},
onError: (error) => {
console.error("Registration failed:", error);
},
}
);
});
return (
<div>
<Input placeholder="์ด๋ฉ์ผ" {...register("email")} />
<Input placeholder="์ด๋ฆ" {...register("username")} />
<Input type="password" placeholder="๋น๋ฐ๋ฒํธ" {...register("password")} />
<Button onClick={handleSubmit} disabled={saveMutation.isPending}>
{saveMutation.isPending ? "๋ฑ๋ก ์ค..." : "๋ฑ๋ก"}
</Button>
</div>
);
}
useTypeForm์ ์ฅ์ :
- โ
ํ์
์์ : Zod ์คํค๋ง์์ ํ์
์๋ ์ถ๋ก
- โ
์๋ ์ ํจ์ฑ ๊ฒ์ฌ: ์คํค๋ง ๊ท์น ์๋ ์ ์ฉ
- โ
๊ฐ๊ฒฐํ ์ฝ๋:
useState + ์ด๋ฒคํธ ํธ๋ค๋ฌ ๋ถํ์
- โ
์๋ฌ ์ฒ๋ฆฌ: ํ๋๋ณ ์๋ฌ ๋ฉ์์ง ์๋ ๊ด๋ฆฌ
- โ
ํตํฉ ์ปดํฌ๋ํธ: Input, Button ๋ฑ ์ฆ์ ์ฌ์ฉ ๊ฐ๋ฅ
๊ถ์ฅ: ์๋ก์ด ํผ์ ์์ฑํ ๋๋ useTypeForm์ ์ฌ์ฉํ์ธ์. ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๋ฅผ ํฌ๊ฒ ์ค์ด๊ณ ํ์
์์ ์ฑ์ ๋ณด์ฅํฉ๋๋ค.
์๋ฌ ํ์ ์์ :
function RegisterFormWithErrors() {
const { register, submit, errors } = useTypeForm(UserSaveParams, {
email: "",
username: "",
password: "",
});
const saveMutation = UserService.useSaveMutation();
const handleSubmit = submit(async (form) => {
saveMutation.mutate({ spa: [form] });
});
return (
<div>
<div>
<Input placeholder="์ด๋ฉ์ผ" {...register("email")} />
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div>
<Input placeholder="์ด๋ฆ" {...register("username")} />
{errors.username && <span className="error">{errors.username.message}</span>}
</div>
<div>
<Input type="password" placeholder="๋น๋ฐ๋ฒํธ" {...register("password")} />
{errors.password && <span className="error">{errors.password.message}</span>}
</div>
<Button onClick={handleSubmit}>๋ฑ๋ก</Button>
</div>
);
}
ํจํด ๋น๊ต:
| ํจํด | ์ฝ๋๋ | ํ์
์์ | ์ ํจ์ฑ ๊ฒ์ฌ | ๊ถ์ฅ๋ |
|---|
| useTypeForm | โญ๏ธโญ๏ธโญ๏ธ ์ ์ | โ
์๋ฒฝ | โ
์๋ | โญ๏ธโญ๏ธโญ๏ธ ์ต๊ณ |
| useEffect + useState | โญ๏ธ ๋ง์ | โ ๏ธ ์๋ | โ ์๋ | โญ๏ธ ๋ฎ์ |
| ์ด๋ฒคํธ ํธ๋ค๋ฌ | โญ๏ธโญ๏ธ ๋ณดํต | โ ๏ธ ์๋ | โ ์๋ | โญ๏ธโญ๏ธ ๋ณดํต |
@sonamu-kit/react-components๋ Material-UI, Ant Design ๋ฑ ๋ค๋ฅธ UI ๋ผ์ด๋ธ๋ฌ๋ฆฌ์๋ ํจ๊ป ์ฌ์ฉํ ์ ์์ต๋๋ค.
SSR์์ Service ์ฌ์ฉํ๊ธฐ
Sonamu๋ Vite + React ๊ธฐ๋ฐ์ SSR์ ์ง์ํฉ๋๋ค. SSR์์๋ queries.generated.ts์ ์๋ ์์ฑ๋ SSR ์ ์ฉ Service๋ฅผ ์ฌ์ฉํฉ๋๋ค.
SSR Service๋?
ํ๋ก ํธ์๋ Service(services.generated.ts)์๋ ๋ณ๊ฐ๋ก, ๋ฐฑ์๋์ SSR ์ ์ฉ Service๊ฐ ์๋ ์์ฑ๋ฉ๋๋ค.
// api/src/application/queries.generated.ts (์๋ ์์ฑ)
import type { SSRQuery } from "sonamu/ssr";
export namespace UserService {
// SSRQuery ๊ฐ์ฒด๋ฅผ ๋ฐํ (HTTP ์์ฒญํ์ง ์์)
export const getUser = <T extends UserSubsetKey>(subset: T, id: number): SSRQuery =>
createSSRQuery("UserModel", "findById", [subset, id], ["User", "getUser"]);
export const me = (): SSRQuery =>
createSSRQuery("UserModel", "me", [], ["User", "me"]);
}
SSR ๋ผ์ฐํธ๋ฅผ ๋ฑ๋กํ ๋ ์ด Service๋ฅผ ์ฌ์ฉํฉ๋๋ค.
// api/src/ssr/routes.ts
import { registerSSR } from "sonamu/ssr";
import { UserService, CompanyService } from "../application/queries.generated";
// ํ์ฌ ์์ธ ํ์ด์ง SSR
registerSSR({
path: "/companies/:companyId",
preload: (params) => [
// Service ๋ฉ์๋ ํธ์ถ โ SSRQuery ๊ฐ์ฒด ๋ฐํ
UserService.me(),
CompanyService.getCompany("A", Number(params.companyId)),
],
});
ํ๋ก ํธ์๋ Service vs SSR Service
| ํญ๋ชฉ | ํ๋ก ํธ์๋ Service | SSR Service |
|---|
| ํ์ผ ์์น | web/src/services/services.generated.ts | api/src/application/queries.generated.ts |
| ๋ฐํ๊ฐ | Promise<Data> (HTTP ์์ฒญ) | SSRQuery (๊ฐ์ฒด) |
| ์ฌ์ฉ ์์น | ๋ธ๋ผ์ฐ์ (React ์ปดํฌ๋ํธ) | ๋ฐฑ์๋ (registerSSR) |
| HTTP ์์ฒญ | โ
์ค์ API ํธ์ถ | โ ๋ฐฑ์๋ Model ์ง์ ์คํ |
// ํ๋ก ํธ์๋ Service (๋ธ๋ผ์ฐ์ )
const user = await UserService.getUser("A", 123); // HTTP GET /api/user/findById
// SSR Service (๋ฐฑ์๋)
const query = UserService.getUser("A", 123); // SSRQuery ๊ฐ์ฒด ๋ฐํ
// โ Sonamu๊ฐ UserModel.findById("A", 123) ์ง์ ์คํ
ํต์ฌ ์ฐจ์ด์ : SSR Service๋ HTTP ์์ฒญ ์์ด ๋ฐฑ์๋ Model ๋ฉ์๋๋ฅผ ์ง์ ์คํํ๋ฏ๋ก ๋คํธ์ํฌ ์ค๋ฒํค๋๊ฐ ์์ต๋๋ค.
๋ ์์๋ณด๊ธฐ
SSR์ ๋ํ ์์ธํ ๋ด์ฉ์ ๋ค์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ธ์.
Service ์์ฑ ๋ฉ์ปค๋์ฆ ์ดํดํ๊ธฐ
Service๋ Namespace ๊ธฐ๋ฐ์ ์ ์ ํจ์ ๋ชจ์์
๋๋ค.
Service๋ ํด๋์ค๊ฐ ์๋ Namespace
// services.generated.ts (์๋ ์์ฑ)
// โ ํด๋์ค๊ฐ ์๋
// class UserService { ... }
// โ
Namespace์
export namespace UserService {
// ์ ์ ํจ์๋ค
export async function getUser<T extends UserSubsetKey>(
subset: T,
id: number
): Promise<UserSubsetMapping[T]> {
return fetch({
method: "GET",
url: `/api/user/getUser?${qs.stringify({ subset, id })}`
});
}
export async function save(spa: UserSaveParams[]): Promise<number[]> {
return fetch({
method: "POST",
url: `/api/user/save`,
data: { spa }
});
}
// TanStack Query Hook
export const useUser = <T extends UserSubsetKey>(
subset: T,
id: number
) => useQuery({
queryKey: ["User", "getUser", subset, id],
queryFn: () => getUser(subset, id)
});
}
Namespace์ ํน์ง:
- ๋ฌด์ํ (Stateless): ์ธ์คํด์ค๋ฅผ ์์ฑํ์ง ์์
- ์ ์ ํธ์ถ:
UserService.getUser() ์ง์ ํธ์ถ
- ํ์
์์ : TypeScript Namespace๋ก ์๋ฒฝํ ํ์
์ถ๋ก
- ํธ๋ฆฌ ์
ฐ์ดํน: ์ฌ์ฉํ์ง ์๋ ํจ์๋ ๋ฒ๋ค์ ํฌํจ ์๋จ
Service๋ ์์กด์ฑ ์ฃผ์
(DI)์ด ์์
Service๋ ์์ ํจ์์ด๋ฏ๋ก ์์กด์ฑ์ด ์ฃผ์
๋์ง ์์ต๋๋ค:
// โ DI๊ฐ ์์ (ํด๋์ค๊ฐ ์๋๋ฏ๋ก)
// new UserService(dependencies)
// โ
์ง์ ํธ์ถ
const user = await UserService.getUser("A", 123);
Context๋ ์ด๋ป๊ฒ ์ ๋ฌ๋๋์?
Service๋ HTTP ์์ฒญ์ผ๋ก Context๋ฅผ ์ ๋ฌํฉ๋๋ค:
// Service ๋ด๋ถ (๊ฐ๋ตํ)
export async function getUser(subset: string, id: number) {
return fetch({
method: "GET",
url: `/api/user/getUser?subset=${subset}&id=${id}`,
// Context๋ HTTP ํค๋๋ก ์ ๋ฌ๋จ
headers: {
"Authorization": `Bearer ${getToken()}`,
"Cookie": document.cookie, // ์ธ์
์ ๋ณด
}
});
}
๋ฐฑ์๋์์๋ HTTP ์์ฒญ์์ Context๋ฅผ ์๋ ์ถ์ถ:
// Backend Model
class UserModelClass extends BaseModelClass {
@api({ httpMethod: "GET" })
async getUser(subset: string, id: number) {
// Context๋ HTTP ์์ฒญ์์ ์๋ ํ์ฑ๋จ
const context = Sonamu.getContext();
console.log(context.user); // ์ธ์ฆ๋ ์ฌ์ฉ์
console.log(context.session); // ์ธ์
์ ๋ณด
console.log(context.ip); // ํด๋ผ์ด์ธํธ IP
// ๊ถํ ์ฒดํฌ ๋ฑ ๊ฐ๋ฅ
if (!context.user) {
throw new UnauthorizedError();
}
return this.findById(id, [subset]);
}
}
Service ์์ฑ ๊ณผ์
์์ฑ ๋จ๊ณ:
- ๋ฐฑ์๋: Model์
@api ๋ฐ์ฝ๋ ์ดํฐ ์์ฑ
- Sonamu Sync:
pnpm sonamu sync ์คํ
- ์๋ ์์ฑ:
services.generated.ts ํ์ผ ์์ฑ
- Namespace ํจ์: ๊ฐ API ๋ฉ์๋๊ฐ Namespace ํจ์๋ก ๋ณํ
- Hook ์์ฑ: GET ๋ฉ์๋๋
useQuery Hook ์๋ ์์ฑ
Model Instance๋ ์ด๋์ ์๋์?
Server Components์์ ์ง์ ์์ฑ:
// Server Component
export default async function UserPage() {
// โ
์๋ฒ์์ Model์ ์ง์ ์ธ์คํด์คํ
const userModel = new UserModel();
const user = await userModel.findById(1, ["A"]);
return <div>{user.username}</div>;
}
์ new UserModel()์ ํด๋ ๋๋์?
- Model์ ๋ฌด์ํ์ด๋ฏ๋ก ๋งค๋ฒ ์๋ก ์์ฑํด๋ ์์
- Constructor์์ ์ด๊ธฐํํ๋ ๊ฒ์ Subset ์ฟผ๋ฆฌ๋ฟ
- Context๋
Sonamu.getContext()๋ก ๋์ ์ผ๋ก ๊ฐ์ ธ์ด
// Model ๋ด๋ถ
class UserModelClass extends BaseModelClass {
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
// ์ํ๋ฅผ ์ ์ฅํ์ง ์์
}
async findById(id: number, subsets: string[]) {
// Context๋ ์ฌ๊ธฐ์ ๋์ ์ผ๋ก ๊ฐ์ ธ์ด
const context = Sonamu.getContext();
// DB ์ฟผ๋ฆฌ ์คํ
return this.getPuri("r", subsets).where("id", id).first();
}
}
Service vs Model ๋น๊ต
| ํญ๋ชฉ | Service (Frontend) | Model (Backend) |
|---|
| ํ์
| Namespace | Class Instance |
| ํธ์ถ ๋ฐฉ์ | UserService.getUser() | userModel.findById() |
| ์ํ | ๋ฌด์ํ (Stateless) | ๋ฌด์ํ (Stateless) |
| ์์กด์ฑ | ์์ | BaseModelClass |
| Context | HTTP ํค๋๋ก ์ ๋ฌ | Sonamu.getContext() |
| ์ฌ์ฉ ์์น | Client Component | Server Component / API |
| ์์ฑ ๋ฐฉ๋ฒ | ์๋ ์์ฑ | ๊ฐ๋ฐ์ ์์ฑ |
Model ์ธ์คํด์ค ์์ฑ ์ ์ฃผ์์ฌํญ:
- Server Components์์๋ง: Client Component์์๋ Model์ importํ ์ ์์
- ๋งค๋ฒ ์์ฑ: ์ฑ๊ธํค ํจํด ์ฌ์ฉ ๋ถํ์ (๋ฌด์ํ์ด๋ฏ๋ก)
- Context ์ ๊ทผ:
Sonamu.getContext()๋ ์์ฒญ ์ปจํ
์คํธ์์ ์๋์ผ๋ก ๊ฐ์ ธ์ด
๊ถ์ฅ ํจํด:
- Client Component: Service ์ฌ์ฉ
- Server Component: Model ์ง์ ์ฌ์ฉ
- API ์๋ํฌ์ธํธ: Model์
@api ๋ฐ์ฝ๋ ์ดํฐ
Client Components
Client Components์์๋ Service๋ฅผ ์์ ๋กญ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค.
// app/users/[id]/edit-button.tsx
"use client";
import { useState } from "react";
import { UserService } from "@/services/services.generated";
export function EditButton({ userId }: { userId: number }) {
const [editing, setEditing] = useState(false);
async function handleEdit() {
setEditing(true);
try {
await UserService.updateProfile({
username: "newname",
});
alert("Updated!");
} catch (error) {
alert("Failed!");
} finally {
setEditing(false);
}
}
return (
<button onClick={handleEdit} disabled={editing}>
{editing ? "Saving..." : "Edit"}
</button>
);
}
์๋ฌ ์ฒ๋ฆฌ
SonamuError ์ฒ๋ฆฌ
Service ํธ์ถ ์ ๋ฐ์ํ๋ ์๋ฌ๋ฅผ ํ์
์์ ํ๊ฒ ์ฒ๋ฆฌํฉ๋๋ค.
import { UserService } from "@/services/services.generated";
import { isSonamuError } from "@/lib/sonamu.shared";
async function updateUser() {
try {
await UserService.updateProfile({
username: "newname",
});
} catch (error) {
if (isSonamuError(error)) {
// Sonamu ์๋ฌ (ํ์
์์ )
console.log("Status:", error.code);
console.log("Message:", error.message);
// Zod ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ
error.issues.forEach((issue) => {
console.log(`${issue.path.join(".")}: ${issue.message}`);
});
// HTTP ์ํ ์ฝ๋๋ณ ์ฒ๋ฆฌ
if (error.code === 401) {
// ์ธ์ฆ ์๋ฌ
console.log("Please login");
} else if (error.code === 403) {
// ๊ถํ ์๋ฌ
console.log("Permission denied");
} else if (error.code === 422) {
// Validation ์๋ฌ
console.log("Invalid data:", error.issues);
}
} else {
// ์ผ๋ฐ ์๋ฌ
console.log("Network error:", error);
}
}
}
React์์ ์๋ฌ ์ฒ๋ฆฌ
import { isSonamuError } from "@/lib/sonamu.shared";
function EditProfile({ userId }: { userId: number }) {
const [error, setError] = useState<string | null>(null);
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
async function handleSubmit(data: any) {
setError(null);
setValidationErrors({});
try {
await UserService.updateProfile(data);
} catch (err) {
if (isSonamuError(err)) {
setError(err.message);
// Zod ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ๋ฅผ ํ๋๋ณ๋ก ๋งคํ
const fieldErrors: Record<string, string> = {};
err.issues.forEach((issue) => {
const field = issue.path.join(".");
fieldErrors[field] = issue.message;
});
setValidationErrors(fieldErrors);
} else {
setError("An unexpected error occurred");
}
}
}
return (
<div>
{error && <div className="error-message">{error}</div>}
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit({/* data */});
}}>
<div>
<input name="username" />
{validationErrors.username && (
<span className="error">{validationErrors.username}</span>
)}
</div>
</form>
</div>
);
}
๊ณ ๊ธ ํจํด
๋ณ๋ ฌ ์์ฒญ
์ฌ๋ฌ API๋ฅผ ๋์์ ํธ์ถํ์ฌ ์ฑ๋ฅ์ ํฅ์์ํต๋๋ค.
import { UserService, PostService } from "@/services/services.generated";
async function loadUserDashboard(userId: number) {
// โ ์์ฐจ ์คํ (๋๋ฆผ)
const user = await UserService.getUser("A", userId);
const posts = await PostService.getPostsByUser(userId);
const comments = await PostService.getCommentsByUser(userId);
return { user, posts, comments };
}
async function loadUserDashboardFast(userId: number) {
// โ
๋ณ๋ ฌ ์คํ (๋น ๋ฆ)
const [user, posts, comments] = await Promise.all([
UserService.getUser("A", userId),
PostService.getPostsByUser(userId),
PostService.getCommentsByUser(userId),
]);
return { user, posts, comments };
}
์ฑ๋ฅ ๋น๊ต:
- ์์ฐจ: 300ms + 200ms + 150ms = 650ms
- ๋ณ๋ ฌ: max(300ms, 200ms, 150ms) = 300ms
Subset ํ์ฉ ์ต์ ํ
์ํฉ์ ๋ฐ๋ผ ์ ์ ํ Subset์ ์ ํํฉ๋๋ค.
// ๋ชฉ๋ก ํ๋ฉด: ๊ธฐ๋ณธ ์ ๋ณด๋ง
const users = await UserService.getUsers("A");
users.map((user) => (
<div key={user.id}>
{user.username} - {user.email}
</div>
));
// ์์ธ ํ๋ฉด: ์ ์ฒด ์ ๋ณด
const fullUser = await UserService.getUser("C", userId);
<div>
<h1>{fullUser.username}</h1>
<p>{fullUser.bio}</p>
<p>Created: {fullUser.createdAt}</p>
</div>
TanStack Query ํ์ฉ
์กฐ๊ฑด๋ถ ํ์นญ
function UserProfile({ userId }: { userId: number | null }) {
const { data: user } = UserService.useUser(
"A",
userId!,
{ enabled: userId !== null } // userId๊ฐ null์ด๋ฉด ํธ์ถ ์ํจ
);
if (!userId) return <div>Please select a user</div>;
if (!user) return <div>Loading...</div>;
return <div>{user.username}</div>;
}
์บ์ ๋ฌดํจํ
import { useQueryClient } from "@tanstack/react-query";
import { UserService } from "@/services/services.generated";
function EditProfile({ userId }: { userId: number }) {
const queryClient = useQueryClient();
async function handleUpdate(data: any) {
await UserService.updateProfile(data);
// ํน์ ์ฟผ๋ฆฌ ๋ฌดํจํ
queryClient.invalidateQueries(
UserService.getUserQueryOptions("A", userId)
);
// ๋๋ ๋ชจ๋ User ์ฟผ๋ฆฌ ๋ฌดํจํ
queryClient.invalidateQueries({
queryKey: ["User"],
});
}
}
Prefetching
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>
);
}
์ค์ ์์
์ ์ฒด CRUD ํ๋ก์ฐ
import { useState } from "react";
import { UserService } from "@/services/services.generated";
import { isSonamuError } from "@/lib/sonamu.shared";
function UserManagement() {
// ๋ชฉ๋ก ์กฐํ (Subset A)
const { data: users, refetch } = UserService.useUsers("A");
// ์์ฑ
async function handleCreate(data: { username: string; email: string }) {
try {
await UserService.create(data);
refetch(); // ๋ชฉ๋ก ์๋ก๊ณ ์นจ
alert("Created!");
} catch (error) {
if (isSonamuError(error)) {
alert(error.message);
}
}
}
// ์์
async function handleUpdate(id: number, data: { username: string }) {
try {
await UserService.update(id, data);
refetch();
alert("Updated!");
} catch (error) {
if (isSonamuError(error)) {
alert(error.message);
}
}
}
// ์ญ์
async function handleDelete(id: number) {
if (!confirm("Are you sure?")) return;
try {
await UserService.delete(id);
refetch();
alert("Deleted!");
} catch (error) {
if (isSonamuError(error)) {
alert(error.message);
}
}
}
return (
<div>
<h1>Users</h1>
{users?.map((user) => (
<div key={user.id}>
{user.username}
<button onClick={() => handleUpdate(user.id, { username: "new" })}>
Edit
</button>
<button onClick={() => handleDelete(user.id)}>
Delete
</button>
</div>
))}
</div>
);
}
์ฃผ์์ฌํญ
Service ์ฌ์ฉ ์ ์ฃผ์์ฌํญ:
- ์์ฑ๋ Service ํ์ผ(
services.generated.ts)์ ์ ๋ ์๋ ์์ ๊ธ์ง
- Subset ํ๋ผ๋ฏธํฐ ํ์: getUser ๋ฑ์์ subset ์ง์ ํ์
- Server Components์์๋ Service ๋์ ๋ฐฑ์๋ ๋ชจ๋ธ ์ง์ ํธ์ถ
- ์๋ฌ ์ฒ๋ฆฌ ์
isSonamuError() ํ์
๊ฐ๋ ์ฌ์ฉ
- TanStack Query Hook์ ์ปดํฌ๋ํธ ๋ด๋ถ์์๋ง ํธ์ถ
await ํค์๋ ํ์ (๋ชจ๋ Service ํจ์๋ async)
- Namespace์ด๋ฏ๋ก new ๋ถํ์:
UserService.getUser() ์ง์ ํธ์ถ
๋ค์ ๋จ๊ณ