Sonamu๊ฐ ์์ฑํ๋ services.generated.ts ํ์ผ์ ํ์
๊ตฌ์กฐ์ ํ์ฉ ๋ฐฉ๋ฒ์ ์์๋ด
๋๋ค.
๊ณต์ ํ์
๊ฐ์
๋จ์ผ ํ์ผ
services.generated.ts ๋ชจ๋ ํ์
ํ ๊ณณ์
์๋ ์์ฑ
๋ฐฑ์๋์์ ์ถ์ถ ์๋ ์์
๋ถํ์
ํ์
์ฌ์ฌ์ฉ
Export๋ ํ์
ํ๋ก์ ํธ ์ ์ฒด ์ฌ์ฉ
์ผ๊ด์ฑ
๋จ์ผ ์ง์ค ๊ณต๊ธ์ ํ์
์ถฉ๋ ์์
services.generated.ts ๊ตฌ์กฐ
ํ์ผ ๊ฐ์
Sonamu๋ ๋ชจ๋ ํ์
๊ณผ Service๋ฅผ ๋จ์ผ ํ์ผ์ ์์ฑํฉ๋๋ค.
// services.generated.ts (์๋ ์์ฑ)
// 1. ๊ณตํต Import
import { useQuery, useMutation, queryOptions } from "@tanstack/react-query";
import qs from "qs";
// 2. Entity ํ์
export interface User {
id: number;
username: string;
email: string;
createdAt: Date;
}
// 3. Subset ํ์
export type UserSubsetKey = "A" | "B" | "C";
export type UserSubsetMapping = {
A: { id: number; username: string };
B: { id: number; username: string; email: string };
C: User;
};
// 4. Service Namespace
export namespace UserService {
// 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 })}`,
});
}
// TanStack Query Hook
export const getUserQueryOptions = <T extends UserSubsetKey>(subset: T, id: number) =>
queryOptions({
queryKey: ["User", "getUser", subset, id],
queryFn: () => getUser(subset, id),
});
export const useUser = <T extends UserSubsetKey>(
subset: T,
id: number,
options?: { enabled?: boolean },
) =>
useQuery({
...getUserQueryOptions(subset, id),
...options,
});
}
// 5. ๋ค๋ฅธ Entity๋ค๋ ๋์ผํ ๊ตฌ์กฐ
export interface Post {
/* ... */
}
export namespace PostService {
/* ... */
}
ํ์ผ ํฌ๊ธฐ:
- ๋ณดํต 1,000 ~ 5,000 ์ค
- Entity์ API๊ฐ ๋ง์ผ๋ฉด 10,000 ์ค ์ด์๋ ๊ฐ๋ฅ
- ํ์ง๋ง ๋ชจ๋ ์๋ ์์ฑ๋๋ฏ๋ก ๊ด๋ฆฌ ๋ถ๋ด ์์
ํ์
์ข
๋ฅ
1. Entity ํ์
๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ์ ๊ตฌ์กฐ๋ฅผ ๋ํ๋
๋๋ค.
// User Entity
export interface User {
id: number;
username: string;
email: string;
role: "admin" | "user";
bio: string | null;
createdAt: Date;
updatedAt: Date;
}
// Post Entity
export interface Post {
id: number;
title: string;
content: string;
author_id: number;
published: boolean;
createdAt: Date;
}
ํน์ง:
- ๋ฐฑ์๋ Entity์ ์์ ํ ๋์ผ
- ๋ชจ๋ ํ๋ ํ์
์ด ์ ํํ ๋งคํ๋จ
- null, undefined, ์ ๋์จ ํ์
๋ชจ๋ ๋ณด์กด
2. Subset ํ์
Entity์ ๋ถ๋ถ ์งํฉ์ ์ ์ํฉ๋๋ค.
// Subset Key (๋ฆฌํฐ๋ด ์ ๋์จ)
export type UserSubsetKey = "A" | "B" | "C";
// Subset Mapping (๊ฐ Key๋ณ ํ์
)
export type UserSubsetMapping = {
A: Pick<User, "id" | "username">;
B: Pick<User, "id" | "username" | "email">;
C: User; // ์ ์ฒด
};
// Mapped Type์ผ๋ก ์ฌ์ฉ
type SubsetA = UserSubsetMapping["A"];
// { id: number; username: string }
Subset ๋ค์ด๋ฐ ๊ท์น:
- A: ์ต์ ํ๋ (id + ํต์ฌ 1~2๊ฐ)
- B: ์ค๊ฐ ํ๋ (A + ์ถ๊ฐ ์ ๋ณด)
- C: ์ ์ฒด ํ๋
3. API ํ๋ผ๋ฏธํฐ ํ์
API ํจ์์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ ์ํฉ๋๋ค.
export namespace UserService {
// ๋ช
์์ ํ๋ผ๋ฏธํฐ ํ์
export async function updateProfile(params: {
username?: string;
bio?: string;
avatar?: string;
}): Promise<{ user: User }> {
return fetch({
method: "PUT",
url: "/api/user/updateProfile",
data: params,
});
}
// ๋ณต์กํ ๊ฒ์ ํ๋ผ๋ฏธํฐ
export async function search(query: {
keyword: string;
role?: "admin" | "user";
isActive?: boolean;
page?: number;
pageSize?: number;
}): Promise<{ users: User[]; total: number }> {
return fetch({
method: "GET",
url: `/api/user/search?${qs.stringify(query)}`,
});
}
}
4. API ์๋ต ํ์
API ํจ์์ ๋ฐํ ํ์
์ ์ ์ํฉ๋๋ค.
export namespace UserService {
// ๋จ์ ์๋ต
export async function getUser(id: number): Promise<{ user: User }> {
// ...
}
// ๋ณต์กํ ์๋ต
export async function getDashboard(): Promise<{
user: User;
stats: {
postCount: number;
followerCount: number;
viewCount: number;
};
recentPosts: Post[];
recentComments: Comment[];
}> {
// ...
}
}
5. TanStack Query ๊ด๋ จ ํ์
React Hook์์ ์ฌ์ฉํ๋ ํ์
๋ค์
๋๋ค.
export namespace UserService {
// Query Options ํ์
(์ฌ์ฌ์ฉ ๊ฐ๋ฅ)
export const getUserQueryOptions = (id: number) =>
queryOptions({
queryKey: ["User", "getUser", id],
queryFn: () => getUser(id),
});
// Hook ํ์
(์๋ ์ถ๋ก )
export const useUser = (id: number, options?: { enabled?: boolean }) =>
useQuery({
...getUserQueryOptions(id),
...options,
});
}
// ์ฌ์ฉ ์ ํ์
์๋ ์ถ๋ก
const { data, isLoading } = UserService.useUser(123);
// data์ ํ์
: { user: User } | undefined
ํ์
์ฌ์ฌ์ฉ
Type Helper ํ์ฉ
TypeScript์ ์ ํธ๋ฆฌํฐ ํ์
์ผ๋ก ๊ธฐ์กด ํ์
์ ์ฌ์ฌ์ฉํฉ๋๋ค.
import type { UserService, User } from "@/services/services.generated";
// 1. ํจ์ ๋ฐํ ํ์
์ถ์ถ
type UserProfile = Awaited<ReturnType<typeof UserService.getProfile>>;
// { user: User; stats: { postCount: number; ... } }
// 2. ํ๋ผ๋ฏธํฐ ํ์
์ถ์ถ
type UpdateParams = Parameters<typeof UserService.updateProfile>[0];
// { username?: string; bio?: string; avatar?: string }
// 3. Entity ๋ถ๋ถ ํ์
type UserBasic = Pick<User, "id" | "username" | "email">;
// 4. ์ต์
๋ ๋ชจ๋ ํ๋
type PartialUser = Partial<User>;
// 5. ํน์ ํ๋ ์ ์ธ
type UserWithoutDates = Omit<User, "createdAt" | "updatedAt">;
์ปดํฌ๋ํธ Props
์์ฑ๋ ํ์
์ Props๋ก ์ฌ์ฉํฉ๋๋ค.
import type { User, Post } from "@/services/services.generated";
// Entity ํ์
์ง์ ์ฌ์ฉ
interface UserCardProps {
user: User;
}
function UserCard({ user }: UserCardProps) {
return (
<div>
<h3>{user.username}</h3>
<p>{user.email}</p>
</div>
);
}
// ์ฌ๋ฌ Entity ์กฐํฉ
interface DashboardProps {
user: User;
posts: Post[];
}
function Dashboard({ user, posts }: DashboardProps) {
return (
<div>
<h1>Welcome, {user.username}</h1>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
ํผ ๋ฐ์ดํฐ ํ์
API ํ๋ผ๋ฏธํฐ ํ์
์ ํผ ๋ฐ์ดํฐ๋ก ์ฌ์ฉํฉ๋๋ค.
import { useState } from "react";
import type { UserService } from "@/services/services.generated";
// ํ๋ผ๋ฏธํฐ ํ์
์ถ์ถ
type UpdateProfileParams = Parameters<typeof UserService.updateProfile>[0];
function EditProfileForm() {
// ํ์
์์ ํ state
const [formData, setFormData] = useState<UpdateProfileParams>({
username: "",
bio: "",
avatar: "",
});
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
await UserService.updateProfile(formData); // โ
ํ์
์ผ์น
}
return (
<form onSubmit={handleSubmit}>
<input
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
/>
{/* ... */}
</form>
);
}
์ํ ๊ด๋ฆฌ
์ ์ญ ์ํ์์๋ ํ์
์ ์ฌ์ฌ์ฉํฉ๋๋ค.
import { create } from "zustand";
import type { User } from "@/services/services.generated";
// Zustand Store
interface AuthStore {
user: User | null;
login: (user: User) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStore>((set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
}));
๋ค์์คํ์ด์ค ํ์ฉ
Service ๊ทธ๋ฃนํ
Entity๋ณ๋ก Service๊ฐ Namespace๋ก ๊ทธ๋ฃนํ๋ฉ๋๋ค.
// services.generated.ts
export namespace UserService {
export async function getUser() {
/* ... */
}
export async function updateUser() {
/* ... */
}
export async function deleteUser() {
/* ... */
}
export const useUser = () => {
/* ... */
};
}
export namespace PostService {
export async function getPost() {
/* ... */
}
export async function createPost() {
/* ... */
}
export const usePost = () => {
/* ... */
};
}
์ฌ์ฉ:
// โ
Namespace๋ก ๋ช
ํํ๊ฒ ๊ตฌ๋ถ
import { UserService, PostService } from "@/services/services.generated";
await UserService.getUser("A", 123);
await PostService.getPost("A", 456);
์ฅ์ :
- ์ด๋ฆ ์ถฉ๋ ๋ฐฉ์ง (getUser vs getPost)
- ๊ด๋ จ ํจ์๋ค์ด ๋
ผ๋ฆฌ์ ์ผ๋ก ๊ทธ๋ฃนํ
- Import๊ฐ ๊ฐ๊ฒฐํด์ง
- IDE ์๋ ์์ฑ์ด ๋ ์ ํํด์ง
Type Import
ํ์
๋ง importํ ๋๋ type ํค์๋๋ฅผ ์ฌ์ฉํฉ๋๋ค.
// โ
ํ์
๋ง import (๋ฒ๋ค ํฌ๊ธฐ ๊ฐ์)
import type { User, Post } from "@/services/services.generated";
// โ ์ ์ฒด import (๋ถํ์ํ ์ฝ๋ ํฌํจ)
import { User, Post } from "@/services/services.generated";
Tree-shaking:
import type์ ์ปดํ์ผ ํ ์ ๊ฑฐ๋จ
- ๋ฒ๋ค ํฌ๊ธฐ ์ต์ ํ
- ๋น๋ ์๋ ํฅ์
ํ์ผ ํฌ๊ธฐ ๊ด๋ฆฌ
๋๊ท๋ชจ ํ๋ก์ ํธ
Entity์ API๊ฐ ๋ง์ผ๋ฉด ํ์ผ์ด ๋งค์ฐ ์ปค์ง ์ ์์ต๋๋ค.
// services.generated.ts (์์)
// 100๊ฐ Entity ร ํ๊ท 50์ค = 5,000์ค
// + Service ํจ์๋ค = 10,000์ค ์ด์
ํ์ง๋ง ๊ด์ฐฎ์ต๋๋ค:
- โ
์๋ ์์ฑ: ์๋ ๊ด๋ฆฌ ๋ถํ์
- โ
Tree-shaking: ์ฌ์ฉํ์ง ์๋ ์ฝ๋๋ ๋ฒ๋ค์์ ์ ์ธ
- โ
IDE ์ฑ๋ฅ: ํ๋ IDE๋ ํฐ ํ์ผ๋ ๋ฌธ์ ์์
- โ
ํ์
์ฒดํฌ: TypeScript ์ปดํ์ผ๋ฌ๊ฐ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌ
Code Splitting
ํ์ํ๋ฉด ๋์ import๋ก ์ฝ๋๋ฅผ ๋ถํ ํ ์ ์์ต๋๋ค.
// โ
ํ์ํ ๋๋ง ๋ก๋
async function loadUserService() {
const { UserService } = await import("@/services/services.generated");
return UserService;
}
// ์ฌ์ฉ
const UserService = await loadUserService();
await UserService.getUser("A", 123);
๋ฒ์ ๊ด๋ฆฌ
Git์ ํฌํจ ์ฌ๋ถ
ํฌํจํ๋ ๊ฒฝ์ฐ (๊ถ์ฅ):
# services.generated.ts๋ฅผ Git์ ํฌํจ
# (์ฃผ์ ์ฒ๋ฆฌ ๋๋ ์ ๊ฑฐ)
# services.generated.ts
์ฅ์ :
- Pull ๋ฐ์ผ๋ฉด ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅ
- ๋ฐฑ์๋ ์์ด๋ ํ๋ก ํธ์๋ ๊ฐ๋ฐ ๊ฐ๋ฅ
- ํ์
๋ณ๊ฒฝ ์ด๋ ฅ ์ถ์ ๊ฐ๋ฅ
ํฌํจํ์ง ์๋ ๊ฒฝ์ฐ:
# services.generated.ts๋ฅผ Git์์ ์ ์ธ
services.generated.ts
์ฅ์ :
- Conflict ๋ฐ์ ์ ์
- ๊ฐ์ ์ต์ ๋ฒ์ ์์ฑ
- Git ํ์คํ ๋ฆฌ๊ฐ ๊น๋
๊ถ์ฅ: ํฌํจํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ผ๋ก ๋ ํธ๋ฆฌํฉ๋๋ค.
์ถฉ๋ ํด๊ฒฐ
Merge conflict ๋ฐ์ ์:
# 1. ์ต์ ์ฝ๋ pull
git pull origin main
# 2. Service ์ฌ์์ฑ
pnpm generate
# 3. ์ฌ์์ฑ๋ ํ์ผ๋ก ์ถฉ๋ ํด๊ฒฐ
git add services.generated.ts
git commit -m "resolve: regenerate services"
๋๋ฒ๊น
์์ฑ๋ ํ์
ํ์ธ
IDE์์ ํ์
์ ๋ฐ๋ก ํ์ธํ ์ ์์ต๋๋ค.
import { UserService } from "@/services/services.generated";
// ํจ์ ์์ ๋ง์ฐ์ค ํธ๋ฒ
UserService.getUser;
// โ IDE๊ฐ ํ์
ํ์
// (alias) getUser<T extends UserSubsetKey>(
// subset: T,
// id: number
// ): Promise<UserSubsetMapping[T]>
๋จ์ถํค:
- VSCode:
Cmd + Click (Mac) / Ctrl + Click (Windows)
- ํ์
์ ์๋ก ๋ฐ๋ก ์ด๋
ํ์
์๋ฌ ๋๋ฒ๊น
ํ์
์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด:
- ์๋ฌ ๋ฉ์์ง ํ์ธ
Property 'username' does not exist on type 'User'.
- ํ์
์ ์ ํ์ธ
// services.generated.ts์์ User ํ์
ํ์ธ
export interface User {
id: number;
displayName: string; // username์ด ์๋!
email: string;
}
- ๋ฐฑ์๋ ํ์ธ
// backend์์ ์ค์ ์ ์ ํ์ธ
@api({ httpMethod: "GET" })
async getUser(): Promise<{ user: { displayName: string } }> {
// username์ด ์๋๋ผ displayName!
}
- ์ฝ๋ ์์
// username โ displayName
console.log(user.displayName);
์ฃผ์์ฌํญ
๊ณต์ ํ์
์ฌ์ฉ ์ ์ฃผ์์ฌํญ: 1. services.generated.ts ์๋ ์์ ๊ธ์ง 2. import type
์ฌ์ฉํ์ฌ ๋ฒ๋ค ํฌ๊ธฐ ์ต์ ํ 3. API ๋ณ๊ฒฝ ์ pnpm generate ํ์ 4. Namespace๋ฅผ ํตํด Service ์ ๊ทผ
5. ํ์
์ฌ์ฌ์ฉ ์ Type Helper ํ์ฉ
๋ค์ ๋จ๊ณ
API ํ์
์ถ๋ก
ํ์
์ถ๋ก ์ดํด
์ปดํ์ผ ํ์ ์๋ฌ
์๋ฌ ๊ฐ์ง ๋ฐฉ๋ฒ
Service ์ฌ์ฉํ๊ธฐ
Service ํ์ฉ๋ฒ
์ปค์คํ
์ปดํฌ๋ํธ
์ปดํฌ๋ํธ ์์ฑ