Sonamuκ° λ°±μλ APIμ νμ
μ νλ‘ νΈμλμμ μλμΌλ‘ μΆλ‘ νμ¬ μμ ν νμ
μμ μ±μ μ 곡νλ λ°©λ²μ μμλ΄
λλ€.
API νμ
μΆλ‘ κ°μ
μλ νμ
μμ±
λ°±μλ β νλ‘ νΈμλ μλ μμ
λΆνμ
μμ ν μΆλ‘
νλΌλ―Έν°λΆν° μλ΅κΉμ§ λͺ¨λ νμ
보μ₯
μ€μκ° λκΈ°ν
API λ³κ²½ μ μλ μ
λ°μ΄νΈ
IDE μ§μ
μλ μμ± νμ
ννΈ
νμ
μΆλ‘ μ΄λ?
λ¬Έμ : μλ νμ
μ μμ μ΄λ €μ
μ ν΅μ μΈ κ°λ°μμλ λ°±μλμ νλ‘ νΈμλμ νμ
μ κ°κ° μλμΌλ‘ μ μν΄μΌ ν©λλ€.
// β λ°±μλ (Node.js + Express)
app.get("/api/user/:id", async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json({ user });
});
// β νλ‘ νΈμλ (μλ νμ
μ μ)
interface User {
id: number;
username: string;
email: string;
// νλ μΆκ°/λ³κ²½ μ μλ λκΈ°ν νμ
}
async function getUser(id: number): Promise<{ user: User }> {
const response = await fetch(`/api/user/${id}`);
return response.json();
}
λ¬Έμ μ :
- μ€λ³΅ μμ
: λ°±μλμ νλ‘ νΈμλμμ κ°μ νμ
λ λ² μ μ
- λκΈ°ν λλ½: λ°±μλ λ³κ²½ μ νλ‘ νΈμλ νμ
μ
λ°μ΄νΈ λλ½
- λ°νμ μλ¬: νμ
λΆμΌμΉλ₯Ό λ°νμμλ§ λ°κ²¬
- μ μ§λ³΄μ μ΄λ €μ: νμ
μ΄ λ§μμ§μλ‘ κ΄λ¦¬ 볡μ‘λ μ¦κ°
ν΄κ²°: μλ νμ
μΆλ‘
Sonamuλ λ°±μλ νμ
μ μλμΌλ‘ μΆλ‘ νμ¬ νλ‘ νΈμλμ μ λ¬ν©λλ€.
// β
λ°±μλ (Sonamu)
@api({ httpMethod: "GET" })
async getUser(userId: number): Promise<{
user: {
id: number;
username: string;
email: string;
};
}> {
const user = await this.findById(userId);
return { user };
}
// β
νλ‘ νΈμλ (μλ μμ±)
export namespace UserService {
export async function getUser(
userId: number
): Promise<{
user: {
id: number;
username: string;
email: string;
};
}> {
return fetch({
method: "GET",
url: `/api/user/getUser?${qs.stringify({ userId })}`,
});
}
}
μ₯μ :
- λ°±μλ νμ
μ΄ νλ‘ νΈμλμ μλμΌλ‘ 볡μ¬λ¨
- API λ³κ²½ μ μλμΌλ‘ λκΈ°νλ¨
- λ¨μΌ μ§μ€ 곡κΈμ (λ°±μλκ° μ μΌν νμ
μμ€)
νμ
μΆλ‘ κ³Όμ
1λ¨κ³: λ°±μλ API μ μ
TypeScript νμ
μ ν¬ν¨νμ¬ APIλ₯Ό μ μν©λλ€.
// backend/models/user.model.ts
import { BaseModelClass, api } from "sonamu";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries,
typeof userLoaderQueries
> {
constructor() {
super("User", userSubsetQueries, userLoaderQueries);
}
@api({ httpMethod: "GET" })
async getProfile(userId: number): Promise<{
user: {
id: number;
username: string;
email: string;
role: "admin" | "user";
createdAt: Date;
};
stats: {
postCount: number;
followerCount: number;
};
}> {
const user = await this.findById(userId);
const stats = await this.getStats(userId);
return { user, stats };
}
}
TypeScript νμ
μμ€ν
νμ©:
- λ°ν νμ
μ λͺ
μμ μΌλ‘ μ μΈ
- μ€μ²© κ°μ²΄, μ λμ¨ νμ
, 리ν°λ΄ νμ
λͺ¨λ μ§μ
- TypeScriptμ λͺ¨λ νμ
κΈ°λ₯ νμ© κ°λ₯
2λ¨κ³: AST νμ±
Sonamuλ TypeScript μ»΄νμΌλ¬ APIλ₯Ό μ¬μ©νμ¬ μ½λλ₯Ό λΆμν©λλ€.
// Sonamu λ΄λΆ λμ (μμ¬ μ½λ)
import * as ts from "typescript";
function extractApiType(methodNode: ts.MethodDeclaration) {
// 1. λ°ν νμ
μΆμΆ
const returnType = typeChecker.getTypeAtLocation(methodNode.type);
// 2. νμ
μ 보λ₯Ό TypeScript μ½λλ‘ λ³ν
const typeString = typeChecker.typeToString(returnType);
// 3. νλΌλ―Έν° νμ
μΆμΆ
const parameters = methodNode.parameters.map((param) => ({
name: param.name.getText(),
type: typeChecker.getTypeAtLocation(param.type),
}));
return {
returnType: typeString,
parameters,
};
}
AST (Abstract Syntax Tree):
- TypeScript μ½λλ₯Ό νΈλ¦¬ κ΅¬μ‘°λ‘ νν
- νμ
μ 보λ₯Ό νλ‘κ·Έλλ° λ°©μμΌλ‘ μΆμΆ κ°λ₯
- 100% μ νν νμ
μ 보 νλ
3λ¨κ³: Service νμ
μμ±
μΆμΆν νμ
μ Service μ½λμ μ½μ
ν©λλ€.
// services.generated.ts (μλ μμ±)
export namespace UserService {
// νλΌλ―Έν° νμ
λ μ νν μΆλ‘
export async function getProfile(
userId: number, // β λ°±μλμ λμΌν νμ
): Promise<{
user: {
id: number;
username: string;
email: string;
role: "admin" | "user"; // β 리ν°λ΄ νμ
λ 보쑴
createdAt: Date;
};
stats: {
postCount: number;
followerCount: number;
};
}> {
return fetch({
method: "GET",
url: `/api/user/getProfile?${qs.stringify({ userId })}`,
});
}
}
νμ
보쑴:
- μ λμ¨ νμ
(
"admin" | "user")
- μ€μ²© κ°μ²΄
- λ°°μ΄ νμ
- Date, null, undefined λ± λͺ¨λ TypeScript νμ
4λ¨κ³: TanStack Query Hook μμ±
React Hookμλ νμ
μ΄ μ νν μ λ¬λ©λλ€.
// services.generated.ts (κ³μ)
export namespace UserService {
export const getProfileQueryOptions = (userId: number) =>
queryOptions({
queryKey: ["User", "getProfile", userId],
queryFn: () => getProfile(userId),
});
export const useProfile = (userId: number, options?: { enabled?: boolean }) =>
useQuery({
...getProfileQueryOptions(userId),
...options,
});
}
νμ
체μΈ:
Backend Method
β TypeScript AST
β Service Function
β Query Options
β React Hook
β Component
λͺ¨λ λ¨κ³μμ νμ
μ΄ 100% 보쑴λ©λλ€!
κ³ κΈ νμ
μΆλ‘
μ λ€λ¦ νμ
μ λ€λ¦μ μ¬μ©νλ APIλ μ νν μΆλ‘ λ©λλ€.
// λ°±μλ
@api({ httpMethod: "GET" })
async getList<T extends "posts" | "comments">(
entityType: T
): Promise<{
items: T extends "posts" ? Post[] : Comment[];
}> {
// ꡬν...
}
// νλ‘ νΈμλ (μλ μμ±)
export namespace DataService {
export async function getList<T extends "posts" | "comments">(
entityType: T
): Promise<{
items: T extends "posts" ? Post[] : Comment[];
}> {
return fetch({
method: "GET",
url: `/api/data/getList?${qs.stringify({ entityType })}`,
});
}
}
// μ¬μ©
const { items } = await DataService.getList("posts");
// itemsμ νμ
: Post[]
Subset μμ€ν
Subset νμ
λ μλμΌλ‘ μμ±λ©λλ€.
// Entity μ μμμ Subset μΆμΆ
export type UserSubsetKey = "A" | "B" | "C";
export type UserSubsetMapping = {
A: { id: number; username: string; email: string };
B: { id: number; username: string; email: string; bio: string };
C: User; // μ 체 νλ
};
// Serviceμμ μ¬μ©
export namespace UserService {
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 })}`,
});
}
}
// νμ
μμ ν μ¬μ©
const basicUser = await UserService.getUser("A", 123);
// νμ
: { id: number; username: string; email: string }
const fullUser = await UserService.getUser("C", 123);
// νμ
: User
Mapped Types:
UserSubsetMapping[T]λ‘ Subsetμ λ°λ₯Έ μ νν νμ
λ°ν
- TypeScriptμ μ‘°κ±΄λΆ νμ
νμ©
볡μ‘ν μ€μ²© ꡬ쑰
κΉκ² μ€μ²©λ νμ
λ μ νν μΆλ‘ λ©λλ€.
// λ°±μλ
@api({ httpMethod: "GET" })
async getDashboard(): Promise<{
user: {
profile: {
name: string;
avatar: string;
};
settings: {
notifications: {
email: boolean;
push: boolean;
};
privacy: {
profileVisibility: "public" | "private";
};
};
};
stats: {
posts: { count: number; latest: Post[] };
followers: { count: number; recent: User[] };
};
}> {
// ꡬν...
}
// νλ‘ νΈμλ (μλ μμ±)
export namespace DashboardService {
export async function getDashboard(): Promise<{
user: {
profile: {
name: string;
avatar: string;
};
settings: {
notifications: {
email: boolean;
push: boolean;
};
privacy: {
profileVisibility: "public" | "private";
};
};
};
stats: {
posts: { count: number; latest: Post[] };
followers: { count: number; recent: User[] };
};
}> {
return fetch({
method: "GET",
url: "/api/dashboard/getDashboard",
});
}
}
// μ¬μ© (μμ ν νμ
μμ μ±)
const dashboard = await DashboardService.getDashboard();
console.log(dashboard.user.settings.notifications.email); // β
boolean
console.log(dashboard.stats.posts.latest[0].title); // β
string
μ€μ νμ©
React μ»΄ν¬λνΈ
νμ
μ΄ μλμΌλ‘ μΆλ‘ λμ΄ IDE μ§μμ λ°μ΅λλ€.
import { UserService } from "@/services/services.generated";
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading } = UserService.useProfile(userId);
if (isLoading) return <div>Loading...</div>;
// dataμ νμ
μ΄ μλμΌλ‘ μΆλ‘ λ¨
return (
<div>
<h1>{data.user.username}</h1>
<p>Role: {data.user.role}</p>
<p>Posts: {data.stats.postCount}</p>
<p>Followers: {data.stats.followerCount}</p>
</div>
);
}
IDE μλ μμ±:
data. μ
λ ₯ μ user, stats μλ μ μ
data.user. μ
λ ₯ μ λͺ¨λ νλ μλ μ μ
- μλͺ»λ νλ μ κ·Ό μ μ¦μ μλ¬ νμ
νμ
μ¬μ¬μ©
μμ±λ νμ
μ λ€λ₯Έ κ³³μμ μ¬μ¬μ©ν μ μμ΅λλ€.
import type { UserService } from "@/services/services.generated";
// Service ν¨μμ λ°ν νμ
μΆμΆ
type UserProfile = Awaited<ReturnType<typeof UserService.getProfile>>;
// νμ
μ¬μ©
function processProfile(profile: UserProfile) {
console.log(profile.user.username);
console.log(profile.stats.postCount);
}
νΌ λ°μ΄ν° νμ
API νλΌλ―Έν° νμ
λ μΆλ‘ λ©λλ€.
// λ°±μλ
@api({ httpMethod: "POST" })
async createPost(params: {
title: string;
content: string;
tags: string[];
published: boolean;
}): Promise<{ post: Post }> {
// ꡬν...
}
// νλ‘ νΈμλ (μλ μμ±)
export namespace PostService {
export async function createPost(params: {
title: string;
content: string;
tags: string[];
published: boolean;
}): Promise<{ post: Post }> {
return fetch({
method: "POST",
url: "/api/post/createPost",
data: params,
});
}
}
// μ¬μ© (νλΌλ―Έν° νμ
κ²μ¦)
function CreatePostForm() {
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
await PostService.createPost({
title: "Hello",
content: "World",
tags: ["typescript", "sonamu"],
published: true,
// author: "John" // β μ»΄νμΌ μλ¬: μ‘΄μ¬νμ§ μλ νλ
});
}
}
νμ
μμ μ± λ³΄μ₯
1. νλΌλ―Έν° κ²μ¦
μλͺ»λ νλΌλ―Έν°λ μ»΄νμΌ νμμ κ°μ§λ©λλ€.
// β μ»΄νμΌ μλ¬
await UserService.getProfile("123"); // string λμ number νμ
// β
μ μ
await UserService.getProfile(123);
2. μλ΅ νμ
보μ₯
API μλ΅μ νμ
μ΄ λ³΄μ₯λ©λλ€.
const { user } = await UserService.getProfile(123);
console.log(user.username); // β
string
console.log(user.age); // β μ»΄νμΌ μλ¬: age νλ μμ
3. null/undefined μ²λ¦¬
μ΅μ
λ νλλ μ νν ννλ©λλ€.
// λ°±μλ
@api({ httpMethod: "GET" })
async getUser(): Promise<{
user: {
id: number;
bio?: string; // μ΅μ
λ
};
}> {
// ꡬν...
}
// νλ‘ νΈμλ
const { user } = await UserService.getUser();
console.log(user.bio?.length); // β
μ΅μ
λ 체μ΄λ νμ
μ£Όμμ¬ν
API νμ
μΆλ‘ μ¬μ© μ μ£Όμμ¬ν: 1. λ°±μλ APIμ λͺ
μμ νμ
μ μΈ νμ 2. any νμ
μ¬μ©
κΈμ§ (νμ
μΆλ‘ λΆκ°) 3. pnpm generate μ€νν΄μΌ νμ
μ
λ°μ΄νΈ 4. μμ±λ νμ
νμΌ μλ μμ
κΈμ§ 5. 볡μ‘ν νμ
μ λ³λ interfaceλ‘ λΆλ¦¬ κΆμ₯
λ€μ λ¨κ³
μ»΄νμΌ νμ μλ¬
API λ³κ²½ κ°μ§
곡μ νμ
νμ
νμΌ κ΅¬μ‘°
Service λμ μ리
μλ μμ± μ΄ν΄
Service μ¬μ©νκΈ°
Service νμ©λ²