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: "john@example.com" ,
};
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 형식을 권장합니다.
다음 단계
로그 조회하기 Naite.get()으로 기록한 로그를 조회하는 방법을 배웁니다.
Naite Viewer VSCode Extension으로 로그를 시각화하는 방법을 알아봅니다.
테스트 디버깅 콜스택 추적으로 버그를 찾는 방법을 배웁니다.
Naite란? Naite 전체 개요로 돌아갑니다.