μλ² μ¬μμ μμ΄ κ°λ°νκΈ°
Sonamuλ‘ κ°λ°ν λλ μ΄λ μ΅λλ€:
- μ½λ μμ
- μ μ₯ (Cmd+S)
- λ! π λ°λ‘ λ°μλ¨
λΈλΌμ°μ λ₯Ό μλ‘κ³ μΉ¨νκ±°λ, μλ²λ₯Ό μ¬μμνκ±°λ, λΉλ λͺ
λ Ήμ΄λ₯Ό μ€νν νμκ° μμ΅λλ€.
μ€μ κ°λ° μλ μ°¨μ΄
HMR μμ΄ (μ ν΅μ λ°©μ):
# Entity μμ β μλ² μ¬μμ β ν
μ€νΈ
1. Sonamu UIμμ Entity μμ
2. Ctrl+C (μλ² μ’
λ£)
3. pnpm dev (μλ² μ¬μμ)
4. 20~30μ΄ λκΈ°...
5. ν
μ€νΈ
λ°λ³΅ν λλ§λ€ 30μ΄μ© λλΉ π«
HMR μμ λ (Sonamu λ°©μ):
# Entity μμ β μ¦μ λ°μ
1. Sonamu UIμμ Entity μμ
2. 2μ΄ ν μ½μμ π Invalidated λ‘κ·Έ
3. ν
μ€νΈ
λ°λ³΅ν΄λ 2μ΄λ©΄ μΆ©λΆ π
ν루μ Entity/APIλ₯Ό 50λ² μμ νλ€λ©΄?
- HMR μμ΄: 50 Γ 30μ΄ = 25λΆ λλΉ
- HMR μμ λ: 50 Γ 2μ΄ = 1.7λΆ
λ€λ₯Έ νλ μμν¬μμ μ°¨μ΄
| νλ μμν¬ | HMR μ§μ | μλ μμ± μ½λ μ±ν¬ | Entity λ³κ²½ λ°μ |
|---|
| NestJS | β μμ | - | μλ μ¬μμ νμ |
| Express | β μμ | - | nodemon μ¬μμ |
| Fastify | β οΈ λΆλΆ μ§μ | - | μλ μ¬μμ νμ |
| Sonamu | β
μμ μ§μ | β
μλ | β
μ¦μ λ°μ |
Sonamuλ§μ νΉλ³ν¨
μΌλ°μ μΈ Node.js HMR:
- λ¨μν νμΌ μ¬λ‘λλ§ ν¨
- μλ μμ± μ½λλ μ§μ κ΄λ¦¬ νμ
- Entity λ³κ²½ μ μ¬λ¬ νμΌ μλ μμ
Sonamu HMR:
- νμΌ μ¬λ‘λ + Syncer μλ μ€ν
- Entity λ³κ²½ β λͺ¨λ κ΄λ ¨ μ½λ μλ μμ±/μ
λ°μ΄νΈ
- μμ‘΄νλ λͺ¨λ νμΌ μλ μ¬λ‘λ
- API μλ μ¬λ±λ‘
μ€μ κ°λ° μλ리μ€
μλλ¦¬μ€ 1: Entity νλ μΆκ°νκΈ°
User Entityμ nickname νλλ₯Ό μΆκ°νλ κ³Όμ :
1λ¨κ³: Sonamu UIμμ νλ μΆκ°
// User Entityμ nickname μΆκ°
nickname: StringProp({ maxLength: 50 });
2λ¨κ³: μ μ₯νλ©΄ μ¦μ
user.entity.ts μλ μ
λ°μ΄νΈ
user.types.ts νμ
μλ μμ±
user.zod.ts Zod μ€ν€λ§ μλ μμ±
UserModel μλ μ¬λ‘λ
- λͺ¨λ UserApi λ©μλ μλ μ¬λ±λ‘
- νλ‘ νΈμλ
UserService μλ μ
λ°μ΄νΈ
3λ¨κ³: μ½μ νμΈ
π Invalidated:
- src/application/user/user.entity.ts
- src/application/user/user.model.ts (with 8 APIs)
- src/application/user/user.api.ts
β
All files are synced!
4λ¨κ³: ν
μ€νΈ
# μλ² μ¬μμ μμ΄ λ°λ‘ API νΈμΆ
curl http://localhost:3000/api/user/1
{
"id": 1,
"name": "John",
"nickname": null // π μ νλκ° μ¦μ λ°μλ¨!
}
β±οΈ HMR μμ λ: 2μ΄
β±οΈ HMR μμ λ: 30μ΄ (μλ² μ¬μμ + μ¬μ»΄νμΌ)
μλλ¦¬μ€ 2: API λ‘μ§ μμ νκΈ°
UserApi.list()μ νμ΄μ§λ€μ΄μ
κ³Ό κ²μ κΈ°λ₯ μΆκ°:
// user.api.ts μμ μ
@api({ httpMethod: "GET" })
async list() {
return UserModel.findMany();
}
// user.api.ts μμ ν
@api({ httpMethod: "GET" })
async list(listParams: ListParams & { keyword?: string }) {
const where = listParams.keyword
? { name: { $like: `%${listParams.keyword}%` } }
: undefined;
return UserModel.findMany({
where,
limit: listParams.num,
offset: (listParams.page - 1) * listParams.num,
});
}
μ μ₯νλ©΄:
π Invalidated:
- src/application/user/user.api.ts
β¨ API re-registered: GET /api/user/list
μ¦μ Postman/Thunder Clientμμ ν
μ€νΈ κ°λ₯!
GET /api/user/list?keyword=john&page=1&num=10
# λ°λ‘ λμν¨ β
μλλ¦¬μ€ 3: Model λΉμ¦λμ€ λ‘μ§ μΆκ°
Userμ νμ± μν μ²΄ν¬ λ‘μ§ μΆκ°:
// user.model.ts
export class UserModel extends BaseModelClass {
static async findActive() {
return this.findMany({
where: { status: "active", deletedAt: null },
});
}
isActive(): boolean {
return this.status === "active" && !this.deletedAt;
}
}
μ μ₯ β 2μ΄ ν β λ€λ₯Έ APIμμ λ°λ‘ μ¬μ© κ°λ₯:
// post.api.ts
@api({ httpMethod: "POST" })
async create(body: PostForm) {
const user = await UserModel.findById(body.userId);
if (!user.isActive()) { // π λ°©κΈ μΆκ°ν λ©μλ λ°λ‘ μ¬μ©!
throw new BadRequestError("User is not active");
}
return PostModel.save(body);
}
Sonamu HMRμ νμ : Syncer ν΅ν©
μΌλ°μ μΈ Node.js HMRμ βνμΌμ΄ λ°λμλ€? λ€μ λΆλ¬μ€μβκ° μ λΆμ
λλ€.
νμ§λ§ Sonamuλ λ€λ¦
λλ€.
λ¬Έμ : μλ μμ± μ½λμ λλ λ§
Sonamuλ Entity κΈ°λ°μΌλ‘ μλ§μ μ½λλ₯Ό μλ μμ±ν©λλ€:
- TypeScript νμ
νμΌ (
*.types.ts)
- Zod μ€ν€λ§ (
*.zod.ts)
- API λΌμ°νΈ λ±λ‘
- νλ‘ νΈμλ Service ν΄λμ€
λ§μ½ Syncerμ HMRμ΄ λ°λ‘ λμνλ€λ©΄:
User Entity μμ
β
Syncerκ° νμΌ μμ± μμ...
β
HMRμ΄ κ°μ§ν΄μ μ¬λ‘λ μμ... β π¨ μμ§ μμ± μ€μΈλ°!
β
νμ΄λ° κΌ¬μ β μ€λ₯ λ°μ
Sonamuμ ν΄κ²°μ±
Syncerκ° HMRμ μ§μ μ μ΄ν©λλ€:
// syncer.ts
async syncFromWatcher(event: string, diffFilePath: AbsolutePath) {
// 1. HMRμκ² νμΌ λ¬΄ν¨ν μμ²
const invalidatedPaths = await hot.invalidateFile(diffFilePath, event);
// 2. μλ μμ± μ½λ μ±ν¬ μλ£ (μμ 보μ₯!)
await this.doSyncActions([diffFilePath]);
// 3. μ΄μ μμ νκ² λͺ¨λ μ¬λ‘λ
await this.autoloadTypes();
await this.autoloadModels();
await this.autoloadApis();
await this.autoloadWorkflows();
// 4. μλ£!
this.eventEmitter.emit("onHMRCompleted");
}
μμκ° λ³΄μ₯λλ―λ‘:
- μ½λ μμ±μ΄ μμ ν λλ ν
- μ¬λ‘λκ° μμλ¨
- νμ μ΅μ μ½λκ° λ‘λλ¨ β
μ΄κ²μ΄ Sonamu HMRμ΄ λ¨μν βλΉ λ₯Έβ κ²μ λμ΄ μμ νκ³ λ―Ώμ μ μλ μ΄μ μ
λλ€.
HMR μν€ν
μ²
μ£Όμ μ»΄ν¬λνΈ
Sonamuμ HMR μμ€ν
μ μΈ κ°μ§ ν΅μ¬ μ»΄ν¬λνΈλ‘ ꡬμ±λ©λλ€:
1. @sonamu-kit/hmr-hook
hot-hookμ forkνμ¬ Sonamuμ λ§κ² 컀μ€ν°λ§μ΄μ§ν ν¨ν€μ§μ
λλ€.
// hmr-hook-register.ts
if (process.env.HOT === "yes" && process.env.API_ROOT_PATH) {
const { hot } = await import("@sonamu-kit/hmr-hook");
await hot.init({
rootDirectory: process.env.API_ROOT_PATH,
boundaries: [`./src/**/*.ts`], // λͺ¨λ .ts νμΌμ΄ HMR λμ
});
console.log("π₯ HMR-hook initialized");
}
Node.jsμ Module Loader APIλ₯Ό μ¬μ©νμ¬ λͺ¨λ λ‘λ© κ³Όμ μ κ°μ
ν©λλ€.
2. Dependency Tree
νμΌ κ° μμ‘΄μ±μ νΈλ¦¬ κ΅¬μ‘°λ‘ μΆμ ν©λλ€:
user.model.ts
ββ user.api.ts
ββ post.api.ts
ββ admin.api.ts
user.model.tsκ° λ³κ²½λλ©΄ μμ‘΄νλ 3κ° νμΌλ ν¨κ» μ¬λ‘λλ©λλ€.
μμ: user.model.tsκ° λ³κ²½λλ©΄ μμ‘΄νλ 3κ° νμΌ(user.api.ts, post.api.ts, admin.api.ts)λ ν¨κ» μ¬λ‘λλ©λλ€.
3. Syncer
νμΌ λ³κ²½μ κ°μ§νκ³ HMRκ³Ό μ½λ μμ±μ μ‘°μ¨ν©λλ€:
// νμΌ μμ²
watcher.on("change", async (filePath) => {
await syncer.syncFromWatcher("change", filePath);
});
HMR νλ‘μΈμ€ μμΈ
κ°λ°μκ° νμΌμ μμ νλ©΄ λ€μ κ³Όμ μ΄ μλμΌλ‘ μ€νλ©λλ€:
1. νμΌ λ³κ²½ κ°μ§
// chokidarλ‘ νμΌμμ€ν
κ°μ
watcher.on("change", (filePath) => {
console.log(`File changed: ${filePath}`);
});
2. λͺ¨λ 무ν¨ν
// ESM μΊμ μ κ±°
const invalidatedPaths = await hot.invalidateFile(diffFilePath, "change");
// ["src/user/user.model.ts", "src/user/user.api.ts", ...]
λ³κ²½λ νμΌμ ESM import μΊμλ₯Ό μ κ±°νκ³ , Dependency Treeλ₯Ό νμνμ¬ μμ‘΄νλ λͺ¨λ νμΌμ μΊμλ μ κ±°ν©λλ€.
3. μλ μμ± μ½λ μ±ν¬
Entityλ Modelμ΄ λ³κ²½λ κ²½μ°, Syncerκ° κ΄λ ¨ μ½λλ₯Ό μλ μμ±ν©λλ€:
await this.doSyncActions([diffFilePath]);
μμ±λλ νμΌ:
- TypeScript νμ
νμΌ (
user.types.ts)
- Zod μ€ν€λ§ (
user.zod.ts)
- νλ‘ νΈμλ Service (
UserService.ts)
4. λͺ¨λ μ¬λ‘λ
await this.autoloadTypes();
await this.autoloadModels();
await this.autoloadApis();
await this.autoloadWorkflows();
무ν¨νλ λͺ¨λμ μΊμκ° μ κ±°λ μνμ΄λ―λ‘ μ΅μ μ½λκ° λ‘λλ©λλ€.
5. API μ¬λ±λ‘
Model νμΌμ΄ λ³κ²½λλ©΄ ν΄λΉ Modelμ λͺ¨λ APIλ₯Ό μ¬λ±λ‘ν©λλ€:
removeInvalidatedRegisteredApis(invalidatedPath: AbsolutePath) {
const entityId = EntityManager.getEntityIdFromPath(invalidatedPath);
const toRemove = registeredApis.filter((api) => api.modelName === `${entityId}Model`);
for (const api of toRemove) {
registeredApis.splice(registeredApis.indexOf(api), 1);
}
return toRemove;
}
μ½μ μΆλ ₯:
π Invalidated:
- src/user/user.model.ts (with 8 APIs)
μλ³Έ hot-hookκ³Όμ μ°¨μ΄μ
Sonamuμ @sonamu-kit/hmr-hookμ μλ³Έ hot-hookμ forkνμ¬ λ€μκ³Ό κ°μ΄ κ°μ νμ΅λλ€:
1. λ³μ κΈ°λ° λμ Import νμ©
// μλ³Έ hot-hook: β λΆκ°λ₯
const modelPath = `./models/${entityId}.model`;
await import(modelPath); // μλ¬ λ°μ!
// Sonamu hmr-hook: β
κ°λ₯
const modelPath = `./models/${entityId}.model`;
await import(modelPath); // μ μ λμ
Sonamuλ Entity κΈ°λ° κ΅¬μ‘°λΌ κ²½λ‘λ₯Ό λμ μΌλ‘ μμ±ν΄μΌ νλ―λ‘ μ΄ κΈ°λ₯μ΄ νμμ μ
λλ€.
2. Boundary κ° μ μ Import νμ©
// user.model.ts (boundary)
import { PostModel } from "./post.model"; // μλ³Έ: β / Sonamu: β
export class UserModel extends BaseModelClass {
async getPosts() {
return PostModel.findMany({ where: { userId: this.id } });
}
}
Model κ° μ°Έμ‘°κ° μμ£Ό λ°μνλ―λ‘ μ μ importλ₯Ό νμ©νμ΅λλ€.
3. νμΌμμ€ν
μμ² λΉνμ±ν
μλ³Έμ μ체 νμΌ μμ²λ₯Ό μ¬μ©νμ§λ§, Sonamuλ Syncerμ μμ²λ§ μ¬μ©ν©λλ€:
// Syncerκ° νμΌ λ³κ²½μ κ°μ§νλ©΄ HMRμ μ§μ μλ¦Ό
await hot.invalidateFile(filePath, "change");
μ΄λ₯Ό ν΅ν΄ μ½λ μμ±κ³Ό λͺ¨λ 무ν¨νμ μμλ₯Ό μ νν μ μ΄ν©λλ€.
4. ts-loaderμμ ν΅ν© κ°μ
TypeScript μ»΄νμΌ ν dist/*.js κ²½λ‘μ μλ³Έ src/*.ts κ²½λ‘μ λΆμΌμΉλ₯Ό ν΄κ²°νμ΅λλ€.
μ±λ₯ μ΅μ ν
μ νμ 무ν¨ν
λ³κ²½λ νμΌκ³Ό κ·Έ μμ‘΄μ±λ§ 무ν¨ννλ―λ‘ λΆνμν μ¬λ‘λλ₯Ό λ°©μ§ν©λλ€:
// user.types.ts λ³κ²½ β user.model.tsλ§ μ¬λ‘λ
// μ 체 νλ‘μ νΈκ° μλ μν₯λ°λ νμΌλ§!
체ν¬μ¬ κΈ°λ° λ³κ²½ κ°μ§
μ€μ λ΄μ©μ΄ λ³κ²½λ νμΌλ§ μ²λ¦¬ν©λλ€:
const changedFiles = await findChangedFilesUsingChecksums();
if (changedFiles.length === 0) {
console.log(chalk.black.bgGreen("All files are synced!"));
return;
}
νμΌμ΄ μ μ₯λμμ΄λ λ΄μ©μ΄ κ°μΌλ©΄ μ±ν¬νμ§ μμ΅λλ€.
Graceful Shutdown μ²λ¦¬
μ±ν¬ μμ
μ€ νλ‘μΈμ€ μ’
λ£λ₯Ό λ°©μ§ν©λλ€:
await runWithGracefulShutdown(
async () => {
await this.doSyncActions(changedFiles);
await renewChecksums();
},
{ whenThisHappens: "SIGUSR2", waitForUpTo: 20000 },
);
Nodemon μ¬μμ μκ·Έλμ λ°μλ μ±ν¬κ° μλ£λ λκΉμ§ 20μ΄κ° λκΈ°ν©λλ€.
HMR νμ±ν
κ°λ° λͺ¨λμμ μλμΌλ‘ νμ±νλ©λλ€:
pnpm dev # HOT=yesκ° μλ μ€μ λ¨
νκ²½λ³μλ‘ μ μ΄ν μλ μμ΅λλ€:
# HMR νμ±ν
HOT=yes pnpm dev
# HMR λΉνμ±ν (λλ²κΉ
μ)
HOT=no pnpm dev
νλ‘λμ
λΉλμμλ HMRμ΄ μλμΌλ‘ λΉνμ±νλλ©°, μ μ λΉλκ° μμ±λ©λλ€.
κ°λ° ν
HMR μν νμΈ
// ν°λ―Έλμ μΆλ ₯λλ λ‘κ·Έ νμΈ
π₯ HMR-hook initialized
// νμΌ λ³κ²½ μ
π Invalidated:
- src/user/user.model.ts (with 8 APIs)
- src/user/user.api.ts
β
All files are synced!
λλ €μ§ κ² κ°λ€λ©΄
νμΌμ μμ νλλ° λ°μμ΄ λλ €μ§ κ² κ°λ€λ©΄:
# μμ‘΄μ± νΈλ¦¬ νμΈ
const dump = await hot.dump();
console.log(`Total modules: ${dump.length}`);
console.log(`Boundaries: ${dump.filter(d => d.boundary).length}`);
μμ‘΄μ±μ΄ λ무 λ§μΌλ©΄ import ꡬ쑰λ₯Ό κ°μ νμΈμ.
μ¬μμμ΄ νμν λ³κ²½μ¬ν
λ€μ νμΌλ€μ μμ νλ©΄ μλ²λ₯Ό μ¬μμν΄μΌ ν©λλ€:
sonamu.config.ts - μ€μ νμΌ
.env - νκ²½λ³μ
package.json - μμ‘΄μ±
μ΄λ° νμΌλ€μ import.meta.hot?.decline()μΌλ‘ νμλμ΄ μμ΄ λ³κ²½ μ μλμΌλ‘ μ¬μμμ μꡬν©λλ€.