์์ฑ๋ 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>
);
}
Next.js์์ ์ฌ์ฉํ๊ธฐ
Server Components (App Router)
Next.js 13+ App Router์ Server Components์์๋ Service๋ฅผ ์ง์ ํธ์ถํ ์ ์์ต๋๋ค. ๋์ ๋ฐฑ์๋ ๋ชจ๋ธ์ ์ง์ ์ฌ์ฉํฉ๋๋ค.
// app/users/[id]/page.tsx
// โ Server Component์์๋ ์๋ํ์ง ์์
// Service๋ ๋ธ๋ผ์ฐ์ ์ฉ fetch๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ์๋ฒ์์ ์ฌ์ฉ ๋ถ๊ฐ
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await UserService.getUser("A", parseInt(params.id)); // โ ์๋ ์ํจ!
return <div>{user.username}</div>;
}
ํด๊ฒฐ ๋ฐฉ๋ฒ: ๋ฐฑ์๋ ๋ชจ๋ธ ์ง์ ์ฌ์ฉ (๊ถ์ฅ)
// app/users/[id]/page.tsx
import { UserModel } from "@/models/user.model";
export default async function UserPage({ params }: { params: { id: string } }) {
// ์๋ฒ์์ ๋ชจ๋ธ์ ์ง์ ์ฌ์ฉ
const userModel = new UserModel();
const { user } = await userModel.getProfile(parseInt(params.id));
return (
<div>
<h1>{user.username}</h1>
<p>{user.email}</p>
</div>
);
}
๊ถ์ฅ ๋ฐฉ์: Server Components์์๋ ๋ฐฑ์๋ ๋ชจ๋ธ์ ์ง์ ์ฌ์ฉํ์ธ์. HTTP ์์ฒญ ์ค๋ฒํค๋๊ฐ ์๊ณ , ํ์
์์ ์ฑ๋ ๋์ผํ๊ฒ ๋ณด์ฅ๋ฉ๋๋ค.
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() ์ง์ ํธ์ถ
๋ค์ ๋จ๊ณ