๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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์ค„ ์ด์ƒ
ํ•˜์ง€๋งŒ ๊ดœ์ฐฎ์Šต๋‹ˆ๋‹ค:
  1. โœ… ์ž๋™ ์ƒ์„ฑ: ์ˆ˜๋™ ๊ด€๋ฆฌ ๋ถˆํ•„์š”
  2. โœ… Tree-shaking: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ฝ”๋“œ๋Š” ๋ฒˆ๋“ค์—์„œ ์ œ์™ธ
  3. โœ… IDE ์„ฑ๋Šฅ: ํ˜„๋Œ€ IDE๋Š” ํฐ ํŒŒ์ผ๋„ ๋ฌธ์ œ์—†์Œ
  4. โœ… ํƒ€์ž… ์ฒดํฌ: 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)
  • ํƒ€์ž… ์ •์˜๋กœ ๋ฐ”๋กœ ์ด๋™

ํƒ€์ž… ์—๋Ÿฌ ๋””๋ฒ„๊น…

ํƒ€์ž… ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด:
  1. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ™•์ธ
Property 'username' does not exist on type 'User'.
  1. ํƒ€์ž… ์ •์˜ ํ™•์ธ
// services.generated.ts์—์„œ User ํƒ€์ž… ํ™•์ธ
export interface User {
  id: number;
  displayName: string; // username์ด ์•„๋‹˜!
  email: string;
}
  1. ๋ฐฑ์—”๋“œ ํ™•์ธ
// backend์—์„œ ์‹ค์ œ ์ •์˜ ํ™•์ธ
@api({ httpMethod: "GET" })
async getUser(): Promise<{ user: { displayName: string } }> {
  // username์ด ์•„๋‹ˆ๋ผ displayName!
}
  1. ์ฝ”๋“œ ์ˆ˜์ •
// username โ†’ displayName
console.log(user.displayName);

์ฃผ์˜์‚ฌํ•ญ

๊ณต์œ  ํƒ€์ž… ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. services.generated.ts ์ˆ˜๋™ ์ˆ˜์ • ๊ธˆ์ง€
  2. import type ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ๋“ค ํฌ๊ธฐ ์ตœ์ ํ™”
  3. API ๋ณ€๊ฒฝ ์‹œ pnpm generate ํ•„์ˆ˜
  4. Namespace๋ฅผ ํ†ตํ•ด Service ์ ‘๊ทผ
  5. ํƒ€์ž… ์žฌ์‚ฌ์šฉ ์‹œ Type Helper ํ™œ์šฉ

๋‹ค์Œ ๋‹จ๊ณ„