๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu์˜ ํ…Œ์ŠคํŠธ ๋กœ๊น… ๋ฐ ๋””๋ฒ„๊น… ์‹œ์Šคํ…œ์ธ Naite์— ๋Œ€ํ•ด ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

Naite ๊ฐœ์š”

ํ…Œ์ŠคํŠธ ๋กœ๊น…

์‹คํ–‰ ์ค‘ ๋กœ๊ทธ ๊ธฐ๋ก์ฒด๊ณ„์ ์ธ ์ถ”์ 

์กฐํšŒ ์‹œ์Šคํ…œ

wildcard ํŒจํ„ด์ฒด์ด๋‹ ์ฟผ๋ฆฌ

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

ํ˜ธ์ถœ ๊ฒฝ๋กœ ์ถ”์ ๋””๋ฒ„๊น… ์ง€์›

VSCode ํ†ตํ•ฉ

์‹ค์‹œ๊ฐ„ ์‹œ๊ฐํ™”Extension ์ง€์›

Naite๋ž€?

Naite(๋‚˜์ดํ…Œ)๋Š” Sonamu์˜ ํ…Œ์ŠคํŠธ ๋กœ๊น… ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘ ๋ฐœ์ƒํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ๊ธฐ๋กํ•˜๊ณ , ์ด๋ฅผ ์กฐํšŒํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๋””๋ฒ„๊น…์„ ๋•์Šต๋‹ˆ๋‹ค. ๋‚˜๋ฌด์˜ ๋‚˜์ดํ…Œ๊ฐ€ ์„ฑ์žฅ ๊ณผ์ •์„ ๊ธฐ๋กํ•˜๋“ฏ, Naite๋Š” ํ…Œ์ŠคํŠธ ์‹คํ–‰์˜ ์ „ ๊ณผ์ •์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋‹จ๊ณ„์—์„œ ์–ด๋–ค ๋ฐ์ดํ„ฐ๊ฐ€ ํ˜๋ €๋Š”์ง€, ์–ด๋–ค ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€๋ฅผ ์‹œ๊ฐ„ ์ˆœ์„œ๋Œ€๋กœ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์™œ Naite๊ฐ€ ํ•„์š”ํ•œ๊ฐ€?

์ผ๋ฐ˜์ ์ธ ๋””๋ฒ„๊น…์˜ ํ•œ๊ณ„

ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋‹ค ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ์„ ์ž์ฃผ ๋งˆ์ฃผํ•ฉ๋‹ˆ๋‹ค:
์—ฌ๋Ÿฌ ํ…Œ์ŠคํŠธ๊ฐ€ ๋™์‹œ์— ์‹คํ–‰๋˜๋ฉด console.log ์ถœ๋ ฅ์ด ๋’ค์„ž์ž…๋‹ˆ๋‹ค. ์–ด๋–ค ๋กœ๊ทธ๊ฐ€ ์–ด๋А ํ…Œ์ŠคํŠธ์—์„œ ๋‚˜์˜จ ๊ฒƒ์ธ์ง€ ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.
test("์‚ฌ์šฉ์ž ์ƒ์„ฑ", async () => {
  console.log("Creating user..."); // ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์˜ ๋กœ๊ทธ์™€ ์„ž์ž„
  const user = await createUser();
  console.log("User created:", user);
});

test("๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ", async () => {
  console.log("Creating post..."); // ์œ„ ํ…Œ์ŠคํŠธ์™€ ์„ž์—ฌ์„œ ์ถœ๋ ฅ
  const post = await createPost();
  console.log("Post created:", post);
});
์ฝ˜์†” ์ถœ๋ ฅ:
Creating user...
Creating post...
User created: { id: 1 }
Post created: { id: 1 }
์–ด๋А ๊ฒƒ์ด ๋จผ์ € ์™„๋ฃŒ๋˜์—ˆ๋Š”์ง€, ์ˆœ์„œ๊ฐ€ ๋’ค์„ž์—ฌ ํŒŒ์•…์ด ์–ด๋ ต์Šต๋‹ˆ๋‹ค.
ํŠน์ • ๋ชจ๋“ˆ์ด๋‚˜ ํ•จ์ˆ˜์˜ ๋กœ๊ทธ๋งŒ ๋ณด๊ณ  ์‹ถ์„ ๋•Œ, console.log๋กœ๋Š” ํ•„ํ„ฐ๋ง์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๋กœ๊ทธ๋ฅผ ๋‹ค ๋ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
// Syncer์˜ ๋™์ž‘๋งŒ ๋ณด๊ณ  ์‹ถ์ง€๋งŒ...
test("์ „์ฒด ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ", async () => {
  await syncer.generateAll(); // ๋‚ด๋ถ€์—์„œ ์ˆ˜์‹ญ ๊ฐœ์˜ console.log
  // Syncer ๊ด€๋ จ ๋กœ๊ทธ๋งŒ ๋ณด๋Š” ๋ฐฉ๋ฒ•์ด ์—†์Œ
});
ํŠน์ • ๋กœ๊ทธ๊ฐ€ ์–ด๋””์„œ ์ถœ๋ ฅ๋˜์—ˆ๋Š”์ง€, ์–ด๋–ค ํ•จ์ˆ˜๋ฅผ ๊ฑฐ์ณ ์™”๋Š”์ง€ ์ถ”์ ํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.
// ์ด ๋กœ๊ทธ๊ฐ€ ์–ด๋””์„œ ๋‚˜์™”๋Š”์ง€ ๋ชจ๋ฆ„
console.log("Processing data...");

// A โ†’ B โ†’ C โ†’ D ์ˆœ์„œ๋กœ ํ˜ธ์ถœ๋˜์—ˆ์ง€๋งŒ
// ์ฝœ์Šคํƒ ์ •๋ณด๊ฐ€ ์—†์–ด ์ถ”์  ๋ถˆ๊ฐ€
Vitest์˜ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ์ถœ๋ ฅ๊ณผ console.log๊ฐ€ ์„ž์—ฌ ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์ง‘๋‹ˆ๋‹ค.
โœ“ test 1 (123ms)
Debug: something...
โœ“ test 2 (98ms)
Debug: another thing...
โœ— test 3 (45ms)

Naite์˜ ํ•ด๊ฒฐ์ฑ…

Naite๋Š” ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋“ค์„ ์ฒด๊ณ„์ ์œผ๋กœ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค:

ํ‚ค ๊ธฐ๋ฐ˜ ๊ด€๋ฆฌ

๊ฐ ๋กœ๊ทธ์— ๊ณ ์œ ํ•œ ํ‚ค๋ฅผ ๋ถ€์—ฌํ•˜์—ฌ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. user:create, syncer:render์ฒ˜๋Ÿผ ๋ชจ๋“ˆ๊ณผ ๊ธฐ๋Šฅ์„ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

wildcard ํ•„ํ„ฐ๋ง

user:*๋กœ user ๊ด€๋ จ ๋กœ๊ทธ๋งŒ, *:create๋กœ ๋ชจ๋“  create ๋กœ๊ทธ๋งŒ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์›ํ•˜๋Š” ์ •๋ณด๋งŒ ๋น ๋ฅด๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ž๋™ ์ฝœ์Šคํƒ ์ถ”์ 

๊ฐ ๋กœ๊ทธ๊ฐ€ ์–ด๋””์„œ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ์ฝœ์Šคํƒ์„ ์ž๋™์œผ๋กœ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค. ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฒฝ๋กœ๋ฅผ ๋ช…ํ™•ํžˆ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ

๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ๋œ ๋กœ๊ทธ ์ €์žฅ์†Œ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์˜ ๋กœ๊ทธ์™€ ์ ˆ๋Œ€ ์„ž์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

VSCode ํ†ตํ•ฉ

VSCode Extension์œผ๋กœ ๋กœ๊ทธ๋ฅผ ์‹ค์‹œ๊ฐ„ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ถœ๋ ฅ๊ณผ ๋ถ„๋ฆฌ๋˜์–ด ๊น”๋”ํ•˜๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฟผ๋ฆฌ ์‹œ์Šคํ…œ

์ฒด์ด๋‹ ์ฟผ๋ฆฌ๋กœ ๋ณต์žกํ•œ ์กฐ๊ฑด์˜ ๋กœ๊ทธ๋„ ์‰ฝ๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. fromFile(), fromFunction(), where() ๋“ฑ์„ ์กฐํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ๊ฐœ๋…

1. Naite.t() - ๋กœ๊ทธ ๊ธฐ๋ก

Naite.t()๋Š” ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋กํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋Š” ํ‚ค(key), ๋‘ ๋ฒˆ์งธ ์ธ์ž๋Š” ๊ธฐ๋กํ•  ๊ฐ’(value)์ž…๋‹ˆ๋‹ค.
import { Naite } from "sonamu";

// ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์‹œ์ž‘
Naite.t("user:create:start", { username: "john" });

const user = await createUser({ username: "john" });

// ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์™„๋ฃŒ
Naite.t("user:create:done", { userId: user.id });
ํ‚ค ๋„ค์ด๋ฐ ๊ทœ์น™:
  • ์ฝœ๋ก (:)์œผ๋กœ ๊ณ„์ธต ๊ตฌ๋ถ„
  • module:function:action ํ˜•์‹ ๊ถŒ์žฅ
  • ์˜ˆ: user:create:start, syncer:render:template, payment:charge:done
์žฅ์ :
  • wildcard ํŒจํ„ด์œผ๋กœ ์กฐํšŒ ๊ฐ€๋Šฅ (user:*, *:create)
  • ๋ชจ๋“ˆ๋ณ„๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ๊ด€๋ฆฌ
  • ์ง๊ด€์ ์ธ ๊ตฌ์กฐ๋กœ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ

2. Naite.get() - ๋กœ๊ทธ ์กฐํšŒ

Naite.get()์€ ๊ธฐ๋ก๋œ ๋กœ๊ทธ๋ฅผ ์กฐํšŒํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ํ‚ค ๋˜๋Š” wildcard ํŒจํ„ด์œผ๋กœ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// ์ •ํ™•ํ•œ ํ‚ค๋กœ ์กฐํšŒ
const logs = Naite.get("user:create:start").result();
// [{ username: "john" }]

// wildcard ํŒจํ„ด์œผ๋กœ ์กฐํšŒ
const allUserLogs = Naite.get("user:*").result();
// user:create:start, user:create:done ๋ชจ๋‘ ์กฐํšŒ

// ์ฒซ ๋ฒˆ์งธ ๋กœ๊ทธ๋งŒ
const firstLog = Naite.get("user:create:start").first();
// { username: "john" }
์ฟผ๋ฆฌ ์ฒด์ด๋‹: ์—ฌ๋Ÿฌ ์กฐ๊ฑด์„ ์ฒด์ด๋‹ํ•˜์—ฌ ๋ณต์žกํ•œ ๊ฒ€์ƒ‰๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค:
Naite.get("user:*")
  .fromFile("user.model.test.ts")     // ํŠน์ • ํŒŒ์ผ์—์„œ๋งŒ
  .fromFunction("createUser")         // ํŠน์ • ํ•จ์ˆ˜์—์„œ๋งŒ
  .where("data.username", "=", "john") // ํŠน์ • ๊ฐ’๋งŒ
  .result();

3. NaiteStore - ๋กœ๊ทธ ์ €์žฅ์†Œ

๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ๋œ NaiteStore๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ์ด๋Š” Map<string, NaiteTrace[]> ํƒ€์ž…์œผ๋กœ, ํ‚ค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋กœ๊ทธ๋ฅผ ๋ฐฐ์—ด๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
type NaiteStore = Map<string, NaiteTrace[]>;

interface NaiteTrace {
  key: string;        // "user:create:start"
  data: any;          // { username: "john" }
  stack: StackFrame[]; // ์ฝœ์Šคํƒ ์ •๋ณด
  at: Date;           // ๊ธฐ๋ก ์‹œ๊ฐ„
}

interface StackFrame {
  functionName: string | null;  // "createUser"
  filePath: string;              // "/Users/.../user.model.ts"
  lineNumber: number;            // 123
}
์˜ˆ์‹œ:
// test() ์‹คํ–‰ ์‹œ ์ƒˆ๋กœ์šด Store ์ƒ์„ฑ
const store = new Map();

// Naite.t() ํ˜ธ์ถœ๋งˆ๋‹ค ์ถ”๊ฐ€
Naite.t("user:create:start", { username: "john" });
// store.set("user:create:start", [{ key: "user:create:start", data: { username: "john" }, ... }])

Naite.t("user:create:start", { username: "jane" });
// ๊ฐ™์€ ํ‚ค์— ์ถ”๊ฐ€๋จ
// store.set("user:create:start", [
//   { key: "user:create:start", data: { username: "john" }, ... },
//   { key: "user:create:start", data: { username: "jane" }, ... }
// ])
ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ:
test("ํ…Œ์ŠคํŠธ 1", async () => {
  Naite.t("key", "value1");
  const data = Naite.get("key").first();
  expect(data).toBe("value1");
});

test("ํ…Œ์ŠคํŠธ 2", async () => {
  // ์ด์ „ ํ…Œ์ŠคํŠธ์˜ ๋กœ๊ทธ๋Š” ์—†์Œ
  const data = Naite.get("key").first();
  expect(data).toBeUndefined();
});
๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ๋…๋ฆฝ๋œ Store๋ฅผ ๊ฐ€์ง€๋ฏ€๋กœ ์„œ๋กœ ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

4. ์ฝœ์Šคํƒ ์ž๋™ ์ถ”์ 

Naite๋Š” Naite.t() ํ˜ธ์ถœ ์‹œ์ ์˜ ์ฝœ์Šคํƒ์„ ์ž๋™์œผ๋กœ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ๊ฐ€ ์–ด๋””์„œ ๊ธฐ๋ก๋˜์—ˆ๋Š”์ง€, ์–ด๋–ค ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฒฝ๋กœ๋ฅผ ๊ฑฐ์ณค๋Š”์ง€ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
test("์‚ฌ์šฉ์ž ์ƒ์„ฑ", async () => {
  Naite.t("test:log", "value");
  // ์ฝœ์Šคํƒ: [test โ†’ runWithMockContext]
});
ํ™œ์šฉ:
  • fromFunction("createUser")๋กœ ํŠน์ • ํ•จ์ˆ˜์—์„œ ๊ธฐ๋ก๋œ ๋กœ๊ทธ๋งŒ ํ•„ํ„ฐ๋ง
  • VSCode Extension์—์„œ ์ฝœ์Šคํƒ ํด๋ฆญ โ†’ ์ฝ”๋“œ ์œ„์น˜๋กœ ๋ฐ”๋กœ ์ด๋™
  • ๋ณต์žกํ•œ ํ˜ธ์ถœ ์ฒด์ธ ๋””๋ฒ„๊น…

์‹ค์ œ ํ™œ์šฉ ์˜ˆ์‹œ

๊ธฐ๋ณธ ์‚ฌ์šฉ - ํ๋ฆ„ ์ถ”์ 

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•์€ ํ…Œ์ŠคํŠธ ํ๋ฆ„์˜ ๊ฐ ๋‹จ๊ณ„๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
test("๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ ํ๋ฆ„", async () => {
  const postModel = new PostModel();
  
  // 1. ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ๊ธฐ๋ก
  Naite.t("post:create:input", {
    title: "Hello World",
    content: "This is content",
    author_id: 1,
  });
  
  // 2. ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ
  const { post } = await postModel.create({
    title: "Hello World",
    content: "This is content",
    author_id: 1,
  });
  
  // 3. ๊ฒฐ๊ณผ ๊ธฐ๋ก
  Naite.t("post:create:output", {
    postId: post.id,
    createdAt: post.created_at,
  });
  
  // 4. ์ „์ฒด ํ๋ฆ„ ๊ฒ€์ฆ
  const logs = Naite.get("post:create:*").result();
  expect(logs).toHaveLength(2);
  expect(logs[0].title).toBe("Hello World");
  expect(logs[1].postId).toBeGreaterThan(0);
});
์ž…์ถœ๋ ฅ ํŒจํ„ด: :input๊ณผ :output ์ ‘๋ฏธ์‚ฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ•จ์ˆ˜์˜ ์ž…๋ ฅ๊ณผ ์ถœ๋ ฅ์„ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๊ณผ์ •์„ ์ถ”์ ํ•˜๋Š” ๋ฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ค‘๊ธ‰ ์‚ฌ์šฉ - ์กฐ๊ฑด๋ถ€ ์ถ”์ 

๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ๋ถ„๊ธฐ๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.
test("์ฃผ๋ฌธ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ", async () => {
  const order = await getOrder(123);
  
  Naite.t("payment:process:start", {
    orderId: order.id,
    amount: order.amount,
    paymentMethod: order.payment_method,
  });
  
  if (order.payment_method === "card") {
    Naite.t("payment:card:charge", { cardNumber: "****1234" });
    await chargeCard(order);
    Naite.t("payment:card:success", { transactionId: "tx_123" });
  } else if (order.payment_method === "bank") {
    Naite.t("payment:bank:transfer", { bankCode: "001" });
    await transferBank(order);
    Naite.t("payment:bank:success", { transferId: "tf_456" });
  }
  
  Naite.t("payment:process:done", { orderId: order.id });
  
  // ์นด๋“œ ๊ฒฐ์ œ๊ฐ€ ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
  const cardLogs = Naite.get("payment:card:*").result();
  expect(cardLogs.length).toBeGreaterThan(0);
});

๊ณ ๊ธ‰ ์‚ฌ์šฉ - ์—๋Ÿฌ ์ถ”์ 

์—๋Ÿฌ ๋ฐœ์ƒ ์ƒํ™ฉ์„ ์ƒ์„ธํžˆ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
test("์ž˜๋ชป๋œ ์ž…๋ ฅ๊ฐ’ ์ฒ˜๋ฆฌ", async () => {
  try {
    Naite.t("user:create:input", { username: "" }); // ๋นˆ ๊ฐ’
    
    await userModel.create({
      username: "",
      email: "[email protected]",
    });
    
    throw new Error("Should have failed");
  } catch (error) {
    Naite.t("user:create:error", {
      errorType: error.constructor.name,
      errorMessage: error.message,
      validationErrors: error.details,
    });
    
    // ์—๋Ÿฌ ๋กœ๊ทธ ํ™•์ธ
    const errorLog = Naite.get("user:create:error").first();
    expect(errorLog.errorType).toBe("ValidationError");
    expect(errorLog.errorMessage).toContain("username");
  }
});
์—๋Ÿฌ ์ถ”์ ์˜ ๊ฐ€์น˜: ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์–ด๋–ค ์ž…๋ ฅ๊ฐ’์œผ๋กœ, ์–ด๋А ๋‹จ๊ณ„์—์„œ ์‹คํŒจํ–ˆ๋Š”์ง€ ๋ช…ํ™•ํžˆ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. VSCode Extension์˜ ์ฝœ์Šคํƒ ๊ธฐ๋Šฅ๊ณผ ๊ฒฐํ•ฉํ•˜๋ฉด ์—๋Ÿฌ ์œ„์น˜๋ฅผ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ž‘๋™ ์›๋ฆฌ

1. ๋กœ๊ทธ ๊ธฐ๋ก ๊ณผ์ •

1

ํ™˜๊ฒฝ ์ฒดํฌ

NODE_ENV === "test"์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์ด ์•„๋‹ˆ๋ฉด ์ฆ‰์‹œ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
2

Context ํ™•์ธ

Sonamu.getContext()๋กœ ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ Context๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. Context๊ฐ€ ์—†์œผ๋ฉด ๋ฌด์‹œํ•ฉ๋‹ˆ๋‹ค.
3

์ฝœ์Šคํƒ ์ˆ˜์ง‘

new Error().stack์œผ๋กœ ํ˜„์žฌ ์ฝœ์Šคํƒ์„ ์ˆ˜์ง‘ํ•˜์—ฌ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค.
4

Trace ์ƒ์„ฑ

ํ‚ค, ๊ฐ’, ์ฝœ์Šคํƒ, ์‹œ๊ฐ„์„ ํฌํ•จํ•œ NaiteTrace ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
5

Store์— ์ €์žฅ

naiteStore.set(key, [...existing, trace])๋กœ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

2. ๋กœ๊ทธ ์กฐํšŒ ๊ณผ์ •

3. VSCode Extension ์ „์†ก

์ง๋ ฌํ™”์˜ ์ค‘์š”์„ฑ: VSCode Extension์œผ๋กœ ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด ๋ชจ๋“  ๊ฐ’์€ JSON์œผ๋กœ ์ง๋ ฌํ™”๋ฉ๋‹ˆ๋‹ค. Naite.t()์— ํ•จ์ˆ˜๋‚˜ ์ˆœํ™˜ ์ฐธ์กฐ ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ๊ฒฝ๊ณ ๊ฐ€ ํ‘œ์‹œ๋˜์ง€๋งŒ, any ํƒ€์ž…์œผ๋กœ ๋ฐ›์•„ ์‚ฌ์šฉ ํŽธ์˜์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

์ฃผ์š” ํŠน์ง•

Naite๋Š” ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋งŒ ๋™์ž‘ํ•˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์— Naite.t()๊ฐ€ ์žˆ์–ด๋„ ์„ฑ๋Šฅ ์˜ํ–ฅ์ด ์ „ํ˜€ ์—†์Šต๋‹ˆ๋‹ค.
if (process.env.NODE_ENV !== "test") {
  return; // ์ฆ‰์‹œ ์ข…๋ฃŒ, ๋น„์šฉ ์—†์Œ
}
๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ๋œ NaiteStore๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. bootstrap.ts์˜ getMockContext()์—์„œ ๋งค๋ฒˆ ์ƒˆ๋กœ์šด Store๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
function getMockContext(): Context {
  return {
    naiteStore: Naite.createStore(), // ์ƒˆ๋กœ์šด Map ์ƒ์„ฑ
    // ...
  };
}
Naite.t(value: any)๋Š” any ํƒ€์ž…์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. TypeScript์˜ ํƒ€์ž… ์•ˆ์ •์„ฑ๋ณด๋‹ค ์‚ฌ์šฉ ํŽธ์˜์„ฑ์„ ์šฐ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.
// ๋ชจ๋“  ํƒ€์ž… ํ—ˆ์šฉ
Naite.t("key", "string");
Naite.t("key", 123);
Naite.t("key", { nested: { object: true } });
Naite.t("key", [1, 2, 3]);
๋‹จ, Extension ์ „์†ก ์‹œ ์ง๋ ฌํ™”๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฐ’์€ ๊ฒฝ๊ณ ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
getAllTraces()๋Š” ๋ชจ๋“  ๊ฐ’์„ JSON์œผ๋กœ ์ง๋ ฌํ™”ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” Vitest์˜ task.meta๋ฅผ ํ†ตํ•œ ํ”„๋กœ์„ธ์Šค ๊ฐ„ ํ†ต์‹ ์„ ์œ„ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
return traces.map((trace) => ({
  key: trace.key,
  value: JSON.parse(JSON.stringify(trace.data)), // ๊ฐ•์ œ ์ง๋ ฌํ™”
  filePath: trace.stack[0]?.filePath ?? "",
  lineNumber: trace.stack[0]?.lineNumber ?? 0,
  at: trace.at.toISOString(),
}));
๊ฐ„๋‹จํ•˜์ง€๋งŒ ๊ฐ•๋ ฅํ•œ ํŒจํ„ด ๋งค์นญ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค:
  • user:*: prefix ๋งค์นญ (๊ธธ์ด ๋ฌด๊ด€)
  • *:create: suffix ๋งค์นญ (๊ธธ์ด ๋™์ผ)
  • user:*:done: ์ค‘๊ฐ„ wildcard (๊ธธ์ด ๋™์ผ)
๋ณต์žกํ•œ ์ •๊ทœ์‹ ๋Œ€์‹  ์ง๊ด€์ ์ธ ํŒจํ„ด์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

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

Naite ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ๋งŒ: NODE_ENV === "test"์—์„œ๋งŒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ž๋™์œผ๋กœ ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.
  2. Context ํ•„์š”: Sonamu.getContext()๊ฐ€ ์žˆ์–ด์•ผ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. bootstrap์˜ runWithMockContext() ์•ˆ์—์„œ๋งŒ ์‚ฌ์šฉํ•˜์„ธ์š”.
  3. ๊ณผ๋„ํ•œ ๋กœ๊น… ์ฃผ์˜: ๋ฃจํ”„ ์•ˆ์—์„œ Naite.t()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์ˆ˜์ฒœ ๊ฐœ์˜ trace๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  4. ํ‚ค ๋„ค์ด๋ฐ ๊ทœ์น™: module:function:action ํ˜•์‹์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ์ผ๊ด€๋œ ๊ทœ์น™์œผ๋กœ ๋‚˜์ค‘์— ์ฐพ๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค.
  5. ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ๊ฐ’ ๊ถŒ์žฅ: VSCode Extension์œผ๋กœ ์ „์†กํ•˜๋ ค๋ฉด JSON์œผ๋กœ ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•จ์ˆ˜๋‚˜ ์ˆœํ™˜ ์ฐธ์กฐ๋Š” ํ”ผํ•˜์„ธ์š”.

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