์์ฑ๋ 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์ ๋ํ ์์ธํ ๋ด์ฉ์ ๋ค์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ธ์.
๋ฐ์ดํฐ ํ๋ฆฌ๋ก๋ฉ
registerSSR๋ก ์๋ฒ์์ ๋ฐ์ดํฐ ๋ฏธ๋ฆฌ ๋ก๋ํ๊ธฐ
SSR ์ค์
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 ์ธ์คํด์ค ์์ฑ ์ ์ฃผ์์ฌํญ: 1. Server Components์์๋ง: Client Component์์๋
Model์ importํ ์ ์์ 2. ๋งค๋ฒ ์์ฑ: ์ฑ๊ธํค ํจํด ์ฌ์ฉ ๋ถํ์ (๋ฌด์ํ์ด๋ฏ๋ก) 3. 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 ์ฌ์ฉ ์ ์ฃผ์์ฌํญ: 1. ์์ฑ๋ Service ํ์ผ(services.generated.ts)์ ์ ๋ ์๋ ์์
๊ธ์ง 2. Subset ํ๋ผ๋ฏธํฐ ํ์: getUser ๋ฑ์์ subset ์ง์ ํ์ 3. Server Components์์๋
Service ๋์ ๋ฐฑ์๋ ๋ชจ๋ธ ์ง์ ํธ์ถ 4. ์๋ฌ ์ฒ๋ฆฌ ์ isSonamuError() ํ์
๊ฐ๋ ์ฌ์ฉ 5.
TanStack Query Hook์ ์ปดํฌ๋ํธ ๋ด๋ถ์์๋ง ํธ์ถ 6. await ํค์๋ ํ์ (๋ชจ๋ Service ํจ์๋
async) 7. Namespace์ด๋ฏ๋ก new ๋ถํ์: UserService.getUser() ์ง์ ํธ์ถ
๋ค์ ๋จ๊ณ
Service ๋์ ์๋ฆฌ
์๋ ์์ฑ ์ดํดํ๊ธฐ
TanStack Query Hook
React์์ ๋ ์ฝ๊ฒ
์๋ฌ ์ฒ๋ฆฌ
์๋ฌ ์ฒ๋ฆฌ ํจํด