๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ์—†์ด ๊ฐœ๋ฐœํ•˜๊ธฐ

Sonamu๋กœ ๊ฐœ๋ฐœํ•  ๋•Œ๋Š” ์ด๋ ‡์Šต๋‹ˆ๋‹ค:
  1. ์ฝ”๋“œ ์ˆ˜์ •
  2. ์ €์žฅ (Cmd+S)
  3. ๋! ๐Ÿ‘ˆ ๋ฐ”๋กœ ๋ฐ˜์˜๋จ
๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๊ฑฐ๋‚˜, ์„œ๋ฒ„๋ฅผ ์žฌ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜, ๋นŒ๋“œ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

์‹ค์ œ ๊ฐœ๋ฐœ ์†๋„ ์ฐจ์ด

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");
}
์ˆœ์„œ๊ฐ€ ๋ณด์žฅ๋˜๋ฏ€๋กœ:
  1. ์ฝ”๋“œ ์ƒ์„ฑ์ด ์™„์ „ํžˆ ๋๋‚œ ํ›„
  2. ์žฌ๋กœ๋“œ๊ฐ€ ์‹œ์ž‘๋จ
  3. ํ•ญ์ƒ ์ตœ์‹  ์ฝ”๋“œ๊ฐ€ ๋กœ๋“œ๋จ โœ…
์ด๊ฒƒ์ด 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()์œผ๋กœ ํ‘œ์‹œ๋˜์–ด ์žˆ์–ด ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ์žฌ์‹œ์ž‘์„ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค.