메인 콘텐츠로 건너뛰기
Naite.t()를 사용하여 테스트 실행 중 데이터를 기록하는 방법을 알아봅니다.

Naite.t() 개요

간단한 API

두 개의 파라미터직관적인 사용

자동 추적

콜스택 자동 수집호출 경로 파악

Any 타입

모든 값 허용유연한 로깅

테스트 전용

NODE_ENV 체크프로덕션 영향 없음

Naite.t()란?

Naite.t()는 테스트 실행 중 특정 시점의 데이터를 기록하는 함수입니다. 첫 번째 인자로 고유한 키(key)를, 두 번째 인자로 기록할 값(value)을 받습니다.
import { Naite } from "sonamu";

// 기본 사용
Naite.t("user:create", { userId: 123, username: "john" });
이렇게 기록된 데이터는:
  • 테스트 실행 중 언제든 Naite.get()으로 조회 가능
  • VSCode Extension으로 실시간 전송
  • 콜스택과 시간 정보와 함께 저장

기본 사용법

단순 로깅

가장 기본적인 사용법은 함수의 입출력을 기록하는 것입니다.
test("사용자 생성 흐름", async () => {
  const userModel = new UserModel();
  
  // 입력 데이터 기록
  const input = {
    username: "john",
    email: "[email protected]",
  };
  Naite.t("user:create:input", input);
  
  // 사용자 생성
  const { user } = await userModel.create(input);
  
  // 출력 데이터 기록
  Naite.t("user:create:output", {
    userId: user.id,
    username: user.username,
  });
});
입출력 패턴: :input/:output 또는 :before/:after 패턴을 사용하면 데이터 변환 과정을 명확히 추적할 수 있습니다.

단계별 로깅

복잡한 프로세스는 단계별로 로그를 남겨 흐름을 추적합니다.
test("주문 처리 전체 흐름", async () => {
  const orderId = 123;
  
  // 1단계: 주문 검증
  Naite.t("order:validate:start", { orderId });
  await validateOrder(orderId);
  Naite.t("order:validate:done", { valid: true });
  
  // 2단계: 재고 확인
  Naite.t("order:inventory:check", { orderId });
  const available = await checkInventory(orderId);
  Naite.t("order:inventory:result", { available });
  
  // 3단계: 결제 처리
  Naite.t("order:payment:start", { orderId });
  await processPayment(orderId);
  Naite.t("order:payment:done", { transactionId: "tx_123" });
  
  // 4단계: 배송 시작
  Naite.t("order:shipping:start", { orderId });
  await startShipping(orderId);
  Naite.t("order:shipping:done", { trackingNumber: "TRK_001" });
});
장점:
  • 어느 단계에서 실패했는지 명확히 파악
  • 각 단계의 소요 시간 측정 가능
  • wildcard 패턴으로 특정 단계만 필터링 (order:payment:*)
start/done 패턴: 각 작업의 시작과 완료를 :start/:done으로 기록하면, 나중에 소요 시간을 계산하거나 완료 여부를 확인하기 쉽습니다.

내부 동작 이해하기

1. 환경 체크

Naite.t()는 가장 먼저 실행 환경을 체크합니다.
t(name: string, value: any) {
  // 테스트 환경이 아니면 즉시 종료
  if (process.env.NODE_ENV !== "test") {
    return;
  }
  // ...
}
이유:
  • 프로덕션 코드에 Naite.t()가 있어도 안전
  • 테스트 전용 기능
  • 런타임 오버헤드 없음
프로덕션 배포 전 Naite.t() 호출을 제거할 필요가 없습니다. 환경 변수로 자동 제어됩니다.

2. Context 확인

Naite는 Sonamu의 Context 시스템을 사용합니다.
try {
  const context = Sonamu.getContext();
  const store = context?.naiteStore;
  
  if (!store) {
    return; // Context 없으면 무시
  }
  // ...
} catch {
  // Context 없는 상황
}
Context의 역할:
  • 각 테스트마다 독립된 naiteStore 제공
  • 테스트 격리 보장
  • AsyncLocalStorage로 비동기 작업에서도 Context 유지
// bootstrap.ts
function getMockContext(): Context {
  return {
    ip: "127.0.0.1",
    session: {},
    user: null,
    naiteStore: Naite.createStore(), // 새로운 Map 생성
    // ...
  };
}

// test() 실행 시마다 새로운 Context
export const test = async (title, fn, options) => {
  return vitestTest(title, options, async (context) => {
    await runWithMockContext(async () => {
      // 이 안에서 Naite.t() 호출 가능
      await fn(context);
    });
  });
};

3. 콜스택 수집

Naite.t() 호출 시점의 콜스택을 자동으로 수집합니다.
// 콜스택 수집
const stack = extractCallStack();

function extractCallStack(): StackFrame[] {
  const stack = new Error().stack;
  
  // "Error", "extractCallStack", "Naite.t" 제외
  const frames = stack.split("\n")
    .slice(3)
    .map(parseStackFrame)
    .filter(frame => frame !== null);
  
  // runWithContext 발견 시 종료
  const contextIndex = frames.findIndex(
    f => f.functionName?.includes("runWithContext")
  );
  
  return contextIndex >= 0 
    ? frames.slice(0, contextIndex + 1) 
    : frames;
}
수집되는 정보:
  • 함수명 (createUser)
  • 파일 경로 (/Users/.../user.model.ts)
  • 라인 번호 (123)
async function createUser() {
  Naite.t("user:create", { username: "john" });
}

test("사용자 생성", async () => {
  await createUser();
});

// 수집되는 콜스택:
// [
//   {
//     functionName: "createUser",
//     filePath: "/Users/.../user.model.ts",
//     lineNumber: 15
//   },
//   {
//     functionName: "test",
//     filePath: "/Users/.../user.model.test.ts",
//     lineNumber: 42
//   },
//   {
//     functionName: "runWithMockContext",
//     filePath: "/Users/.../bootstrap.ts",
//     lineNumber: 58
//   }
// ]

4. Trace 생성 및 저장

수집한 정보로 NaiteTrace 객체를 생성하고 Store에 저장합니다.
const trace: NaiteTrace = {
  key: name,
  data: value,
  stack: stack,
  at: new Date(),
};

// Store에 추가 (항상 배열로 관리)
const existing = store.get(name) ?? [];
store.set(name, [...existing, trace]);
배열로 관리하는 이유:
  • 같은 키로 여러 번 호출 가능
  • 시간 순서대로 추적
  • 반복 작업 로깅 지원
Naite.t("user:create", { userId: 1 });

// Store:
// "user:create" => [{ key, data: { userId: 1 }, ... }]

키 네이밍 전략

계층적 구조

콜론(:)으로 계층을 구분하면 나중에 wildcard 패턴으로 쉽게 조회할 수 있습니다.
// ✅ 계층적 구조
Naite.t("user:create", { /* ... */ });
Naite.t("user:update", { /* ... */ });
Naite.t("user:delete", { /* ... */ });

Naite.t("syncer:renderTemplate", { /* ... */ });
Naite.t("syncer:writeFile", { /* ... */ });

// wildcard로 쉽게 조회
Naite.get("user:*").result();         // 모든 user 관련
Naite.get("syncer:*").result();       // 모든 syncer 관련
Naite.get("*:create").result();       // 모든 create 작업

권장 패턴

1

module:function

가장 기본적인 패턴입니다.
Naite.t("user:create", { /* ... */ });
Naite.t("post:update", { /* ... */ });
Naite.t("syncer:render", { /* ... */ });
2

module:function:action

더 상세한 추적이 필요할 때 사용합니다.
Naite.t("user:create:input", { /* ... */ });
Naite.t("user:create:validation", { /* ... */ });
Naite.t("user:create:db", { /* ... */ });
Naite.t("user:create:done", { /* ... */ });
3

module:function:action:detail

매우 복잡한 흐름에서 사용합니다.
Naite.t("order:payment:card:charge:start", { /* ... */ });
Naite.t("order:payment:card:charge:done", { /* ... */ });
Naite.t("order:payment:bank:transfer:start", { /* ... */ });
Naite.t("order:payment:bank:transfer:done", { /* ... */ });
너무 깊은 계층(5단계 이상)은 오히려 가독성을 떨어뜨립니다. 대부분의 경우 3단계면 충분합니다.

실전 패턴

1. 조건부 추적

비즈니스 로직의 분기를 명확히 추적합니다.
async function processPayment(order: Order) {
  Naite.t("payment:start", { 
    orderId: order.id,
    method: order.payment_method 
  });
  
  if (order.payment_method === "card") {
    Naite.t("payment:card:selected", { cardNumber: "****1234" });
    await chargeCard(order);
    Naite.t("payment:card:success", { transactionId: "tx_123" });
  } else if (order.payment_method === "bank") {
    Naite.t("payment:bank:selected", { bankCode: "001" });
    await transferBank(order);
    Naite.t("payment:bank:success", { transferId: "tf_456" });
  }
  
  Naite.t("payment:done", { orderId: order.id });
}

test("카드 결제", async () => {
  const order = { id: 1, payment_method: "card" };
  await processPayment(order);
  
  // 카드 결제 경로가 실행되었는지 확인
  const cardLogs = Naite.get("payment:card:*").result();
  expect(cardLogs.length).toBeGreaterThan(0);
  
  // 계좌 이체는 실행 안 됨
  const bankLogs = Naite.get("payment:bank:*").result();
  expect(bankLogs).toHaveLength(0);
});
활용:
  • A/B 테스트 경로 확인
  • 권한 분기 검증
  • 에러 핸들링 경로 추적

2. 에러 추적

에러 발생 시 상세 정보를 기록합니다.
async function createUser(data: UserCreateInput) {
  Naite.t("user:create:input", data);
  
  try {
    // 유효성 검사
    await validateUser(data);
    Naite.t("user:create:validation:pass", {});
    
    // DB 저장
    const user = await db.insert("users").values(data);
    Naite.t("user:create:success", { userId: user.id });
    
    return user;
  } catch (error) {
    // 에러 상세 기록
    Naite.t("user:create:error", {
      errorType: error.constructor.name,
      errorMessage: error.message,
      errorCode: error.code,
      inputData: data,
      stack: error.stack,
    });
    throw error;
  }
}

test("잘못된 입력 처리", async () => {
  try {
    await createUser({ username: "" }); // 빈 값
    throw new Error("Should have failed");
  } catch (error) {
    // 에러 로그 확인
    const errorLog = Naite.get("user:create:error").first();
    
    expect(errorLog).toBeDefined();
    expect(errorLog.errorType).toBe("ValidationError");
    expect(errorLog.errorMessage).toContain("username");
    
    // 유효성 검사를 통과하지 못했는지 확인
    const validationPass = Naite.get("user:create:validation:pass").result();
    expect(validationPass).toHaveLength(0);
  }
});
에러 추적 패턴: try-catch의 catch 블록에서 Naite.t()를 호출하면, 에러 발생 시점의 모든 컨텍스트를 캡처할 수 있습니다. 콜스택과 함께 저장되어 디버깅이 매우 쉬워집니다.

3. 성능 측정

시간 정보를 기록하여 병목 지점을 파악합니다.
test("데이터 처리 성능", async () => {
  Naite.t("process:start", { timestamp: Date.now() });
  
  // 단계 1: 데이터 조회
  Naite.t("process:fetch:start", { timestamp: Date.now() });
  const data = await fetchLargeData();
  Naite.t("process:fetch:done", { 
    timestamp: Date.now(),
    dataSize: data.length 
  });
  
  // 단계 2: 데이터 처리
  Naite.t("process:transform:start", { timestamp: Date.now() });
  const processed = await transformData(data);
  Naite.t("process:transform:done", { 
    timestamp: Date.now(),
    processedCount: processed.length 
  });
  
  // 단계 3: DB 저장
  Naite.t("process:save:start", { timestamp: Date.now() });
  await saveToDatabase(processed);
  Naite.t("process:save:done", { timestamp: Date.now() });
  
  Naite.t("process:end", { timestamp: Date.now() });
  
  // 각 단계별 소요 시간 계산
  const logs = Naite.get("process:*").result();
  const start = logs.find(l => l.timestamp).timestamp;
  const fetchDone = logs.find(l => l.timestamp && l.dataSize).timestamp;
  const transformDone = logs.find(l => l.timestamp && l.processedCount).timestamp;
  const saveDone = logs[logs.length - 2].timestamp;
  
  console.log(`Fetch: ${fetchDone - start}ms`);
  console.log(`Transform: ${transformDone - fetchDone}ms`);
  console.log(`Save: ${saveDone - transformDone}ms`);
});

4. 반복 작업 추적

루프나 배치 작업에서는 요약 정보만 기록합니다.
// ❌ 루프 안에서 로깅
for (let i = 0; i < 10000; i++) {
  Naite.t("loop:iteration", { i, value: data[i] });
  // 10,000개의 trace 생성!
}

값 타입과 직렬화

Any 타입 허용

Naite.t()는 any 타입을 받아 모든 JavaScript 값을 기록할 수 있습니다.
// ✅ 모든 타입 가능
Naite.t("key", "string");
Naite.t("key", 123);
Naite.t("key", true);
Naite.t("key", { nested: { object: true } });
Naite.t("key", [1, 2, 3]);
Naite.t("key", new Date());
Naite.t("key", null);
Naite.t("key", undefined);
설계 이유:
  • TypeScript 타입 안정성보다 사용 편의성 우선
  • 테스트 작성 중 타입 에러로 방해받지 않음
  • expect()와 같은 철학

직렬화 경고

VSCode Extension으로 전송하려면 JSON 직렬화가 필요합니다. 직렬화 불가능한 값은 경고가 표시됩니다.
// ✅ 직렬화 가능
Naite.t("key", { data: "value" });
Naite.t("key", [1, 2, 3]);
Naite.t("key", "string");
Naite.t("key", 123);
Naite.t("key", true);
╔════════════════════════════════════════════════════════════════╗
  [Naite] Non-serializable value detected!
╠════════════════════════════════════════════════════════════════╣
  Key: user:create
  Reason: Cannot serialize function
  Location: /Users/.../user.model.test.ts
  Line: 15
╠════════════════════════════════════════════════════════════════╣
  Naite.t() accepts any type of value. However, values will
  be serialized to JSON when exported via Naite.getAllTraces().
╚════════════════════════════════════════════════════════════════╝
경고가 표시되어도 테스트는 정상 실행됩니다. 단, VSCode Extension에서 해당 값을 볼 수 없습니다.

성능 고려사항

1. 테스트 환경만 동작

if (process.env.NODE_ENV !== "test") {
  return; // 즉시 종료, 비용 없음
}
프로덕션 코드에 Naite.t()가 있어도 성능 영향이 전혀 없습니다.

2. 과도한 로깅 주의

루프 안에서 Naite.t()를 호출하면 수천 개의 trace가 생성되어:
  • 메모리 사용량 증가
  • 테스트 속도 저하
  • VSCode Extension 느려짐
권장사항:
  • 루프 시작/종료만 기록
  • 문제 발생 시에만 기록
  • 요약 정보 활용

3. 조건부 로깅

디버깅이 필요할 때만 상세 로깅을 활성화합니다.
const DEBUG = process.env.DEBUG_NAITE === "true";

function processItem(item: any) {
  if (DEBUG) {
    Naite.t("process:detail", item);
  }
  
  // 처리 로직
}

// 사용:
// DEBUG_NAITE=true pnpm test

베스트 프랙티스

1

명확한 키 사용

// ✅ 올바른 방법
Naite.t("user:create:input", { username: "john" });
Naite.t("user:create:validation", { valid: true });
Naite.t("user:create:db", { query: "INSERT..." });

// ❌ 잘못된 방법
Naite.t("data1", { username: "john" });
Naite.t("data2", { valid: true });
Naite.t("data3", { query: "INSERT..." });
2

일관된 구조

// ✅ 올바른 방법: 일관된 계층
Naite.t("user:create", { /* ... */ });
Naite.t("user:update", { /* ... */ });
Naite.t("user:delete", { /* ... */ });

Naite.t("post:create", { /* ... */ });
Naite.t("post:update", { /* ... */ });
Naite.t("post:delete", { /* ... */ });
3

최소 정보

// ✅ 올바른 방법: 필요한 정보만
Naite.t("user:create", {
  userId: user.id,
  username: user.username,
});

// ❌ 잘못된 방법: 불필요한 정보까지
Naite.t("user:create", {
  ...user,              // 모든 필드
  ...request,           // Request 전체
  ...context,           // Context 전체
});
4

의미 있는 위치에 로깅

// ✅ 올바른 방법: 중요한 분기점에
async function processUser(user: User) {
  Naite.t("user:process:start", { userId: user.id });
  
  if (user.isAdmin) {
    Naite.t("user:process:admin", { userId: user.id });
    await processAdmin(user);
  } else {
    Naite.t("user:process:regular", { userId: user.id });
    await processRegular(user);
  }
  
  Naite.t("user:process:done", { userId: user.id });
}

주의사항

Naite.t() 사용 시 주의사항:
  1. 테스트 전용: NODE_ENV === "test"에서만 동작합니다.
  2. Context 필요: Sonamu.getContext()가 있어야 합니다. bootstrap의 runWithMockContext() 안에서만 사용하세요.
  3. 직렬화 권장: VSCode Extension 전송을 위해 직렬화 가능한 값을 권장합니다.
  4. 과도한 로깅 금지: 루프 안에서 호출하면 성능이 저하됩니다.
  5. 키 규칙: module:function:action 형식을 권장합니다.

다음 단계