Naite.t()를 사용하여 테스트 실행 중 데이터를 기록하는 방법을 알아봅니다.
Naite.t() 개요
테스트 전용 NODE_ENV 체크 프로덕션 영향 없음
Naite.t()란?
Naite.t()는 테스트 실행 중 특정 시점의 데이터를 기록하는 함수입니다. 첫 번째 인자로 고유한 키(key)를, 두 번째 인자로 기록할 값(value)을 받습니다.
import { Naite } from "sonamu" ;
// 기본 사용
Naite . t ( "user:create" , { userId: 123 , username: "john" });
이렇게 기록된 데이터는:
테스트 실행 중 언제든 Naite.get()으로 조회 가능
VSCode Extension으로 실시간 전송
콜스택과 시간 정보와 함께 저장
기본 사용법
단순 로깅
가장 기본적인 사용법은 함수의 입출력을 기록하는 것입니다.
예시 1: 사용자 생성
예시 2: 게시글 업데이트
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 }, ... }]
Naite . t ( "user:create" , { userId: 1 });
Naite . t ( "user:create" , { userId: 2 });
Naite . t ( "user:create" , { userId: 3 });
// Store:
// "user:create" => [
// { key, data: { userId: 1 }, ... },
// { key, data: { userId: 2 }, ... },
// { key, data: { userId: 3 }, ... }
// ]
키 네이밍 전략
계층적 구조
콜론(:)으로 계층을 구분하면 나중에 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 작업
// ❌ 평면적 구조
Naite . t ( "userCreate" , { /* ... */ });
Naite . t ( "userUpdate" , { /* ... */ });
Naite . t ( "syncerRender" , { /* ... */ });
// wildcard 사용 불가
// "user*"로는 매칭 안 됨
권장 패턴
module:function
가장 기본적인 패턴입니다. Naite . t ( "user:create" , { /* ... */ });
Naite . t ( "post:update" , { /* ... */ });
Naite . t ( "syncer:render" , { /* ... */ });
module:function:action
더 상세한 추적이 필요할 때 사용합니다. Naite . t ( "user:create:input" , { /* ... */ });
Naite . t ( "user:create:validation" , { /* ... */ });
Naite . t ( "user:create:db" , { /* ... */ });
Naite . t ( "user:create:done" , { /* ... */ });
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 생성!
}
// ✅ 요약 정보만
Naite . t ( "loop:start" , {
count: 10000 ,
timestamp: Date . now ()
});
for ( let i = 0 ; i < 10000 ; i ++ ) {
// 로깅 없음
processItem ( data [ i ]);
}
Naite . t ( "loop:done" , {
count: 10000 ,
duration: Date . now () - startTime ,
successCount: results . filter ( r => r . success ). length
});
// ✅ 문제가 있을 때만
for ( let i = 0 ; i < 10000 ; i ++ ) {
const result = processItem ( data [ i ]);
if ( ! result . success ) {
Naite . t ( "loop:error" , {
index: i ,
error: result . error
});
}
}
값 타입과 직렬화
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 . t ( "key" , functionValue ); // Function
Naite . t ( "key" , symbolValue ); // Symbol
Naite . t ( "key" , circularRef ); // Circular reference
Naite . t ( "key" , new WeakMap ()); // WeakMap
╔════════════════════════════════════════════════════════════════╗
║ [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
베스트 프랙티스
명확한 키 사용
// ✅ 올바른 방법
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..." });
일관된 구조
// ✅ 올바른 방법: 일관된 계층
Naite . t ( "user:create" , { /* ... */ });
Naite . t ( "user:update" , { /* ... */ });
Naite . t ( "user:delete" , { /* ... */ });
Naite . t ( "post:create" , { /* ... */ });
Naite . t ( "post:update" , { /* ... */ });
Naite . t ( "post:delete" , { /* ... */ });
최소 정보
// ✅ 올바른 방법: 필요한 정보만
Naite . t ( "user:create" , {
userId: user . id ,
username: user . username ,
});
// ❌ 잘못된 방법: 불필요한 정보까지
Naite . t ( "user:create" , {
... user , // 모든 필드
... request , // Request 전체
... context , // Context 전체
});
의미 있는 위치에 로깅
// ✅ 올바른 방법: 중요한 분기점에
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() 사용 시 주의사항 :
테스트 전용 : NODE_ENV === "test"에서만 동작합니다.
Context 필요 : Sonamu.getContext()가 있어야 합니다. bootstrap의 runWithMockContext() 안에서만 사용하세요.
직렬화 권장 : VSCode Extension 전송을 위해 직렬화 가능한 값을 권장합니다.
과도한 로깅 금지 : 루프 안에서 호출하면 성능이 저하됩니다.
키 규칙 : module:function:action 형식을 권장합니다.
다음 단계