์๋ฒ ์ฌ์์ ์์ด ๊ฐ๋ฐํ๊ธฐ
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 BaseModel {
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 BaseModel {
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()์ผ๋ก ํ์๋์ด ์์ด ๋ณ๊ฒฝ ์ ์๋์ผ๋ก ์ฌ์์์ ์๊ตฌํฉ๋๋ค.