HMR ์ฌ์ฉ ์ค ๋ฐ์ํ ์ ์๋ ๋ฌธ์ ์ ์ค์ ํด๊ฒฐ ๋ฐฉ๋ฒ์
๋๋ค.
๋ณ๊ฒฝ์ฌํญ์ด ๋ฐ์๋์ง ์์
// user.model.ts ์์
export class UserModel extends BaseModel {
isActive(): boolean {
return this.status === "active"; // ์ด ๋ก์ง์ ์ถ๊ฐํ๋๋ฐ...
}
}
์ ์ฅํ์ง๋ง API ํธ์ถ ์ ์ฌ์ ํ isActive is not a function ์๋ฌ๊ฐ ๋ฐ์ํฉ๋๋ค.
์์ธ 1: ์ ์ Import ์ฌ์ฉ
์ด๋๊ฐ์์ UserModel์ ์ ์ ์ผ๋ก importํ์ ๊ฐ๋ฅ์ฑ์ด ๋์ต๋๋ค.
ํ์ธ ๋ฐฉ๋ฒ:
# ํ๋ก์ ํธ์์ ์ ์ import ์ฐพ๊ธฐ
grep -r "import.*UserModel.*from" src/
# ์ถ๋ ฅ ์์:
# src/some-file.ts:import { UserModel } from "./user.model"; โ ๐จ ์ด๊ฒ ๋ฌธ์ !
ํด๊ฒฐ ๋ฐฉ๋ฒ: ๋์ import๋ก ๋ณ๊ฒฝ
// Before - โ
import { UserModel } from "./application/user/user.model";
app.get("/users", async (req, res) => {
const users = await UserModel.findMany();
res.json(users);
});
// After - โ
app.get("/users", async (req, res) => {
const { UserModel } = await import("./application/user/user.model");
const users = await UserModel.findMany();
res.json(users);
});
Sonamu์ Syncer๋ Entity ๊ธฐ๋ฐ ํ์ผ๋ค์ ์๋์ผ๋ก ๋์ importํ๋ฏ๋ก, ์ผ๋ฐ์ ์ธ Model/API ํ์ผ์์๋ ์ด ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค. ์ฃผ๋ก ์ปค์คํ
์คํฌ๋ฆฝํธ๋ ์ด๊ธฐํ ์ฝ๋์์ ๋ฐ์ํฉ๋๋ค.
์์ธ 2: ์ฝ์์ ์๋ฌ๊ฐ ์จ์ด์์
HMR ์ค ์๋ฌ๊ฐ ๋ฐ์ํ์ง๋ง ๋์ณค์ ์ ์์ต๋๋ค.
ํ์ธ ๋ฐฉ๋ฒ:
ํฐ๋ฏธ๋์ ์คํฌ๋กคํด์ ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ์๋์ง ํ์ธ:
๐ Invalidated:
- src/user/user.model.ts
โ Error loading module: /path/to/user.model.ts
SyntaxError: Unexpected token '}'
at Module._compile (internal/modules/cjs/loader.js:1137:14)
ํด๊ฒฐ ๋ฐฉ๋ฒ:
์๋ฌ ๋ฉ์์ง์ ํ์ผ๊ณผ ๋ผ์ธ ๋ฒํธ๋ฅผ ํ์ธํ๊ณ ์์ ํฉ๋๋ค.
์์ธ 3: ์บ์๊ฐ ๊ผฌ์
๋๋ฌผ์ง๋ง ESM ์บ์๊ฐ ์์ ํ ์ ๊ฑฐ๋์ง ์์ ์ ์์ต๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ: ์๋ฒ ์ฌ์์
# Ctrl+C๋ก ์ข
๋ฃ ํ
pnpm dev
โFile not imported dynamicallyโ ์๋ฌ
FileNotImportedDynamicallyException: File /path/to/user.model.ts must be imported dynamically
Boundary ํ์ผ(user.model.ts)์ด ์ด๋๊ฐ์์ ์ ์ import๋ก ๋ก๋๋์์ต๋๋ค.
๋น ๋ฅธ ํด๊ฒฐ
์ต์
1: ์ ์ import๋ฅผ ๋์ import๋ก ๋ณ๊ฒฝ (๊ถ์ฅ)
์๋ฌ ๋ฉ์์ง์ ๋์จ ํ์ผ์ ์ฐพ์์:
// Before
import { UserModel } from "./user.model";
// After
const { UserModel } = await import("./user.model");
์ต์
2: ํด๋น ํ์ผ์ Boundary์์ ์ ์ธ
์ ๋ง ๋์ import๋ก ๋ฐ๊ฟ ์ ์๋ ๊ฒฝ์ฐ:
// hmr-hook-register.ts
await hot.init({
rootDirectory: process.env.API_ROOT_PATH,
boundaries: [
`./src/**/*.ts`,
`!./src/config/**/*`, // config ํด๋ ์ ์ธ
],
});
์ต์
3: ์๋ฌ๋ฅผ ๋ฌด์ (๊ถ์ฅํ์ง ์์)
await hot.init({
boundaries: [`./src/**/*.ts`],
throwWhenBoundariesAreNotDynamicallyImported: false, // ์๋ฌ ๋ฌด์
});
์ต์
3์ ์ฌ์ฉํ๋ฉด ํด๋น ํ์ผ์ HMR์ด ์๋ํ์ง ์์ต๋๋ค. ๊ฐ๋ฐ ํธ์์ฑ์ด ํฌ๊ฒ ๋จ์ด์ง๋ฏ๋ก ๊ถ์ฅํ์ง ์์ต๋๋ค.
์ฌ๋ก๋ ์ ๋ฉ๋ชจ๋ฆฌ ์ฆ๊ฐ
# ์ฒ์
RSS: 150MB, Heap: 80MB
# ํ์ผ์ 10๋ฒ ์์ ํ
RSS: 350MB, Heap: 200MB # ๐จ ๋ฉ๋ชจ๋ฆฌ๊ฐ ๊ณ์ ์ฆ๊ฐ!
์ด์ ๋ชจ๋์ ๋ฆฌ์์ค(ํ์ด๋จธ, ์ด๋ฒคํธ ๋ฆฌ์ค๋, ์ฐ๊ฒฐ ๋ฑ)๊ฐ ์ ๋ฆฌ๋์ง ์์ ๋์๊ฐ ๋ฐ์ํฉ๋๋ค.
ํ์ธ ๋ฐฉ๋ฒ:
// ์ฌ๋ก๋ํ ๋๋ง๋ค ์คํ
console.log("Active handles:", process._getActiveHandles().length);
console.log("Active requests:", process._getActiveRequests().length);
// ์ซ์๊ฐ ๊ณ์ ์ฆ๊ฐํ๋ฉด ๋ฆฌ์์ค ๋์ ๋ฐ์ ์ค!
ํด๊ฒฐ ๋ฐฉ๋ฒ
import.meta.hot.dispose()๋ก ๋ฆฌ์์ค๋ฅผ ์ ๋ฆฌํฉ๋๋ค:
์์ 1: ํ์ด๋จธ
// notification-polling.ts
const timer = setInterval(async () => {
await checkNewNotifications();
}, 5000);
// โ
์ฌ๋ก๋ ์ ํ์ด๋จธ ์ ๋ฆฌ
import.meta.hot?.dispose(() => {
clearInterval(timer);
console.log("โจ Timer cleaned up");
});
์์ 2: WebSocket
// websocket-client.ts
const ws = new WebSocket("ws://notification-server.com");
ws.on("message", (data) => {
console.log("Notification:", data);
});
// โ
์ฌ๋ก๋ ์ ์ฐ๊ฒฐ ์ข
๋ฃ
import.meta.hot?.dispose(() => {
ws.close();
console.log("โจ WebSocket closed");
});
์์ 3: ์ด๋ฒคํธ ๋ฆฌ์ค๋
// event-handler.ts
import { EventEmitter } from "events";
const emitter = new EventEmitter();
function handleData(data: any) {
console.log("Data received:", data);
}
emitter.on("data", handleData);
// โ
์ฌ๋ก๋ ์ ๋ฆฌ์ค๋ ์ ๊ฑฐ
import.meta.hot?.dispose(() => {
emitter.removeListener("data", handleData);
console.log("โจ Event listener removed");
});
์์ 4: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปค๋ฅ์
ํ
// custom-db-pool.ts
import { Pool } from "pg";
const pool = new Pool({
host: "localhost",
port: 5432,
database: "mydb",
});
// โ
์ฌ๋ก๋ ์ ํ ์ข
๋ฃ
import.meta.hot?.dispose(async () => {
await pool.end();
console.log("โจ DB pool closed");
});
API๊ฐ ์ค๋ณต ๋ฑ๋ก๋จ
โ ๏ธ Warning: Route POST /api/user/create is already registered
โ ๏ธ Warning: Route GET /api/user/list is already registered
์๋ฒ ๋ก๊ทธ์ ๊ฐ์ API๊ฐ ์ฌ๋ฌ ๋ฒ ๋ฑ๋ก๋์๋ค๋ ๊ฒฝ๊ณ ๊ฐ ํ์๋ฉ๋๋ค.
Model ํ์ผ ์ฌ๋ก๋ ์ ๊ธฐ์กด API๊ฐ ์ ๊ฑฐ๋์ง ์์์ต๋๋ค.
Syncer ๋ก๊ทธ์์ API ์ ๊ฑฐ ์ฌ๋ถ ํ์ธ:
๐ Invalidated:
- src/user/user.model.ts (with 8 APIs) # โ API ๊ฐ์๊ฐ ํ์๋์ด์ผ ํจ
# ๋ง์ฝ "with X APIs"๊ฐ ์๋ค๋ฉด ์ ๊ฑฐ๋์ง ์์ ๊ฒ!
ํด๊ฒฐ ๋ฐฉ๋ฒ
์ผ๋ฐ์ ์ผ๋ก Syncer๊ฐ ์๋ ์ฒ๋ฆฌํ์ง๋ง, ๋ฌธ์ ๊ฐ ๊ณ์๋๋ฉด:
1. ์๋ฒ ์ฌ์์
# ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ
Ctrl+C
pnpm dev
2. Syncer ๋ก์ง ํ์ธ (๋๋ฌธ ๊ฒฝ์ฐ)
// syncer.ts ํ์ธ
removeInvalidatedRegisteredApis(invalidatedPath: AbsolutePath) {
if (!invalidatedPath.endsWith(".model.ts")) {
return [];
}
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;
}
์ด ๋ก์ง์ด ์คํ๋์ง ์๋๋ค๋ฉด Sonamu ๋ฒ๊ทธ์ผ ์ ์์ต๋๋ค. GitHub Issues์ ๋ณด๊ณ ํด์ฃผ์ธ์.
์ํ ์์กด์ฑ ๋ฌธ์
# ํ์ผ์ ์์ ํ๋ฉด
๐ Invalidated:
- src/user/user.model.ts
- src/post/post.model.ts
- src/user/user.model.ts # โ ๐จ ๋ค์ ๋ฑ์ฅ!
- src/post/post.model.ts # โ ๐จ ๋ฌดํ ๋ฐ๋ณต!
# ๋๋
RangeError: Maximum call stack size exceeded
HMR์ด ๋ฌดํ ๋ฃจํ์ ๋น ์ง๊ฑฐ๋ ์ผ๋ถ ๋ชจ๋์ด ๋ก๋๋์ง ์์ต๋๋ค.
Model ๊ฐ ์ํ ์ฐธ์กฐ:
// user.model.ts
import { PostModel } from "../post/post.model";
export class UserModel extends BaseModel {
async getPosts() {
return PostModel.findMany({ where: { userId: this.id } });
}
}
// post.model.ts
import { UserModel } from "../user/user.model"; // โ ๐จ ์ํ ์ฐธ์กฐ!
export class PostModel extends BaseModel {
async getAuthor() {
return UserModel.findById(this.userId);
}
}
ํด๊ฒฐ ๋ฐฉ๋ฒ
์ต์
1: ํ์
๋ง import (๊ถ์ฅ)
// user.model.ts
import type { PostSaved } from "../post/post.types"; // โ
ํ์
๋ง
export class UserModel extends BaseModel {
async getPosts(): Promise<PostSaved[]> {
// ๋์ import๋ก ์ค์ ํด๋์ค ๋ก๋
const { PostModel } = await import("../post/post.model");
return PostModel.findMany({ where: { userId: this.id } });
}
}
// post.model.ts
import type { UserSaved } from "../user/user.types"; // โ
ํ์
๋ง
export class PostModel extends BaseModel {
async getAuthor(): Promise<UserSaved> {
const { UserModel } = await import("../user/user.model");
return UserModel.findById(this.userId);
}
}
์ต์
2: ๊ณตํต ํ์
ํ์ผ ๋ถ๋ฆฌ
// shared-types.ts
export type UserSaved = { /* ... */ };
export type PostSaved = { /* ... */ };
// user.model.ts
import type { PostSaved } from "./shared-types";
// post.model.ts
import type { UserSaved } from "./shared-types";
์ต์
3: ์์กด์ฑ ๋ฐฉํฅ ์ฌ์ค๊ณ
// post.model.ts์์๋ง user.model.ts๋ฅผ ์ฐธ์กฐ
import { UserModel } from "../user/user.model"; // โ
๋จ๋ฐฉํฅ
export class PostModel extends BaseModel {
async getAuthor() {
return UserModel.findById(this.userId);
}
}
// user.model.ts์์๋ post.model.ts๋ฅผ ์ฐธ์กฐํ์ง ์์
// (ํ์ํ๋ฉด PostApi์์ ์ฒ๋ฆฌ)
ํน์ ํ์ผ๋ง HMR์ด ์ ๋จ
// user.model.ts ์์ โ โ
HMR ์๋
// post.model.ts ์์ โ โ
HMR ์๋
// admin-helper.ts ์์ โ โ ๋ฐ์ ์ ๋จ
์์ธ 1: Boundary ํจํด์ ๋งค์นญ๋์ง ์์
ํ์ธ:
const dump = await hot.dump();
const helper = dump.find(d => d.nodePath.includes("admin-helper.ts"));
if (!helper) {
console.log("โ ์ด ํ์ผ์ Boundary๊ฐ ์๋๋๋ค!");
}
ํด๊ฒฐ:
// hmr-hook-register.ts
await hot.init({
rootDirectory: process.env.API_ROOT_PATH,
boundaries: [
`./src/**/*.ts`, // ์ด๋ฏธ ํฌํจ๋์ด ์์ด์ผ ํจ
],
});
์์ธ 2: ํ์ผ์ด import๋์ง ์์
HMR์ ์ค์ ๋ก import๋ ํ์ผ๋ง ์ถ์ ํฉ๋๋ค.
ํ์ธ:
const dump = await hot.dump();
console.log(`Total tracked files: ${dump.length}`);
// admin-helper.ts๊ฐ ๋ชฉ๋ก์ ์๋ค๋ฉด ์๋ฌด๋ importํ์ง ์๋ ๊ฒ!
ํด๊ฒฐ:
ํด๋น ํ์ผ์ ์ด๋๊ฐ์์ importํ๊ฑฐ๋, ํ์ ์๋ค๋ฉด ์ญ์ ํฉ๋๋ค.
SSR ํ์ผ ๋ณ๊ฒฝ ์ ์ฌ๋ก๋ ์ ๋จ
// src/ssr/routes.ts ์์
// ์ ์ฅํด๋ ๋ฐ์ ์ ๋จ
SSR ํ์ผ์ ํน๋ณํ ์ฒ๋ฆฌ๋ฉ๋๋ค.
Syncer๊ฐ SSR ํ์ผ ๋ณ๊ฒฝ์ ๊ฐ์งํ๋ฉด ์๋์ผ๋ก ์ฒ๋ฆฌํ์ง๋ง, ์๋ ์ฌ๋ก๋๊ฐ ํ์ํ ๊ฒฝ์ฐ:
# ์๋ฒ ์ฌ์์
Ctrl+C
pnpm dev
๋๋ ์ฝ๋์์:
// syncer.ts์์ SSR ํ์ผ ์ฒ๋ฆฌ ํ์ธ
if (diffFilePath.includes("/src/ssr/")) {
console.log("SSR config changed - reloading...");
await hot.invalidateFile(diffFilePath, event);
await this.autoloadSSRRoutes();
}
HMR์ด ๋๋ฌด ๋๋ฆผ
# ํ์ผ ์ ์ฅ
# 5์ด ํ...
๐ Invalidated: # โ ๐จ ๋๋ฌด ๋๋ฆผ!
์์กด์ฑ ํธ๋ฆฌ๊ฐ ๋๋ฌด ๊น๊ฑฐ๋ ๋์ด์ ๋ง์ ํ์ผ์ด ๋ฌดํจํ๋ฉ๋๋ค.
ํ์ธ:
const dump = await hot.dump();
const userModel = dump.find(d => d.nodePath.includes("user.model.ts"));
console.log(`Dependencies: ${userModel?.children?.length}`);
// 100๊ฐ ์ด์์ด๋ฉด ๋ฌธ์ !
ํด๊ฒฐ ๋ฐฉ๋ฒ
1. import ์ต์ํ
// โ ๋์ ์ - ๋ชจ๋ Model import
import { UserModel } from "./user.model";
import { PostModel } from "./post.model";
import { CommentModel } from "./comment.model";
import { CategoryModel } from "./category.model";
import { TagModel } from "./tag.model";
// ... 20๊ฐ ๋
// โ
์ข์ ์ - ํ์ํ ๊ฒ๋ง
import { UserModel } from "./user.model";
import { PostModel } from "./post.model";
2. lodash ๋ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ import ์ต์ ํ
// โ ๋์ ์
import * as lodash from "lodash";
// โ
์ข์ ์
import { pick, omit } from "lodash";
3. Boundary ๋ฒ์ ์ถ์
// hmr-hook-register.ts
await hot.init({
rootDirectory: process.env.API_ROOT_PATH,
boundaries: [
// ๋ชจ๋ ํ์ผ ๋์ ์์ฃผ ๋ณ๊ฒฝ๋๋ ๊ฒ๋ง
`./src/**/*.model.ts`,
`./src/**/*.api.ts`,
],
});
๋๋ฒ๊น
ํ
HMR ๋ก๊ทธ ํ์ฑํ
์์ธํ HMR ๋์์ ํ์ธํ๋ ค๋ฉด:
DEBUG=hmr-hook:* pnpm dev
์ถ๋ ฅ ์์:
hmr-hook:loader File change src/user/user.model.ts
hmr-hook:dependency_tree Invalidating /path/to/user.model.ts
hmr-hook:dependency_tree Finding dependents...
hmr-hook:dependency_tree Found 3 dependent files
hmr-hook:loader Invalidated 3 files
์์กด์ฑ ํธ๋ฆฌ ํ์ธ
const dump = await hot.dump();
// ํน์ ํ์ผ์ ์์กด์ฑ ํ์ธ
const userModel = dump.find(d => d.nodePath.includes("user.model.ts"));
console.log("Children:", userModel?.children);
// ์ ์ฒด ํธ๋ฆฌ๋ฅผ JSON์ผ๋ก ์ ์ฅ
import { writeFileSync } from "fs";
writeFileSync("dependency-tree.json", JSON.stringify(dump, null, 2));
๋ฌดํจํ๋ ํ์ผ ์ถ์
Syncer๊ฐ ์ฝ์์ ์ถ๋ ฅํ๋ ๋ก๊ทธ๋ฅผ ์ฃผ์๊น๊ฒ ํ์ธ:
๐ Invalidated:
- src/user/user.model.ts (with 8 APIs)
- src/user/user.api.ts
- src/post/post.api.ts # โ ์ post.api.ts๊น์ง?
# user.model.ts๊ฐ ๋ณ๊ฒฝ๋์๋๋ฐ post.api.ts๋ ์ฌ๋ก๋๋จ
# โ post.api.ts์์ UserModel์ ์ฌ์ฉ ์ค
๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ชจ๋ํฐ๋ง
// memory-monitor.ts
setInterval(() => {
const used = process.memoryUsage();
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] Memory:`, {
rss: `${Math.round(used.rss / 1024 / 1024)}MB`,
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
external: `${Math.round(used.external / 1024 / 1024)}MB`,
});
}, 10000); // 10์ด๋ง๋ค
๋ง์ง๋ง ์๋จ: ์์ ์ฌ์์
๋ชจ๋ ํด๊ฒฐ ๋ฐฉ๋ฒ์ด ์คํจํ ๊ฒฝ์ฐ:
# 1. ํ๋ก์ธ์ค ์ข
๋ฃ
Ctrl+C
# 2. ๋น๋ ๋๋ ํ ๋ฆฌ ์ญ์
rm -rf dist/
# 3. ์ฒดํฌ์ฌ ์ญ์
rm -rf .sonamu/checksums/
# 4. node_modules ์ญ์ (์ ๋ง ์ฌ๊ฐํ ๊ฒฝ์ฐ)
rm -rf node_modules/
pnpm install
# 5. ์ฌ์์
pnpm dev
๋์ ์์ฒญํ๊ธฐ
์ฌ์ ํ ๋ฌธ์ ๊ฐ ํด๊ฒฐ๋์ง ์์ผ๋ฉด:
1. GitHub Issues์ ๋ณด๊ณ ํ์ธ์
์๋ ๋งํฌ์์ ์๋ก์ด ์ด์๋ฅผ ์์ฑํด์ฃผ์ธ์:
https://github.com/cartanova-ai/sonamu/issues
2. ๋ค์ ์ ๋ณด๋ฅผ ํฌํจํด์ฃผ์ธ์
๋ฌธ์ ํด๊ฒฐ์ ์ํด ๋ค์ ์ ๋ณด๋ฅผ ํจ๊ป ์ ๊ณตํด์ฃผ์ธ์:
- Sonamu ๋ฒ์ (
package.json)
- Node.js ๋ฒ์ (
node -v)
- ๋ฐ์ํ ์๋ฌ ๋ฉ์์ง
- ์ฌํ ๊ฐ๋ฅํ ์ต์ ์์
- HMR ๋ก๊ทธ (
DEBUG=hmr-hook:* pnpm dev)
3. ํ์ผ ๊ตฌ์กฐ ์์๋ฅผ ์์ฑํด์ฃผ์ธ์
๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ํ์ผ ๊ตฌ์กฐ๋ฅผ ์๋์ ๊ฐ์ด ๋ช
ํํ๊ฒ ํ์ํด์ฃผ์ธ์:
์์ ์ค๋ช
์ ํจ๊ป ์์ฑํด์ฃผ์ธ์:
์ฆ์:
- user.model.ts๋ฅผ ์์ ํ๊ณ ์ ์ฅ
- ์ฝ์์ "Invalidated: user.model.ts" ํ์๋จ
- ํ์ง๋ง user.api.ts์์ ์ฌ์ ํ ์ด์ ์ฝ๋ ์คํ
- ์๋ฒ ์ฌ์์ํ๋ฉด ์ ์ ์๋
์๋ํ ํด๊ฒฐ ๋ฐฉ๋ฒ:
1. ๋์ import ํ์ธ โ ์ด๋ฏธ ๋์ ์ผ๋ก ๋ก๋๋จ
2. ์๋ฒ ์ฌ์์ โ ์์๋ก ํด๊ฒฐ๋์ง๋ง ๋ค์ ๋ฐ์
3. ์บ์ ์ญ์ (rm -rf dist/) โ ํจ๊ณผ ์์
HMR ๋ก๊ทธ:
[๋ก๊ทธ ๋ด์ฉ ์ฒจ๋ถ]
์ด๋ ๊ฒ ์์ธํ๊ฒ ์์ฑํ๋ฉด ๋น ๋ฅธ ๋ต๋ณ์ ๋ฐ์ ์ ์์ต๋๋ค!