Sonamu๋ ์ํฐํฐ ์ค์ฌ์ ํ์คํ ๊ฐ๋ฐ์ ๊ฐ๋ฅํ๊ฒ ํ๋ TypeScript ํ๋ ์์ํฌ์
๋๋ค. ์ด ๊ฐ์ด๋์์๋ Sonamu์ ์ ์ฒด ์ํคํ
์ฒ์ ํต์ฌ ์๋ ์๋ฆฌ๋ฅผ ์์๋ด
๋๋ค.
Sonamu์ ์ฒ ํ
Sonamu๋ ๋ค์ ์์น์ ๊ธฐ๋ฐ์ผ๋ก ์ค๊ณ๋์์ต๋๋ค:
1. Entity First (์ํฐํฐ ์ฐ์ )
๋ชจ๋ ๊ฐ๋ฐ์ ์ํฐํฐ ์ ์์์ ์์ํฉ๋๋ค. ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋จผ์ ์ ์ํ๋ฉด, ๋๋จธ์ง๋ ์๋์ผ๋ก ์์ฑ๋ฉ๋๋ค.
2. Type Safety (ํ์
์์ ์ฑ)
๋ฐฑ์๋๋ถํฐ ํ๋ก ํธ์๋๊น์ง ์์ ํ ํ์
์์ ์ฑ์ ๋ณด์ฅํฉ๋๋ค. ์ปดํ์ผ ํ์์ ์ค๋ฅ๋ฅผ ๋ฐ๊ฒฌํ ์ ์์ต๋๋ค.
3. Code Generation (์ฝ๋ ์๋ ์์ฑ)
๋ฐ๋ณต์ ์ธ ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๋ฅผ ์๋์ผ๋ก ์์ฑํฉ๋๋ค. ๊ฐ๋ฐ์๋ ๋น์ฆ๋์ค ๋ก์ง์๋ง ์ง์คํ ์ ์์ต๋๋ค.
4. Developer Experience (๊ฐ๋ฐ์ ๊ฒฝํ)
HMR, ํ์
์ถ๋ก , Sonamu UI ๋ฑ์ผ๋ก ๋ฐ์ด๋ ๊ฐ๋ฐ ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค.
์ ์ฒด ์ํคํ
์ฒ
Sonamu๋ 6๊ฐ์ ๋ ์ด์ด๊ฐ ์๋์ผ๋ก ์ฐ๊ฒฐ๋์ด ๋์ํฉ๋๋ค:
ํต์ฌ ๊ตฌ์ฑ ์์
1. Entity (์ํฐํฐ)
์ํฐํฐ๋ Sonamu์ ๋ชจ๋ ๊ฒ์ ์์์ ์
๋๋ค.
{
"entityId": "User",
"tableName": "users",
"title": "์ฌ์ฉ์",
"properties": [
{
"name": "id",
"type": "int",
"isPrimary": true
},
{
"name": "email",
"type": "varchar"
},
{
"name": "name",
"type": "varchar"
}
],
"subsets": {
"A": ["id", "email", "name", "created_at"],
"C": ["id", "email", "name"]
}
}
์ํฐํฐ ์ ์์ ์ญํ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง์ ์์ค
- TypeScript ํ์
์์ฑ์ ๊ธฐ๋ฐ
- API ์ธํฐํ์ด์ค์ ์ ์
- ํ๋ก ํธ์๋ ํ์
์ ๊ธฐ์ค
2. Type System (ํ์
์์คํ
)
์ํฐํฐ ์ ์๋ก๋ถํฐ 3๊ฐ์ง ํ์
ํ์ผ์ด ์๋ ์์ฑ๋ฉ๋๋ค:
// ์๋ ์์ฑ + ํ์ฅ ๊ฐ๋ฅ
import { z } from "zod";
import { UserBaseSchema } from "../sonamu.generated";
// Base ํ์
(์๋ ์์ฑ)
export type User = z.infer<typeof UserBaseSchema>;
// ์ปค์คํ
ํ์
(์ง์ ์์ฑ ๊ฐ๋ฅ)
export const UserSaveParams = UserBaseSchema.partial({
id: true,
created_at: true,
});
export type UserSaveParams = z.infer<typeof UserSaveParams>;
ํ์
ํ์ผ 3์ข
์ธํธ
{entity}.types.ts - ์ํฐํฐ๋ณ ํ์
(ํ์ฅ ๊ฐ๋ฅ)
sonamu.generated.ts - ์ ์ฒด Base ์คํค๋ง (์๋ ์์ฑ)
sonamu.generated.sso.ts - Subset ์ฟผ๋ฆฌ (์๋ ์์ฑ)
3. Model (๋น์ฆ๋์ค ๋ก์ง)
Model์ ์ํฐํฐ์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ด๋นํฉ๋๋ค.
import { api, BaseModelClass } from "sonamu";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userSubsetQueries } from "../sonamu.generated.sso";
class UserModelClass extends BaseModelClass<
UserSubsetKey,
UserSubsetMapping,
typeof userSubsetQueries
> {
constructor() {
super("User", userSubsetQueries);
}
// CRUD API
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
async findById<T extends UserSubsetKey>(
subset: T,
id: number,
): Promise<UserSubsetMapping[T]> {
const user = await this.db().where("id", id).first();
return user;
}
// ์ปค์คํ
๋น์ฆ๋์ค ๋ก์ง
@api({ httpMethod: "POST", clients: ["axios"] })
async changePassword(userId: number, newPassword: string): Promise<void> {
await this.db()
.where("id", userId)
.update({ password: await this.hashPassword(newPassword) });
}
private async hashPassword(password: string): Promise<string> {
// ๋น๋ฐ๋ฒํธ ํด์ฑ ๋ก์ง
return password;
}
}
export const UserModel = new UserModelClass();
Model์ ํน์ง
@api ๋ฐ์ฝ๋ ์ดํฐ๋ก ์๋ REST API ์์ฑ
BaseModelClass ์์์ผ๋ก ๊ธฐ๋ณธ CRUD ๋ฉ์๋ ์ ๊ณต
- Puri ์ฟผ๋ฆฌ ๋น๋๋ก ํ์
์์ ํ DB ์ฟผ๋ฆฌ
- ๋น์ฆ๋์ค ๋ก์ง์๋ง ์ง์ค ๊ฐ๋ฅ
4. Syncer (๋๊ธฐํ ์์คํ
)
Syncer๋ Sonamu์ ํต์ฌ ์์ง์
๋๋ค. ํ์ผ ๋ณ๊ฒฝ์ ๊ฐ์งํ๊ณ ์๋์ผ๋ก ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.
// Syncer์ ์ฃผ์ ์ญํ
1. ํ์ผ ๋ณ๊ฒฝ ๊ฐ์ง (Checksum ๊ธฐ๋ฐ)
- entity.json ๋ณ๊ฒฝ โ Types ์ฌ์์ฑ
- model.ts ๋ณ๊ฒฝ โ Service ์ฌ์์ฑ
- types.ts ๋ณ๊ฒฝ โ Web์ผ๋ก ๋ณต์ฌ
2. ์ฝ๋ ์๋ ์์ฑ
- sonamu.generated.ts
- sonamu.generated.sso.ts
- {Entity}Service.ts (ํ๋ก ํธ์๋)
3. ํ์ผ ๋๊ธฐํ
- api/src/application โ web/src/services
- ํ์
ํ์ผ ๋ณต์ฌ
- sonamu.shared.ts ๋ฐฐํฌ
4. HMR ํธ๋ฆฌ๊ฑฐ
- ๋ณ๊ฒฝ๋ ๋ชจ๋ ๋ฌดํจํ
- API ์๋ฒ ์๋ ์ฌ์์
Syncer ๋์ ์์
- ์๋:
pnpm dev ์คํ ์ค ํ์ผ ๋ณ๊ฒฝ ์ (HMR)
- ์๋:
pnpm sync ๋ช
๋ น์ด ์คํ ์
5. API Layer (REST API)
Model์ @api ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ์๋์ผ๋ก REST API๋ฅผ ์์ฑํฉ๋๋ค.
// Model ๋ฉ์๋
@api({ httpMethod: "GET", clients: ["axios"] })
async findById(subset: UserSubsetKey, id: number): Promise<User> {
// ...
}
// โ ์๋ ๋ณํ
// REST API ์๋ํฌ์ธํธ
GET /api/users/:id?subset=A
// Request
curl http://localhost:1028/api/users/1?subset=A
// Response
{
"id": 1,
"email": "user@example.com",
"name": "ํ๊ธธ๋",
"created_at": "2025-01-06T12:00:00Z"
}
์๋ ์์ฑ๋๋ ๊ฒ๋ค
- โ
REST API ๋ผ์ฐํธ
- โ
์์ฒญ ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ (Zod)
- โ
์๋ต ํ์
- โ
์๋ฌ ํธ๋ค๋ง
- โ
API ๋ฌธ์ (sonamu.generated.http)
6. Frontend Service (ํ๋ก ํธ์๋ ํตํฉ)
Model์ API๋ ์๋์ผ๋ก ํ๋ก ํธ์๋ Service๋ก ์์ฑ๋ฉ๋๋ค.
web/src/services/UserService.ts (์๋ ์์ฑ)
export class UserService {
static async findById(subset: UserSubsetKey, id: number): Promise<User> {
const res = await axios.get(`/api/users/${id}`, {
params: { subset },
});
return res.data;
}
static async changePassword(
userId: number,
newPassword: string,
): Promise<void> {
await axios.post("/api/users/changePassword", {
userId,
newPassword,
});
}
}
ํ์
์์ ์ฑ
๋ฐฑ์๋์ ํ์
์ด ํ๋ก ํธ์๋์ ๊ทธ๋๋ก ๋๊ธฐํ๋์ด, ํ์
๋ถ์ผ์น ์ค๋ฅ๋ฅผ ์ปดํ์ผ ํ์์ ๋ฐ๊ฒฌํ ์ ์์ต๋๋ค!
๊ฐ๋ฐ ํ๋ก์ฐ
์ค์ ๊ฐ๋ฐ ์ Sonamu๊ฐ ์ด๋ป๊ฒ ์๋ํ๋์ง ์ดํด๋ด
์๋ค:
1. ์ํฐํฐ ์ ์ (Sonamu UI)
{
"entityId": "Post",
"properties": [
{ "name": "id", "type": "int", "isPrimary": true },
{ "name": "title", "type": "varchar" }
]
}
์ ์ฅ ์ ์๋ ์์ฑ:
post.types.ts
sonamu.generated.ts ์
๋ฐ์ดํธ
2. ๋ง์ด๊ทธ๋ ์ด์
(Migration ํญ)
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);
์คํ ์:
- ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ
์ด๋ธ ์์ฑ
3. Model ์์ฑ (์ง์ ๋๋ ์ค์บํด๋ฉ)
@api({ httpMethod: "GET" })
async findById(id: number): Promise<Post> {
return await this.db().where("id", id).first();
}
์ ์ฅ ์ ์๋ ์์ฑ:
- REST API:
GET /api/posts/:id
PostService.ts (ํ๋ก ํธ์๋)
sonamu.generated.http ์
๋ฐ์ดํธ
4. ํ๋ก ํธ์๋ ์ฌ์ฉ
// ํ์
์์ ํ๊ฒ API ํธ์ถ
const post = await PostService.findById(1);
console.log(post.title); // โ
ํ์
์ฒดํฌ
console.log(post.invalid); // โ ์ปดํ์ผ ์๋ฌ!
ํ์
์์ ์ฑ:
- ๋ฐฑ์๋ ํ์
๋ณ๊ฒฝ ์ ํ๋ก ํธ์๋์์ ์ฆ์ ์ค๋ฅ ๊ฐ์ง
HMR (Hot Module Replacement)
Sonamu๋ ๊ฐ๋ ฅํ HMR ์์คํ
์ ์ ๊ณตํฉ๋๋ค.
HMR ์๋ ๋ฐฉ์
// 1. ํ์ผ ๋ณ๊ฒฝ ๊ฐ์ง
user.model.ts ์์ โ Watcher ๊ฐ์ง
// 2. ๋ชจ๋ ๋ฌดํจํ
hmr-hook์ด ์์กด์ฑ ๊ทธ๋ํ ๋ถ์
user.model.ts์ ์์กด ๋ชจ๋๋ค invalidate
// 3. ์ฌ์์ฑ (Syncer)
- services.generated.ts ์ฌ์์ฑ (ํ๋ก ํธ์๋)
- sonamu.generated.ts ์
๋ฐ์ดํธ (ํ์
)
- API ๋ผ์ฐํธ ์ฌ๋ฑ๋ก
// 4. ์๋ฒ ์ฌ์์
graceful shutdown โ reload
HMR์ ์ด์
- โ
๋น ๋ฅธ ํผ๋๋ฐฑ - ์ฝ๋ ๋ณ๊ฒฝ ํ 1-2์ด ๋ด ๋ฐ์
- โ
์ํ ์ ์ง - ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ๋ฑ ์ ์ง
- โ
์๋ ๋๊ธฐํ - ํ๋ก ํธ์๋ Service ์๋ ์
๋ฐ์ดํธ
์๋ ์์ฑ ๋ฉ์ปค๋์ฆ
Sonamu๊ฐ ์๋์ผ๋ก ์์ฑํ๋ ๊ฒ๋ค์ ์ ๋ฆฌํด๋ด
์๋ค:
์ํฐํฐ ์ ์ ๋ณ๊ฒฝ ์
user.entity.json ์์
โ
์๋ ์์ฑ:
โ
user.types.ts (ํ์
์ ์)
โ
sonamu.generated.ts (Base ์คํค๋ง)
โ
sonamu.generated.sso.ts (Subset ์ฟผ๋ฆฌ)
โ
migration SQL (ํ
์ด๋ธ ๋ณ๊ฒฝ)
Model ํ์ผ ๋ณ๊ฒฝ ์
user.model.ts ์์
โ
์๋ ์์ฑ:
โ
UserService.ts (ํ๋ก ํธ์๋)
โ
services.generated.ts (Service ํตํฉ)
โ
sonamu.generated.http (API ๋ฌธ์)
โ
REST API ๋ผ์ฐํธ ์ฌ๋ฑ๋ก
Types ํ์ผ ๋ณ๊ฒฝ ์
user.types.ts ์์
โ
์๋ ๋๊ธฐํ:
โ
web/src/services/user.types.ts (๋ณต์ฌ)
โ
ํ๋ก ํธ์๋์์ ์ฆ์ ์ฌ์ฉ ๊ฐ๋ฅ
์ ๋ ์์ ํ๋ฉด ์ ๋๋ ํ์ผ
sonamu.generated.ts - ๋ค์ sync ์ ๋ฎ์ด์์์ง
{Entity}Service.ts - ๋ค์ sync ์ ๋ฎ์ด์์์ง
sonamu.generated.sso.ts - ๋ค์ sync ์ ๋ฎ์ด์์์ง
์ด ํ์ผ๋ค์ ํญ์ ์๋์ผ๋ก ์์ฑ๋๋ฏ๋ก ์ง์ ์์ ํ์ง ๋ง์ธ์!
ํ์
์์ ์ฑ์ ํ๋ฆ
Sonamu์ End-to-End ํ์
์์ ์ฑ์ ์๊ฐํํ๋ฉด:
// 1. Entity ์ ์
{
"name": "email",
"type": "varchar"
}
โ
// 2. ํ์
์์ฑ (์๋)
type User = {
email: string;
}
โ
// 3. Model ๋ฉ์๋ (ํ์
์์ )
async findByEmail(email: string): Promise<User> {
return await this.db().where("email", email).first();
}
โ
// 4. REST API (์๋ ์์ฑ, ํ์
์์ )
GET /api/users/email/:email
Response: User
โ
// 5. Frontend Service (์๋ ์์ฑ, ํ์
์์ )
static async findByEmail(email: string): Promise<User> {
const res = await axios.get(`/api/users/email/${email}`);
return res.data; // ํ์
: User
}
โ
// 6. React Component (ํ์
์์ )
const user = await UserService.findByEmail("test@example.com");
console.log(user.email); // โ
ํ์
์ฒดํฌ
console.log(user.invalid); // โ ์ปดํ์ผ ์๋ฌ!
ํ์
์์ ์ฑ ๋ณด์ฅ
- ์ํฐํฐ ์ ์ โ ํ์
์์ฑ
- Model โ API ํ์
์ถ๋ก
- API โ Service ํ์
๋๊ธฐํ
- Service โ UI ํ์
์ฒดํฌ
์ด๋ ๋จ๊ณ์์๋ ํ์
์ด ๋ณ๊ฒฝ๋๋ฉด, ์ ์ฒด ์ฒด์ธ์ ๋ฐ์๋์ด ์ปดํ์ผ ํ์์ ์ค๋ฅ๋ฅผ ๋ฐ๊ฒฌํฉ๋๋ค!
Sonamu์ ์ฃผ์ ์ฅ์
1. ๊ฐ๋ฐ ์๋ ํฅ์
์ ํต์ ์ธ ๋ฐฉ๋ฒ:
1. ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ ์ค๊ณ
2. Migration ์์ฑ
3. ๋ฐฑ์๋ ํ์
์ ์
4. API ์ปจํธ๋กค๋ฌ ์์ฑ
5. API ๋ผ์ฐํธ ๋ฑ๋ก
6. ํ๋ก ํธ์๋ ํ์
์ ์
7. API ํด๋ผ์ด์ธํธ ์์ฑ
โฑ๏ธ ์ด ์์ ์๊ฐ: 2-3์๊ฐ
Sonamu ๋ฐฉ๋ฒ:
1. ์ํฐํฐ ์ ์ (Sonamu UI)
2. ์ค์บํด๋ฉ (Model ์๋ ์์ฑ)
โฑ๏ธ ์ด ์์ ์๊ฐ: 10-15๋ถ
โ
์ฝ 90% ์๊ฐ ๋จ์ถ!
2. ํ์
์์ ์ฑ
// โ ์ ํต์ ์ธ ๋ฐฉ๋ฒ: ๋ฐํ์ ์๋ฌ
const user = await fetch("/api/users/1").then(r => r.json());
console.log(user.eamil); // ์คํ! ๋ฐํ์์๋ง ๋ฐ๊ฒฌ โ ๏ธ
// โ
Sonamu: ์ปดํ์ผ ์๋ฌ
const user = await UserService.findById("A", 1);
console.log(user.eamil); // ์ปดํ์ผ ์๋ฌ! ์ฆ์ ๋ฐ๊ฒฌ โ
3. ์ ์ง๋ณด์์ฑ
// ์ํฐํฐ ๋ณ๊ฒฝ: email โ username
// โ ์ ํต์ ์ธ ๋ฐฉ๋ฒ:
// 1. DB Migration ์์ฑ
// 2. ๋ฐฑ์๋ ํ์
์์
// 3. API ์์
// 4. ํ๋ก ํธ์๋ ํ์
์์
// 5. API ํธ์ถ๋ถ ์ ๋ถ ์์
// โ ๏ธ ๋๋ฝ๋ ๊ณณ์ ๋ฐํ์ ์๋ฌ!
// โ
Sonamu:
// 1. entity.json์์ ํ๋๋ช
๋ณ๊ฒฝ
// 2. Migration ์๋ ์์ฑ
// 3. ๋ชจ๋ ํ์
์๋ ์
๋ฐ์ดํธ
// 4. ์ปดํ์ผ ์๋ฌ๋ก ์์ ํ์ํ ๊ณณ ์ฆ์ ํ์
// โ
๋น ๋จ๋ฆฐ ๊ณณ ์์ด ์์ ํ๊ฒ ๋ณ๊ฒฝ!
4. ์ผ๊ด์ฑ
// โ
Sonamu๋ ์ฝ๋ ์คํ์ผ์ด ์๋์ผ๋ก ์ผ๊ด๋ฉ๋๋ค
// ๋ชจ๋ Service๊ฐ ๋์ผํ ํจํด
UserService.findById(...)
PostService.findById(...)
CommentService.findById(...)
// ๋ชจ๋ ํ์
์ด ๋์ผํ ๊ตฌ์กฐ
type User = z.infer<typeof UserBaseSchema>;
type Post = z.infer<typeof PostBaseSchema>;
type Comment = z.infer<typeof CommentBaseSchema>;
์ ์ฝ์ฌํญ๊ณผ ํธ๋ ์ด๋์คํ
Sonamu์ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ ์ผ๋ถ ์ ์ฝ์ฌํญ์ ๋๋ฐํฉ๋๋ค:
์์๋์ด์ผ ํ ์ ์ฝ์ฌํญ
- ํ์ต ๊ณก์ : Sonamu์ ๊ฐ๋
๊ณผ ๊ท์น์ ์ดํดํด์ผ ํฉ๋๋ค
- ์๋ ์์ฑ ํ์ผ ์์ ๋ถ๊ฐ: ์๋ ์์ ์ ๋ฎ์ด์์์ง๋๋ค
- ์ํฐํฐ ์ค์ฌ ์ค๊ณ ํ์: Sonamu ๋ฐฉ์์ ๋ฐ๋ผ์ผ ํฉ๋๋ค
- ๋ณต์กํ ์ฟผ๋ฆฌ: ๋งค์ฐ ๋ณต์กํ ๊ฒฝ์ฐ Raw SQL ์ฌ์ฉ ํ์
ํ์ง๋ง ์ฅ์ ์ด ํจ์ฌ ํฝ๋๋คโ
๊ฐ๋ฐ ์๋ ํฅ์ (90% ์๊ฐ ๋จ์ถ)
โ
ํ์
์์ ์ฑ ๋ณด์ฅ
โ
์ ์ง๋ณด์์ฑ ํฅ์
โ
์ฝ๋ ์ผ๊ด์ฑ
โ
ํ ์์ฐ์ฑ ํฅ์
๋ค์ ๋จ๊ณ
Sonamu์ ์๋ ์๋ฆฌ๋ฅผ ์ดํดํ๋ค๋ฉด, ์ด์ ๊ฐ ๊ตฌ์ฑ ์์๋ฅผ ์์ธํ ์์๋ณด์ธ์: