๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
VSCode Extension์ธ Naite Viewer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘ ๊ธฐ๋ก๋œ ๋กœ๊ทธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์‹œ๊ฐํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

Naite Viewer ๊ฐœ์š”

์‹ค์‹œ๊ฐ„ ์‹œ๊ฐํ™”

ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘๋กœ๊ทธ ์ฆ‰์‹œ ํ‘œ์‹œ

Unix Socket ํ†ต์‹ 

ํ”„๋กœ์„ธ์Šค ๊ฐ„ ํ†ต์‹ ๋น ๋ฅธ ์ „์†ก

ํ”„๋กœ์ ํŠธ๋ณ„ ๊ฒฉ๋ฆฌ

์†Œ์ผ“ ํŒŒ์ผ ๋ถ„๋ฆฌ๋…๋ฆฝ์  ์šด์˜

์ž๋™ ์—ฐ๊ฒฐ

Extension ์‹คํ–‰ ์‹œ์ž๋™ ์†Œ์ผ“ ์ƒ์„ฑ

Naite Viewer๋ž€?

Naite Viewer๋Š” Sonamu VSCode Extension์— ํฌํ•จ๋œ ๊ธฐ๋Šฅ์œผ๋กœ, ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘ ๊ธฐ๋ก๋œ Naite ๋กœ๊ทธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค. Naite๋Š” ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘ Naite.t()๋กœ ๊ธฐ๋กํ•œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ VSCode Extension์œผ๋กœ ์ „์†กํ•˜์—ฌ, ๊ฐœ๋ฐœ์ž๊ฐ€ ํ…Œ์ŠคํŠธ ํ๋ฆ„์„ ์‹œ๊ฐ์ ์œผ๋กœ ์ถ”์ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด๋‚˜ ์—ฌ๋Ÿฌ ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด ์–ฝํžŒ ์ƒํ™ฉ์—์„œ ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์š” ๊ธฐ๋Šฅ

1

์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ ํ‘œ์‹œ

ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘ Naite.t()๋กœ ๊ธฐ๋ก๋œ ๋ชจ๋“  ๋กœ๊ทธ๋ฅผ ์ฆ‰์‹œ VSCode ํŒจ๋„์— ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ฝ˜์†”์—์„œ ๋กœ๊ทธ๋ฅผ ์ฐพ์„ ํ•„์š” ์—†์ด ๊ตฌ์กฐํ™”๋œ ํ˜•ํƒœ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
2

ํ…Œ์ŠคํŠธ๋ณ„ ๊ทธ๋ฃนํ™”

๊ฐ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ณ„๋กœ ๋กœ๊ทธ๋ฅผ ์ž๋™ ๊ทธ๋ฃนํ™”ํ•ฉ๋‹ˆ๋‹ค. Suite ๋‹จ์œ„๋กœ๋„ ๋ถ„๋ฅ˜๋˜์–ด ๋Œ€๊ทœ๋ชจ ํ…Œ์ŠคํŠธ์—์„œ๋„ ์›ํ•˜๋Š” ๋กœ๊ทธ๋ฅผ ๋น ๋ฅด๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
3

์ฝœ์Šคํƒ ์ถ”์ 

๊ฐ ๋กœ๊ทธ์˜ ํ˜ธ์ถœ ์œ„์น˜์™€ ์ „์ฒด ์ฝœ์Šคํƒ ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๋กœ๊ทธ๋ฅผ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ์ฝ”๋“œ ์œ„์น˜๋กœ ๋ฐ”๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
4

ํ•„ํ„ฐ๋ง ๋ฐ ๊ฒ€์ƒ‰

wildcard ํŒจํ„ด(user:*)์œผ๋กœ ํŠน์ • ๋ชจ๋“ˆ์˜ ๋กœ๊ทธ๋งŒ ํ•„ํ„ฐ๋งํ•˜๊ฑฐ๋‚˜, ํ‚ค์›Œ๋“œ๋กœ ๋กœ๊ทธ ๋‚ด์šฉ์„ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„ํ‚คํ…์ฒ˜

1. Unix Socket ํ†ต์‹ 

Naite Viewer๋Š” Unix Socket์„ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ํ”„๋กœ์„ธ์Šค์™€ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์€ ํŒŒ์ผ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ํ”„๋กœ์„ธ์Šค ๊ฐ„ ์ง์ ‘ ํ†ต์‹ ์ด ๊ฐ€๋Šฅํ•˜์—ฌ ๋น ๋ฅด๊ณ  ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ์†Œ์ผ“ ํ†ต์‹ ์˜ ์ด์ :
  • ๋น ๋ฅธ ์ „์†ก: HTTP๋‚˜ ํŒŒ์ผ๋ณด๋‹ค ํ›จ์”ฌ ๋น ๋ฅธ IPC(Inter-Process Communication)
  • ์‹ค์‹œ๊ฐ„์„ฑ: ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ฆ‰์‹œ ๋กœ๊ทธ๊ฐ€ Extension์— ์ „๋‹ฌ๋จ
  • ๊ฒฉ๋ฆฌ: ํ”„๋กœ์ ํŠธ๋ณ„ ๋…๋ฆฝ๋œ ์†Œ์ผ“์œผ๋กœ ์ถฉ๋Œ ๋ฐฉ์ง€
์†Œ์ผ“ ๊ฒฝ๋กœ ๊ทœ์น™:
~/.sonamu/naite-{hash}.sock
{hash}๋Š” sonamu.config.ts ๊ฒฝ๋กœ์˜ MD5 ํ•ด์‹œ ์•ž 8์ž๋ฆฌ์ž…๋‹ˆ๋‹ค.
๊ฐ ํ”„๋กœ์ ํŠธ๋Š” sonamu.config.ts ํŒŒ์ผ์˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ณ ์œ ํ•œ ํ•ด์‹œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค:
import { createHash } from "crypto";

const configPath = "/project/api/src/sonamu.config.ts";
const hash = createHash("md5")
  .update(configPath)
  .digest("hex")
  .slice(0, 8);

// ์˜ˆ: "a1b2c3d4"
// ์ตœ์ข… ์†Œ์ผ“ ๊ฒฝ๋กœ: ~/.sonamu/naite-a1b2c3d4.sock
์ด ๋ฐฉ์‹์œผ๋กœ ์—ฌ๋Ÿฌ Sonamu ํ”„๋กœ์ ํŠธ๋ฅผ ๋™์‹œ์— ์‹คํ–‰ํ•ด๋„ ๊ฐ๊ฐ ๋…๋ฆฝ๋œ ์†Œ์ผ“์„ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ๊ทธ๊ฐ€ ์„ž์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

2. ๋ฉ”์‹œ์ง€ ํ”„๋กœํ† ์ฝœ

ํ…Œ์ŠคํŠธ ํ”„๋กœ์„ธ์Šค์™€ Extension ๊ฐ„์—๋Š” 3๊ฐ€์ง€ ํƒ€์ž…์˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์ „์†ก๋ฉ๋‹ˆ๋‹ค.

run/start - ํ…Œ์ŠคํŠธ ๋Ÿฐ ์‹œ์ž‘

ํ…Œ์ŠคํŠธ ๋Ÿฐ์ด ์‹œ์ž‘๋  ๋•Œ ์ „์†ก๋ฉ๋‹ˆ๋‹ค. Extension์€ ์ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์œผ๋ฉด ๊ธฐ์กด ๋กœ๊ทธ๋ฅผ ๋ชจ๋‘ ํด๋ฆฌ์–ดํ•˜๊ณ  ์ƒˆ๋กœ์šด ํ…Œ์ŠคํŠธ ๋Ÿฐ์„ ์ค€๋น„ํ•ฉ๋‹ˆ๋‹ค.
{
  type: "run/start",
  startedAt: "2025-01-08T12:34:56.789Z"
}
Watch ๋ชจ๋“œ์—์„œ ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์žฌ์‹คํ–‰๋˜๋Š”๋ฐ, ๋งค๋ฒˆ run/start ๋ฉ”์‹œ์ง€๊ฐ€ ์ „์†ก๋˜์–ด Viewer๊ฐ€ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.

test/result - ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ

๊ฐ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ๋งˆ๋‹ค ์ „์†ก๋ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ์™€ ํ•จ๊ป˜ Naite ๋กœ๊ทธ(traces)๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.
{
  type: "test/result",
  receivedAt: "2025-01-08T12:34:57.123Z",
  suiteName: "UserModel",
  suiteFilePath: "/Users/.../user.model.test.ts",
  testName: "์‚ฌ์šฉ์ž ์ƒ์„ฑ",
  testFilePath: "/Users/.../user.model.test.ts",
  testLine: 15,
  status: "pass",
  duration: 123,
  traces: [
    {
      key: "user:create:input",
      value: { username: "john" },
      filePath: "/Users/.../user.model.test.ts",
      lineNumber: 20,
      at: "2025-01-08T12:34:57.100Z"
    }
  ]
}
ํฌํ•จ ์ •๋ณด:
  • ํ…Œ์ŠคํŠธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ: Suite๋ช…, ํŒŒ์ผ ๊ฒฝ๋กœ, ๋ผ์ธ ๋ฒˆํ˜ธ
  • ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ: ์„ฑ๊ณต/์‹คํŒจ ์ƒํƒœ, ์†Œ์š” ์‹œ๊ฐ„, ์—๋Ÿฌ ์ •๋ณด
  • Naite traces: Naite.t()๋กœ ๊ธฐ๋ก๋œ ๋ชจ๋“  ๋กœ๊ทธ

run/end - ํ…Œ์ŠคํŠธ ๋Ÿฐ ์ข…๋ฃŒ

๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์ „์†ก๋ฉ๋‹ˆ๋‹ค. Extension์€ ์ด ๋ฉ”์‹œ์ง€๋กœ ํ…Œ์ŠคํŠธ ๋Ÿฐ์ด ๋๋‚ฌ์Œ์„ ์ธ์ง€ํ•ฉ๋‹ˆ๋‹ค.
{
  type: "run/end",
  endedAt: "2025-01-08T12:34:58.789Z"
}

3. ์ „์†ก ํ๋ฆ„

์ „์ฒด ํ…Œ์ŠคํŠธ ์‹คํ–‰ ๊ณผ์ •์—์„œ ๋ฉ”์‹œ์ง€๊ฐ€ ์ „์†ก๋˜๋Š” ์ˆœ์„œ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
// 1. ํ…Œ์ŠคํŠธ ์‹œ์ž‘ - ์†Œ์ผ“ ์—ฐ๊ฒฐ ๋ฐ run/start ์ „์†ก
beforeAll(async () => {
  await NaiteReporter.startTestRun();
  // โ†’ send({ type: "run/start" })
});

// 2. ํ…Œ์ŠคํŠธ ์‹คํ–‰ - Naite ๋กœ๊ทธ ์ˆ˜์ง‘
test("์‚ฌ์šฉ์ž ์ƒ์„ฑ", async () => {
  Naite.t("user:create:input", { username: "john" });
  
  const { user } = await userModel.create({ username: "john" });
  
  Naite.t("user:create:output", { userId: user.id });
  
  // ํ…Œ์ŠคํŠธ ์ข…๋ฃŒ ํ›„ afterEach ์‹คํ–‰
});

// 3. ํ…Œ์ŠคํŠธ ์ข…๋ฃŒ - traces ์ˆ˜์ง‘ ๋ฐ ์ „์†ก
afterEach(async ({ task }) => {
  // getAllTraces()๋กœ ๋ชจ๋“  Naite ๋กœ๊ทธ ์ˆ˜์ง‘
  task.meta.traces = Naite.getAllTraces();
  
  // Extension์œผ๋กœ ์ „์†ก
  await NaiteReporter.reportTestResult({
    testName: task.name,
    status: task.result?.state,
    traces: task.meta.traces,
    // ...
  });
  // โ†’ send({ type: "test/result", traces: [...] })
});

// 4. ์ „์ฒด ๋Ÿฐ ์ข…๋ฃŒ - run/end ์ „์†ก ๋ฐ ์†Œ์ผ“ ์ข…๋ฃŒ
afterAll(() => {
  await NaiteReporter.endTestRun();
  // โ†’ send({ type: "run/end" })
  // โ†’ socket.end()
});
๋ฒ„ํผ๋ง ๋ฉ”์ปค๋‹ˆ์ฆ˜: Extension์ด ์•„์ง ์‹œ์ž‘๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ์†Œ์ผ“ ์—ฐ๊ฒฐ์ด ์ง€์—ฐ๋˜๋Š” ๊ฒฝ์šฐ, NaiteReporter๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋ฒ„ํผ์— ์ €์žฅํ–ˆ๋‹ค๊ฐ€ ์—ฐ๊ฒฐ ์„ฑ๊ณต ์‹œ ํ•œ ๋ฒˆ์— ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด Extension์„ ๋Šฆ๊ฒŒ ์ผœ๋„ ์ด์ „ ํ…Œ์ŠคํŠธ ๋กœ๊ทธ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„ค์น˜ ๋ฐ ์„ค์ •

1. Extension ์„ค์น˜

1

VSCode Marketplace์—์„œ ์„ค์น˜

  1. VSCode ์™ผ์ชฝ ์‚ฌ์ด๋“œ๋ฐ”์—์„œ Extensions ์•„์ด์ฝ˜ ํด๋ฆญ
  2. โ€œSonamuโ€ ๊ฒ€์ƒ‰
  3. Install ๋ฒ„ํŠผ ํด๋ฆญ
2

Extension ํ™œ์„ฑํ™” ํ™•์ธ

์„ค์น˜ ํ›„ VSCode ํ•˜๋‹จ ์ƒํƒœ๋ฐ”์— Sonamu ์•„์ด์ฝ˜์ด ๋‚˜ํƒ€๋‚˜๋ฉด ์ •์ƒ์ ์œผ๋กœ ํ™œ์„ฑํ™”๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

2. ์ž๋™ ์—ฐ๊ฒฐ

Extension์ด ์‹คํ–‰๋˜๋ฉด ์ž๋™์œผ๋กœ ์†Œ์ผ“ ์„œ๋ฒ„๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค:
1

ํ”„๋กœ์ ํŠธ ๊ฐ์ง€

ํ˜„์žฌ ์›Œํฌ์ŠคํŽ˜์ด์Šค์—์„œ sonamu.config.ts ํŒŒ์ผ์„ ์ฐพ์Šต๋‹ˆ๋‹ค.
2

ํ•ด์‹œ ๊ณ„์‚ฐ

sonamu.config.ts์˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋กœ MD5 ํ•ด์‹œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (์•ž 8์ž๋ฆฌ).
3

์†Œ์ผ“ ์„œ๋ฒ„ ์‹œ์ž‘

~/.sonamu/naite-{hash}.sock ๊ฒฝ๋กœ์— Unix Socket ์„œ๋ฒ„๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
4

ํ…Œ์ŠคํŠธ ํ”„๋กœ์„ธ์Šค ๋Œ€๊ธฐ

ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์‹œ NaiteReporter๊ฐ€ ์ด ์†Œ์ผ“์œผ๋กœ ์—ฐ๊ฒฐ๋ฉ๋‹ˆ๋‹ค.
Extension์€ ํ”„๋กœ์ ํŠธ ๊ฒฝ๋กœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์†Œ์ผ“์„ ์ƒ์„ฑํ•˜๋ฏ€๋กœ, ํ”„๋กœ์ ํŠธ ํด๋”๋ฅผ ์ง์ ‘ ์—ด์–ด์•ผ ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ์ƒ์œ„ ํด๋”๋ฅผ ์—ด๋ฉด ์†Œ์ผ“ ๊ฒฝ๋กœ๊ฐ€ ๋‹ฌ๋ผ์ ธ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3. ํ…Œ์ŠคํŠธ ์‹คํ–‰

pnpm test
๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ํ•œ ๋ฒˆ ์‹คํ–‰ํ•˜๊ณ  ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์‹œ ์ž๋™์œผ๋กœ Extension๊ณผ ์—ฐ๊ฒฐ๋˜์–ด ๋กœ๊ทธ๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ๋ฒ•

1. Naite Viewer ํŒจ๋„ ์—ด๊ธฐ

VSCode์—์„œ Naite Viewer ํŒจ๋„ ์—ด๊ธฐ

Command Palette์—์„œ โ€˜Naite: Open Viewerโ€™๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ํŒจ๋„์„ ์—ฌ๋Š” ๋ชจ์Šต

  1. Cmd+Shift+P (macOS) ๋˜๋Š” Ctrl+Shift+P (Windows/Linux)
  2. โ€œNaite: Open Viewerโ€ ์ž…๋ ฅ
  3. Enter

2. ๋กœ๊ทธ ํ™•์ธ

ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์‹œ Naite Viewer์— ๋กœ๊ทธ๊ฐ€ ์ž๋™์œผ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

Viewer ํ™”๋ฉด ๊ตฌ์„ฑ

Naite Viewer๋Š” ํ…Œ์ŠคํŠธ๋ฅผ 3๋‹จ๊ณ„ ๊ณ„์ธต์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค:
Naite Viewer ๋ฉ”์ธ ํ™”๋ฉด

์‹ค์ œ Naite Viewer์—์„œ ํ…Œ์ŠคํŠธ ๋กœ๊ทธ๊ฐ€ ํ‘œ์‹œ๋˜๋Š” ๋ชจ์Šต (Suite > Test > Trace ๊ณ„์ธต ๊ตฌ์กฐ)

์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ: Watch ๋ชจ๋“œ์—์„œ ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์ž๋™ ์žฌ์‹คํ–‰๋˜๊ณ , Viewer๋„ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ ๋ณ€๊ฒฝ์˜ ์˜ํ–ฅ์„ ๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3. ์ฝœ์Šคํƒ ํ™•์ธ

๋กœ๊ทธ๋ฅผ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น Naite.t() ํ˜ธ์ถœ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
user:create:input
{ username: "john", email: "[email protected]" }

๐Ÿ“ ์ง์ ‘ ํ˜ธ์ถœ ์œ„์น˜:
  /Users/.../user.model.test.ts:20
  
๐Ÿ“š ์ „์ฒด ์ฝœ์Šคํƒ:
  1. test (user.model.test.ts:20)
     โ†‘ ํด๋ฆญํ•˜๋ฉด ์ด ์œ„์น˜๋กœ ์ด๋™
  2. createUser (user.model.ts:45)
  3. runWithMockContext (bootstrap.ts:58)
์ฝœ์Šคํƒ์˜ ์˜๋ฏธ:
  1. test (20๋ฒˆ์งธ ์ค„): ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ Naite.t()๋ฅผ ํ˜ธ์ถœํ•œ ์œ„์น˜
  2. createUser (45๋ฒˆ์งธ ์ค„): ํ…Œ์ŠคํŠธ๊ฐ€ ํ˜ธ์ถœํ•œ ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
  3. runWithMockContext: Sonamu์˜ Context ๋ž˜ํผ (์—ฌ๊ธฐ๊นŒ์ง€๋งŒ ํ‘œ์‹œ)
Naite Viewer ์ฝœ์Šคํƒ ์ƒ์„ธ ์ •๋ณด

๋กœ๊ทธ๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ ํ‘œ์‹œ๋˜๋Š” ์ฝœ์Šคํƒ ์ •๋ณด์™€ ์ฝ”๋“œ ์œ„์น˜๋กœ ์ด๋™ํ•˜๋Š” ๊ธฐ๋Šฅ

์ฝœ์Šคํƒ์˜ ๊ฐ ํ•ญ๋ชฉ์„ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ํŒŒ์ผ์˜ ์ •ํ™•ํ•œ ๋ผ์ธ์œผ๋กœ ๋ฐ”๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋ณต์žกํ•œ ํ˜ธ์ถœ ์ฒด์ธ์„ ๋””๋ฒ„๊น…ํ•  ๋•Œ ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

4. ํ•„ํ„ฐ๋ง ๋ฐ ๊ฒ€์ƒ‰

๋Œ€๊ทœ๋ชจ ํ…Œ์ŠคํŠธ์—์„œ๋Š” ์ˆ˜๋ฐฑ ๊ฐœ์˜ ๋กœ๊ทธ๊ฐ€ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•„ํ„ฐ๋ง ๊ธฐ๋Šฅ์œผ๋กœ ์›ํ•˜๋Š” ๋กœ๊ทธ๋งŒ ๋น ๋ฅด๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
wildcard ํŒจํ„ด์œผ๋กœ ํŠน์ • ๋ชจ๋“ˆ์˜ ๋กœ๊ทธ๋งŒ ํ‘œ์‹œ:
user:*           โ†’ user:create, user:update, user:delete
syncer:*         โ†’ syncer:renderTemplate, syncer:writeFile
*:create         โ†’ user:create, post:create
syncer:*:user    โ†’ syncer:renderTemplate:user, syncer:writeFile:user
์ž…๋ ฅ์ฐฝ์— ํŒจํ„ด์„ ์ž…๋ ฅํ•˜๋ฉด ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ•„ํ„ฐ๋ง๋ฉ๋‹ˆ๋‹ค.

์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€

1. ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋””๋ฒ„๊น…

์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋ฅผ ๊ฑฐ์น˜๋Š” ๋ณต์žกํ•œ ๋กœ์ง์„ ๋””๋ฒ„๊น…ํ•  ๋•Œ ๊ฐ ๋‹จ๊ณ„์˜ ์ƒํƒœ๋ฅผ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
test("์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์ „์ฒด ํ๋ฆ„", async () => {
  // 1๋‹จ๊ณ„: ์ฃผ๋ฌธ ๊ฒ€์ฆ
  Naite.t("order:validate:start", { orderId: 123 });
  await validateOrder(123);
  Naite.t("order:validate:done", { valid: true });
  
  // 2๋‹จ๊ณ„: ์žฌ๊ณ  ํ™•์ธ
  Naite.t("order:inventory:check", { productId: 456 });
  await checkInventory(456);
  Naite.t("order:inventory:available", { quantity: 10 });
  
  // 3๋‹จ๊ณ„: ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
  Naite.t("order:payment:start", { amount: 50000 });
  await processPayment({ orderId: 123, amount: 50000 });
  Naite.t("order:payment:done", { transactionId: "tx_789" });
  
  // 4๋‹จ๊ณ„: ๋ฐฐ์†ก ์‹œ์ž‘
  Naite.t("order:shipping:start", { orderId: 123 });
  await startShipping(123);
  Naite.t("order:shipping:done", { trackingNumber: "TRK_001" });
});
Viewer์—์„œ๋Š” ์ด๋ ‡๊ฒŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค:
โœ“ ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์ „์ฒด ํ๋ฆ„ (456ms)
  โ””โ”€ order:validate:start     { orderId: 123 }
  โ””โ”€ order:validate:done      { valid: true }
  โ””โ”€ order:inventory:check    { productId: 456 }
  โ””โ”€ order:inventory:available { quantity: 10 }
  โ””โ”€ order:payment:start      { amount: 50000 }
  โ””โ”€ order:payment:done       { transactionId: "tx_789" }
  โ””โ”€ order:shipping:start     { orderId: 123 }
  โ””โ”€ order:shipping:done      { trackingNumber: "TRK_001" }
๋งŒ์•ฝ ๊ฒฐ์ œ ๋‹จ๊ณ„์—์„œ ์‹คํŒจํ–ˆ๋‹ค๋ฉด:
  • order:payment:start๊นŒ์ง€๋งŒ ๋กœ๊ทธ๊ฐ€ ์žˆ์Œ
  • ์–ด๋А ๋‹จ๊ณ„๊นŒ์ง€ ์ •์ƒ ์ง„ํ–‰๋˜์—ˆ๋Š”์ง€ ๋ช…ํ™•ํžˆ ํŒŒ์•…
  • ์ฝœ์Šคํƒ์œผ๋กœ ์ •ํ™•ํ•œ ์‹คํŒจ ์œ„์น˜ ํ™•์ธ
ํ•„ํ„ฐ ํ™œ์šฉ:
  • order:payment:* โ†’ ๊ฒฐ์ œ ๊ด€๋ จ ๋กœ๊ทธ๋งŒ ํ‘œ์‹œ
  • order:*:start โ†’ ๋ชจ๋“  ๋‹จ๊ณ„์˜ ์‹œ์ž‘ ๋กœ๊ทธ๋งŒ ํ‘œ์‹œ

2. Syncer ์ฝ”๋“œ ์ƒ์„ฑ ์ถ”์ 

Sonamu์˜ Syncer๊ฐ€ ์–ด๋–ค ํ…œํ”Œ๋ฆฟ์„ ๋ Œ๋”๋งํ•˜๊ณ  ์–ด๋–ค ํŒŒ์ผ์„ ์ƒ์„ฑํ–ˆ๋Š”์ง€ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
test("User ์—”ํ‹ฐํ‹ฐ ์ „์ฒด ์ƒ์„ฑ", async () => {
  await Sonamu.syncer.generateAll({ entityId: "User" });
  
  // syncer:* ํ•„ํ„ฐ๋กœ Syncer ๊ด€๋ จ ๋กœ๊ทธ๋งŒ ํ™•์ธ
  const syncerLogs = Naite.get("syncer:*").result();
  
  expect(syncerLogs.length).toBeGreaterThan(0);
});
Viewer์—๋Š” Syncer๊ฐ€ ์ˆ˜ํ–‰ํ•œ ๋ชจ๋“  ์ž‘์—…์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค:
โœ“ User ์—”ํ‹ฐํ‹ฐ ์ „์ฒด ์ƒ์„ฑ (2.3s)
  โ””โ”€ syncer:generateAll:start     { entityId: "User" }
  โ””โ”€ syncer:renderTemplate        { template: "model", entityId: "User" }
  โ””โ”€ syncer:writeFile             { path: "user.model.ts" }
  โ””โ”€ syncer:renderTemplate        { template: "types", entityId: "User" }
  โ””โ”€ syncer:writeFile             { path: "user.types.ts" }
  โ””โ”€ syncer:renderTemplate        { template: "service", entityId: "User" }
  โ””โ”€ syncer:writeFile             { path: "user.service.ts" }
  โ””โ”€ syncer:generateAll:done      { filesCreated: 3 }
๋ถ„์„:
  • ์–ด๋–ค ์ˆœ์„œ๋กœ ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
  • ๊ฐ ํ…œํ”Œ๋ฆฟ ๋ Œ๋”๋ง ์‹œ๊ฐ„ ์ธก์ •
  • ํŠน์ • ํ…œํ”Œ๋ฆฟ๋งŒ ํ•„ํ„ฐ๋งํ•˜์—ฌ ํ™•์ธ (syncer:renderTemplate:*)

3. API ํ˜ธ์ถœ ์ฒด์ธ ์ถ”์ 

์—ฌ๋Ÿฌ API๋ฅผ ์—ฐ์‡„์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ, ๊ฐ API์˜ ์ž…์ถœ๋ ฅ์„ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
test("๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ โ†’ ๋Œ“๊ธ€ ์ถ”๊ฐ€ โ†’ ์•Œ๋ฆผ ๋ฐœ์†ก", async () => {
  // 1. ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ
  Naite.t("api:post:create:request", { title: "Hello" });
  const { post } = await postModel.create({ title: "Hello" });
  Naite.t("api:post:create:response", { postId: post.id });
  
  // 2. ๋Œ“๊ธ€ ์ถ”๊ฐ€
  Naite.t("api:comment:create:request", { postId: post.id, content: "Nice!" });
  const { comment } = await commentModel.create({ 
    post_id: post.id, 
    content: "Nice!" 
  });
  Naite.t("api:comment:create:response", { commentId: comment.id });
  
  // 3. ์ž‘์„ฑ์ž์—๊ฒŒ ์•Œ๋ฆผ
  Naite.t("api:notification:send:request", { 
    userId: post.author_id,
    type: "new_comment"
  });
  await notificationService.send({ 
    userId: post.author_id,
    type: "new_comment",
    data: { commentId: comment.id }
  });
  Naite.t("api:notification:send:response", { sent: true });
});
request/response ํŒจํ„ด: API ํ˜ธ์ถœ ์ „ํ›„๋กœ :request์™€ :response ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๋ฉด, ๊ฐ API์˜ ์ž…์ถœ๋ ฅ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์—์„œ ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

4. ์„ฑ๋Šฅ ๋ณ‘๋ชฉ ์ง€์  ํŒŒ์•…

์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ๊ตฌ๊ฐ„์„ ์ฐพ์•„ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
test("๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ", async () => {
  Naite.t("perf:start", { timestamp: Date.now() });
  
  // ๋‹จ๊ณ„๋ณ„ ์‹œ๊ฐ„ ์ธก์ •
  Naite.t("perf:fetch:start", { timestamp: Date.now() });
  const data = await fetchLargeData();
  Naite.t("perf:fetch:done", { 
    timestamp: Date.now(),
    dataSize: data.length 
  });
  
  Naite.t("perf:process:start", { timestamp: Date.now() });
  const processed = await processData(data);
  Naite.t("perf:process:done", { 
    timestamp: Date.now(),
    processedCount: processed.length 
  });
  
  Naite.t("perf:save:start", { timestamp: Date.now() });
  await saveToDatabase(processed);
  Naite.t("perf:save:done", { timestamp: Date.now() });
  
  Naite.t("perf:end", { timestamp: Date.now() });
});
Viewer์—์„œ ๊ฐ ๋‹จ๊ณ„์˜ timestamp๋ฅผ ๋น„๊ตํ•˜๋ฉด:
โœ“ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ (5.2s)
  perf:start          12:34:56.000
  perf:fetch:start    12:34:56.001
  perf:fetch:done     12:34:57.500  โ† 1.5์ดˆ ์†Œ์š”
  perf:process:start  12:34:57.501
  perf:process:done   12:35:00.800  โ† 3.3์ดˆ ์†Œ์š” (๋ณ‘๋ชฉ!)
  perf:save:start     12:35:00.801
  perf:save:done      12:35:01.200  โ† 0.4์ดˆ ์†Œ์š”
  perf:end            12:35:01.201
๋ถ„์„ ๊ฒฐ๊ณผ:
  • processData๊ฐ€ 3.3์ดˆ๋กœ ๊ฐ€์žฅ ์˜ค๋ž˜ ๊ฑธ๋ฆผ (๋ณ‘๋ชฉ)
  • fetchLargeData๋Š” 1.5์ดˆ๋กœ ์–‘ํ˜ธ
  • saveToDatabase๋Š” 0.4์ดˆ๋กœ ๋น ๋ฆ„
โ†’ processData ์ตœ์ ํ™”์— ์ง‘์ค‘ํ•ด์•ผ ํ•จ

5. ์—๋Ÿฌ ์›์ธ ์ถ”์ 

์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์–ด๋А ๋‹จ๊ณ„์—์„œ, ์–ด๋–ค ๋ฐ์ดํ„ฐ๋กœ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
test("์ž˜๋ชป๋œ ์ž…๋ ฅ๊ฐ’ ์ฒ˜๋ฆฌ", async () => {
  try {
    Naite.t("user:create:input", { username: "" }); // ๋นˆ ๊ฐ’
    
    await userModel.create({ username: "", email: "[email protected]" });
  } catch (error) {
    Naite.t("user:create:error", {
      errorType: error.constructor.name,
      errorMessage: error.message,
      inputData: { username: "" }
    });
    
    // ์—๋Ÿฌ ๋กœ๊ทธ์˜ ์ฝœ์Šคํƒ์„ ๋ณด๋ฉด ์ •ํ™•ํ•œ ์‹คํŒจ ์œ„์น˜๋ฅผ ์•Œ ์ˆ˜ ์žˆ์Œ
  }
});
์—๋Ÿฌ ์‹œ ์ฝœ์Šคํƒ์˜ ์ค‘์š”์„ฑ: Viewer์˜ ์ฝœ์Šคํƒ ์ •๋ณด๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ์ •ํ™•ํ•œ ์ฝ”๋“œ ๋ผ์ธ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ด๋Š” console.log๋กœ๋Š” ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ค์šด ๋ณต์žกํ•œ ํ˜ธ์ถœ ์ฒด์ธ์—์„œ ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๋ถ€ ๊ตฌ์กฐ ๊นŠ์ด ์ดํ•ดํ•˜๊ธฐ

NaiteReporter์˜ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ

NaiteReporter๋Š” ์†Œ์ผ“ ์—ฐ๊ฒฐ์„ ์•ˆ์ •์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๋ฒ„ํผ๋ง๊ณผ ์žฌ์—ฐ๊ฒฐ ๋กœ์ง์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
class NaiteReporterClass {
  private socketPath: string | null = null;
  private socket: Socket | null = null;
  private connected = false;
  private buffer: string[] = [];

  /**
   * ์†Œ์ผ“ ์—ฐ๊ฒฐ ํ™•๋ณด
   * - ์ด๋ฏธ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์œผ๋ฉด ์ฆ‰์‹œ ๋ฐ˜ํ™˜
   * - ์—ฐ๊ฒฐ ์ค‘์ด๋ฉด ๋ฒ„ํผ์— ๋ฉ”์‹œ์ง€ ์ €์žฅ
   * - ์—ฐ๊ฒฐ ์‹คํŒจ๋Š” ๋ฌด์‹œ (Extension์ด ๊บผ์ ธ์žˆ์„ ์ˆ˜ ์žˆ์Œ)
   */
  private async ensureConnection(): Promise<void> {
    if (this.connected) return;
    
    return new Promise((resolve, reject) => {
      this.socketPath = getSocketPath();
      this.socket = connect(this.socketPath);
      
      this.socket.on("connect", () => {
        this.connected = true;
        
        // ๋ฒ„ํผ์— ์Œ“์ธ ๋ฉ”์‹œ์ง€ ์ „์†ก
        for (const msg of this.buffer) {
          this.socket?.write(msg);
        }
        this.buffer = [];
        
        resolve();
      });
      
      this.socket.on("error", () => {
        // Extension์ด ๊บผ์ ธ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์—๋Ÿฌ ๋ฌด์‹œ
        this.connected = false;
        this.socket = null;
        reject();
      });
      
      this.socket.on("close", () => {
        this.connected = false;
        this.socket = null;
      });
    });
  }

  /**
   * ๋ฉ”์‹œ์ง€ ์ „์†ก
   * - ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์œผ๋ฉด ์ฆ‰์‹œ ์ „์†ก
   * - ์—ฐ๊ฒฐ ์ค‘์ด๋ฉด ๋ฒ„ํผ์— ์ €์žฅ
   */
  private async send(data: NaiteMessage): Promise<void> {
    const msg = `${JSON.stringify(data)}\n`;
    
    await this.ensureConnection().catch(() => {});
    
    if (this.connected && this.socket) {
      this.socket.write(msg);
    } else {
      this.buffer.push(msg);
    }
  }

  async startTestRun() {
    if (process.env.CI) return; // CI์—์„œ๋Š” ๋ฌด์‹œ
    
    await this.send({
      type: "run/start",
      startedAt: new Date().toISOString(),
    });
  }

  async reportTestResult(result: TestResult) {
    if (process.env.CI) return;
    
    await this.send({
      type: "test/result",
      receivedAt: new Date().toISOString(),
      ...result,
    });
  }

  async endTestRun() {
    if (process.env.CI) return;
    
    await this.send({
      type: "run/end",
      endedAt: new Date().toISOString(),
    });
    
    // ์†Œ์ผ“ ์ข…๋ฃŒ
    if (this.socket) {
      this.socket.end();
      this.socket = null;
      this.connected = false;
    }
  }
}
ํ•ต์‹ฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜:
  1. ๋ฒ„ํผ๋ง: Extension์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์–ด๋„ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฒ„ํผ์— ์ €์žฅ
  2. ๋А๊ธ‹ํ•œ ์—ฐ๊ฒฐ: ์—ฐ๊ฒฐ ์‹คํŒจ๋ฅผ ์—๋Ÿฌ๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Œ
  3. ์ž๋™ ์žฌ์ „์†ก: ์—ฐ๊ฒฐ ์„ฑ๊ณต ์‹œ ๋ฒ„ํผ์˜ ๋ชจ๋“  ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†ก
  4. CI ๊ฐ์ง€: CI ํ™˜๊ฒฝ์—์„œ๋Š” ์†Œ์ผ“ ํ†ต์‹ ์„ ๊ฑด๋„ˆ๋œ€

ํ”„๋กœ์ ํŠธ๋ณ„ ์†Œ์ผ“ ๊ฒฉ๋ฆฌ

์—ฌ๋Ÿฌ Sonamu ํ”„๋กœ์ ํŠธ๋ฅผ ๋™์‹œ์— ์ž‘์—…ํ•  ๋•Œ ๋กœ๊ทธ๊ฐ€ ์„ž์ด์ง€ ์•Š๋„๋ก ๊ฒฉ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
import { createHash } from "crypto";
import { join } from "path";

function getProjectHash(configPath: string): string {
  return createHash("md5")
    .update(configPath)
    .digest("hex")
    .slice(0, 8);
}

// ์˜ˆ์‹œ
const projectA = "/Users/noa/project-a/api/src/sonamu.config.ts";
const hashA = getProjectHash(projectA);
// โ†’ "a1b2c3d4"

const projectB = "/Users/noa/project-b/api/src/sonamu.config.ts";
const hashB = getProjectHash(projectB);
// โ†’ "e5f6g7h8"
์žฅ์ :
  • ํ”„๋กœ์ ํŠธ ๊ฐ„ ๋กœ๊ทธ ์™„์ „ ๊ฒฉ๋ฆฌ
  • ๋™์‹œ ์‹คํ–‰ ์ง€์› (A ํ…Œ์ŠคํŠธ ์ค‘ B ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ)
  • ์ถฉ๋Œ ์—†๋Š” ์•ˆ์ •์  ์šด์˜

bootstrap.ts์™€์˜ ํ†ตํ•ฉ

Sonamu์˜ ํ…Œ์ŠคํŠธ bootstrap์€ ๊ฐ ํ…Œ์ŠคํŠธ๋งˆ๋‹ค Naite.getAllTraces()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋กœ๊ทธ๋ฅผ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค.
export const test = Object.assign(
  async (title: string, fn: TestFunction<object>, options?: TestOptions) => {
    return vitestTest(title, options, async (context) => {
      await runWithMockContext(async () => {
        try {
          // ํ…Œ์ŠคํŠธ ์‹คํ–‰
          await fn(context);
          
          // ์„ฑ๊ณต ์‹œ์—๋„ traces ์ˆ˜์ง‘
          context.task.meta.traces = Naite.getAllTraces();
        } catch (e: unknown) {
          // ์‹คํŒจ ์‹œ์—๋„ traces ์ˆ˜์ง‘ (์—๋Ÿฌ ์ถ”์ ์šฉ)
          context.task.meta.traces = Naite.getAllTraces();
          throw e;
        }
      });
    });
  },
  // ... skip, only, todo ๋“ฑ๋„ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
);

// afterEach์—์„œ Extension์œผ๋กœ ์ „์†ก
afterEach(async ({ task }) => {
  await NaiteReporter.reportTestResult({
    suiteName: task.suite?.name ?? "(no suite)",
    suiteFilePath: task.file?.filepath,
    testName: task.name,
    testFilePath: task.file?.filepath ?? "",
    testLine: task.location?.line ?? 0,
    status: task.result?.state ?? "pass",
    duration: task.result?.duration ?? 0,
    error: task.result?.errors?.[0]
      ? {
          message: task.result.errors[0].message,
          stack: task.result.errors[0].stack,
        }
      : undefined,
    traces: task.meta?.traces ?? [],
  });
});
ํ•ต์‹ฌ ํฌ์ธํŠธ:
  1. test() ๋ž˜ํผ: Vitest์˜ test()๋ฅผ ๋ž˜ํ•‘ํ•˜์—ฌ ์ž๋™์œผ๋กœ traces ์ˆ˜์ง‘
  2. try-catch ์–‘์ชฝ: ์„ฑ๊ณต/์‹คํŒจ ๋ชจ๋‘ traces๋ฅผ ์ˆ˜์ง‘ํ•˜์—ฌ ์—๋Ÿฌ ๋””๋ฒ„๊น… ์ง€์›
  3. task.meta ํ™œ์šฉ: Vitest์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•˜์—ฌ traces ์ „๋‹ฌ
  4. afterEach ์ „์†ก: ๊ฐ ํ…Œ์ŠคํŠธ ์ข…๋ฃŒ ํ›„ Reporter๋กœ ์ „์†ก

๋ฌธ์ œ ํ•ด๊ฒฐ

Extension์ด ๋กœ๊ทธ๋ฅผ ๋ฐ›์ง€ ๋ชปํ•จ

์ฆ์ƒ:
  • VSCode ํ•˜๋‹จ ์ƒํƒœ๋ฐ”์— Sonamu ์•„์ด์ฝ˜์ด ์—†์Œ
  • Naite Viewer ํŒจ๋„์„ ์—ด ์ˆ˜ ์—†์Œ
ํ•ด๊ฒฐ:
  1. Extensions ํƒญ์—์„œ Sonamu Extension ํ™•์ธ
  2. โ€œEnableโ€ ๋˜๋Š” โ€œReloadโ€ ๋ฒ„ํŠผ ํด๋ฆญ
  3. VSCode ์žฌ์‹œ์ž‘: Cmd+Shift+P โ†’ โ€œReload Windowโ€
์ฆ์ƒ:
  • Extension์€ ์‹คํ–‰ ์ค‘
  • ํ…Œ์ŠคํŠธ๋Š” ์„ฑ๊ณตํ•˜์ง€๋งŒ ๋กœ๊ทธ๊ฐ€ ์•ˆ ๋ณด์ž„
  • ์ฝ˜์†”์— โ€œENOENTโ€ ๋˜๋Š” โ€œECONNREFUSEDโ€ ์—๋Ÿฌ
์›์ธ: ํ”„๋กœ์ ํŠธ ํด๋”๊ฐ€ ์•„๋‹Œ ์ƒ์œ„ ํด๋”๋ฅผ ์—ด์–ด ์†Œ์ผ“ ๊ฒฝ๋กœ๊ฐ€ ๋‹ฌ๋ผ์งํ•ด๊ฒฐ:
  1. VSCode์—์„œ ํ”„๋กœ์ ํŠธ ํด๋” ์ง์ ‘ ์—ด๊ธฐ
  2. Extension ์žฌ์‹œ์ž‘
# โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ•
code ~/projects  # ์ƒ์œ„ ํด๋” ์—ด๊ธฐ

# โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•
code ~/projects/my-sonamu-app  # ํ”„๋กœ์ ํŠธ ํด๋” ์ง์ ‘ ์—ด๊ธฐ
์ฆ์ƒ:
  • โ€œPermission deniedโ€ ์—๋Ÿฌ
  • ์†Œ์ผ“ ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€๋งŒ ์ ‘๊ทผ ๋ถˆ๊ฐ€
ํ•ด๊ฒฐ:
# ์†Œ์ผ“ ํŒŒ์ผ ํ™•์ธ
ls -la ~/.sonamu/

# ๋ฌธ์ œ๊ฐ€ ์žˆ์œผ๋ฉด ์‚ญ์ œ
rm ~/.sonamu/naite-*.sock

# Extension๊ณผ ํ…Œ์ŠคํŠธ ์žฌ์‹œ์ž‘

๋กœ๊ทธ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์•„ ๋А๋ฆผ

๋กœ๊น… ์ „๋žต: Naite๋Š” ์„ฑ๋Šฅ์„ ์œ„ํ•ด ์„ค๊ณ„๋˜์—ˆ์ง€๋งŒ, ๊ณผ๋„ํ•œ ๋กœ๊น…์€ ํ”ผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
// โŒ ๋ฃจํ”„ ์•ˆ์—์„œ ๋กœ๊น…
for (let i = 0; i < 10000; i++) {
  Naite.t("loop:iteration", { i });
  // 10,000๊ฐœ์˜ trace ์ƒ์„ฑ!
}

// โŒ ๋ชจ๋“  ์ค‘๊ฐ„ ๋‹จ๊ณ„ ๋กœ๊น…
function processData(data: any[]) {
  for (const item of data) {
    Naite.t("process:step1", item);
    Naite.t("process:step2", item);
    Naite.t("process:step3", item);
    // ๋„ˆ๋ฌด ์ƒ์„ธํ•จ
  }
}

CI ํ™˜๊ฒฝ์—์„œ ์†Œ์ผ“ ์—ฐ๊ฒฐ ์—๋Ÿฌ

์ž๋™ ๋น„ํ™œ์„ฑํ™”: NaiteReporter๋Š” CI ํ™˜๊ฒฝ์„ ์ž๋™ ๊ฐ์ง€ํ•˜์—ฌ ์†Œ์ผ“ ํ†ต์‹ ์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.
// NaiteReporter ๋‚ด๋ถ€
async startTestRun() {
  if (process.env.CI) {
    return; // CI์—์„œ๋Š” ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š์Œ
  }
  
  await this.send({ type: "run/start" });
}
CI ๊ฐ์ง€ ์กฐ๊ฑด:
  • process.env.CI === "true"
  • GitHub Actions, GitLab CI, CircleCI ๋“ฑ์—์„œ ์ž๋™ ์„ค์ •๋จ
๋งŒ์•ฝ ๋กœ์ปฌ์—์„œ CI ๋ชจ๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด:
CI=true pnpm test

Watch ๋ชจ๋“œ์—์„œ ๋กœ๊ทธ๊ฐ€ ์Œ“์ž„

Watch ๋ชจ๋“œ์—์„œ ํŒŒ์ผ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์ˆ˜์ •ํ•˜๋ฉด ์ด์ „ ํ…Œ์ŠคํŠธ ๋กœ๊ทธ๊ฐ€ ๋‚จ์•„์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ž๋™ ํด๋ฆฌ์–ด: run/start ๋ฉ”์‹œ์ง€๊ฐ€ ์ „์†ก๋˜๋ฉด Extension์ด ์ž๋™์œผ๋กœ ์ด์ „ ๋กœ๊ทธ๋ฅผ ํด๋ฆฌ์–ดํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ๋ฅผ ๋™์‹œ์— ์ž‘์—…ํ•˜๋Š” ๊ฒฝ์šฐ ์ˆ˜๋™ ํด๋ฆฌ์–ด๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ˆ˜๋™ ํด๋ฆฌ์–ด ๋ฐฉ๋ฒ•:
  1. Naite Viewer ํŒจ๋„ ์ƒ๋‹จ์˜ โ€œClear Allโ€ ๋ฒ„ํŠผ ํด๋ฆญ
  2. ๋˜๋Š” Command Palette โ†’ โ€œNaite: Clear Logsโ€

์ฃผ์˜์‚ฌํ•ญ

Naite Viewer ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. Extension ํ•„์ˆ˜: VSCode Extension์ด ์‹คํ–‰ ์ค‘์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Extension ์—†์ด ํ…Œ์ŠคํŠธ๋งŒ ์‹คํ–‰ํ•˜๋ฉด ๋กœ๊ทธ๊ฐ€ ๋ฒ„ํผ์— ์Œ“์˜€๋‹ค๊ฐ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.
  2. ๋กœ์ปฌ ์ „์šฉ: ์›๊ฒฉ ์„œ๋ฒ„(SSH, Codespaces ๋“ฑ)์—์„œ๋Š” ์†Œ์ผ“ ํ†ต์‹ ์ด ์ œํ•œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•˜์„ธ์š”.
  3. CI ์ž๋™ ๋น„ํ™œ์„ฑํ™”: CI ํ™˜๊ฒฝ(process.env.CI)์—์„œ๋Š” ์†Œ์ผ“ ํ†ต์‹ ์ด ์ž๋™์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  4. ํ”„๋กœ์ ํŠธ๋ณ„ ๋…๋ฆฝ: ๊ฐ ํ”„๋กœ์ ํŠธ๋Š” ๋…๋ฆฝ๋œ ์†Œ์ผ“์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ๋ฅผ ๋™์‹œ์— ์ž‘์—…ํ•  ๋•Œ ์˜ฌ๋ฐ”๋ฅธ Viewer ์ฐฝ์„ ํ™•์ธํ•˜์„ธ์š”.
  5. ์ง๋ ฌํ™” ํ•„์š”: Extension์œผ๋กœ ์ „์†ก๋˜๋Š” ๊ฐ’์€ JSON์œผ๋กœ ์ง๋ ฌํ™”๋ฉ๋‹ˆ๋‹ค. Naite.t()์— ํ•จ์ˆ˜๋‚˜ ์ˆœํ™˜ ์ฐธ์กฐ ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ๊ฒฝ๊ณ ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
  6. ๊ณผ๋„ํ•œ ๋กœ๊น… ์ง€์–‘: ๋ฃจํ”„ ์•ˆ์—์„œ Naite.t()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์ˆ˜์ฒœ ๊ฐœ์˜ ๋กœ๊ทธ๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ๋‹จ๊ณ„