LogTape가 JavaScript/TypeScript를 위한 최고의 로깅 라이브러리가 되어야 하는 이유

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

다양하고 끊임없이 진화하는 JavaScript 생태계에서 로깅은 애플리케이션 개발, 디버깅 및 모니터링을 위한 중요한 구성 요소로 남아 있습니다. 수많은 로깅 라이브러리가 존재하지만, LogTape는 단순함, 유연성 및 크로스 런타임 호환성의 독특한 조합으로 돋보입니다. 애플리케이션이나 라이브러리를 구축하든 상관없이 LogTape가 다음 JavaScript 또는 TypeScript 프로젝트에서 고려해야 할 이유를 살펴보겠습니다.

의존성 없음: 가벼운 풋프린트

LogTape의 가장 매력적인 특징 중 하나는 의존성이 전혀 없다는 점입니다. "의존성 지옥"이 많은 JavaScript 프로젝트를 괴롭히는 시대에, LogTape는 상쾌한 대안을 제공합니다:

// LogTape 자체 외에 추가 패키지를 설치할 필요 없음
import { configure, getConsoleSink, getLogger } from "@logtape/logtape";

이러한 제로 의존성 접근 방식은 여러 가지 장점을 제공합니다:

번들 크기 감소
전이적 의존성이 없어 패키지 크기가 작아짐
향상된 안정성
업스트림 의존성으로 인한 호환성 깨짐 위험 없음
단순화된 보안
서드파티 코드로부터의 잠재적 취약점 감소
낮은 통합 오버헤드
특히 사용자에게 추가 의존성 부담을 주고 싶지 않은 라이브러리 작성자에게 가치 있음

런타임 다양성: 한 번 작성하고 어디서나 로깅하기

많은 인기 있는 로깅 라이브러리가 주로 Node.js에 초점을 맞추는 반면, LogTape는 다양한 JavaScript 런타임에서 원활한 지원을 제공합니다:

이러한 런타임 유연성은 배포 환경에 관계없이 일관된 로깅 패턴을 사용할 수 있음을 의미합니다:

// 동일한 API가 모든 JavaScript 런타임에서 원활하게 작동
import { getLogger } from "@logtape/logtape";

const logger = getLogger(["my-service", "user-management"]);

// Node.js, Deno, Bun, 브라우저 또는 엣지 함수에서 작동
logger.info`User ${userId} logged in successfully`;

여러 플랫폼에서 작업하거나 런타임 간 전환하는 프로젝트의 팀에게 이러한 일관성은 매우 가치 있습니다. 다른 로깅 라이브러리나 접근 방식을 배울 필요 없이 LogTape는 어디서나 동일한 방식으로 작동합니다.

계층적 카테고리: 세밀한 제어

LogTape의 계층적 카테고리 시스템은 JavaScript 로깅 라이브러리 중에서 놀랍게도 드문 뛰어난 기능입니다. 카테고리를 사용하면 로그를 트리와 같은 구조로 구성할 수 있습니다:

// 부모 카테고리
const appLogger = getLogger(["my-app"]);

// 자식 카테고리는 부모로부터 설정을 상속받음
const dbLogger = getLogger(["my-app", "database"]);

// 손자 카테고리
const queryLogger = getLogger(["my-app", "database", "queries"]);

// getChild()를 사용한 대체 접근 방식
const userLogger = appLogger.getChild("users");
const authLogger = userLogger.getChild("auth");

이러한 계층적 접근 방식은 강력한 이점을 제공합니다:

대상 필터링
애플리케이션의 다른 부분에 대해 다른 로그 레벨 구성 가능
상속
자식 로거가 부모로부터 설정을 상속받아 구성 오버헤드 감소
조직적 명확성
로그가 자연스럽게 애플리케이션의 모듈 구조를 따름

다음은 다양한 카테고리에 대한 로깅 레벨을 구성하는 방법의 예입니다:

await configure({
  sinks: {
    console: getConsoleSink(),
    file: getFileSink("app.log"),
  },
  loggers: [
    // 모든 앱 로그에 대한 기본 구성
    { 
      category: ["my-app"], 
      lowestLevel: "info", 
      sinks: ["console", "file"] 
    },
    // 데이터베이스 컴포넌트에 대해서만 더 상세한 로깅
    { 
      category: ["my-app", "database"], 
      lowestLevel: "debug", 
      sinks: ["file"] 
    }
  ]
});

이 구성을 통해 "info" 레벨 이상의 모든 애플리케이션 로그는 콘솔과 파일 모두에 기록되는 반면, 데이터베이스 관련 로그는 더 상세한 "debug" 레벨 정보를 포함하지만 로그 파일에만 기록됩니다.

구조화된 로깅: 단순 텍스트를 넘어서

현대적인 로깅은 단순한 텍스트 문자열을 넘어섭니다. LogTape는 로그 항목을 일반 텍스트가 아닌 데이터 객체로 취급하는 구조화된 로깅을 채택합니다:

logger.info("User logged in", {
  userId: 123456,
  username: "johndoe",
  loginTime: new Date(),
  ipAddress: "192.168.1.1"
});

LogTape는 또한 메시지의 플레이스홀더를 지원하여 구조화된 데이터와 사람이 읽을 수 있는 텍스트를 연결합니다:

logger.info("User {username} (ID: {userId}) logged in from {ipAddress}", {
  userId: 123456,
  username: "johndoe",
  ipAddress: "192.168.1.1"
});

구조화된 로깅은 상당한 이점을 제공합니다:

향상된 검색 가능성
텍스트 파싱 대신 특정 필드 값으로 검색
더 나은 분석
구조화된 필드에 대한 데이터 분석 수행
일관된 형식
표준화된 로그 형식 적용
기계 판독 가능
로그 관리 시스템에 의한 처리 용이

성능에 민감한 애플리케이션의 경우, LogTape는 구조화된 데이터의 지연 평가를 제공합니다:

logger.debug("Performance metrics", () => ({
  memoryUsage: process.memoryUsage(),
  cpuUsage: process.cpuUsage(),
  timestamp: performance.now()
}));

이 함수는 디버그 레벨이 활성화된 경우에만 평가되므로, 억제된 로그 레벨에 대한 불필요한 계산을 방지합니다.

매우 간단한 싱크와 필터: 최소한의 상용구 코드

LogTape의 확장성에 대한 접근 방식은 놀라울 정도로 간단합니다. 사용자 정의 싱크(출력 대상)와 필터를 만드는 데 최소한의 상용구 코드만 필요합니다.

매우 간단한 싱크

LogTape에서 싱크는 로그 레코드를 받는 함수일 뿐입니다:

// 사용자 정의 싱크 생성은 함수를 정의하는 것만큼 간단합니다
const mySink = (record) => {
  const timestamp = new Date(record.timestamp).toISOString();
  const level = record.level.toUpperCase();
  const category = record.category.join('.');
  
  // 사용자 정의 대상으로 전송
  myCustomLogService.send({
    time: timestamp,
    priority: level,
    component: category,
    message: record.message,
    ...record.properties
  });
};

// 구성에서 사용자 정의 싱크 사용
await configure({
  sinks: {
    console: getConsoleSink(),
    custom: mySink
  },
  loggers: [
    { category: ["my-app"], sinks: ["console", "custom"] }
  ]
});

이를 클래스 확장, 여러 메서드 구현 또는 특정 패턴 따르기를 요구하는 다른 라이브러리와 비교해보세요. LogTape의 접근 방식은 상쾌할 정도로 간단합니다.

간단한 필터

마찬가지로, LogTape의 필터는 불리언을 반환하는 함수일 뿐입니다:

// 높은 우선순위 또는 특정 컴포넌트 로그만 통과시키는 필터
const importantLogsFilter = (record) => {
  // 항상 오류 포함
  if (record.level === "error" || record.level === "fatal") {
    return true;
  }
  
  // 항상 결제 관련 로그 포함
  if (record.category.includes("payments")) {
    return true;
  }
  
  // 다른 로그 필터링
  return false;
};

await configure({
  // ...싱크 구성
  filters: {
    important: importantLogsFilter
  },
  loggers: [
    { 
      category: ["my-app"], 
      sinks: ["alertSystem"], 
      filters: ["important"] 
    }
  ]
});

LogTape는 또한 레벨 기반 필터링을 위한 편리한 축약형을 제공합니다:

await configure({
  // ...싱크 구성
  filters: {
    // "warning" 레벨 이상에 대한 필터 생성
    warningAndAbove: "warning"
  },
  loggers: [
    { 
      category: ["my-app"], 
      sinks: ["console"], 
      filters: ["warningAndAbove"] 
    }
  ]
});

라이브러리 작성자에게 완벽한 선택

LogTape는 사용자에게 부담을 주지 않으면서 로깅을 통합하고자 하는 라이브러리 작성자에게 특히 적합합니다. 핵심 철학은 간단합니다:

  1. 라이브러리는 로깅 출력 지점을 제공
  2. 애플리케이션은 이러한 로그 처리 방법을 구성

다음은 라이브러리가 LogTape를 구현하는 방법의 예입니다:

// my-awesome-lib/database.js
import { getLogger } from "@logtape/logtape";

export class Database {
  private logger = getLogger(["my-awesome-lib", "database"]);

  constructor(host, port, user) {
    this.host = host;
    this.port = port;
    this.user = user;
  }

  connect() {
    this.logger.info("Connecting to database", {
      host: this.host,
      port: this.port,
      user: this.user
    });
    
    // 연결 로직...
    
    this.logger.debug("Connection established");
  }
  
  query(sql) {
    this.logger.debug("Executing query", { sql });
    // 쿼리 로직...
  }
}

핵심은 라이브러리가 절대 configure()를 호출하지 않는다는 것입니다. 대신, 적절한 레벨과 컨텍스트 데이터가 있는 유용한 로그 출력 지점을 제공합니다.

라이브러리를 사용하는 애플리케이션은 이러한 로그를 처리하는 방법을 정확히 결정할 수 있습니다:

// 애플리케이션 코드
import { configure, getConsoleSink } from "@logtape/logtape";
import { Database } from "my-awesome-lib";

// 로그 처리 방법 구성
await configure({
  sinks: {
    console: getConsoleSink(),
    file: getFileSink("app.log")
  },
  loggers: [
    // 모든 라이브러리 로그 처리
    { 
      category: ["my-awesome-lib"], 
      lowestLevel: "info", 
      sinks: ["file"] 
    },
    // 개발 중 데이터베이스 컴포넌트에 대해 더 상세한 로깅
    { 
      category: ["my-awesome-lib", "database"], 
      lowestLevel: "debug", 
      sinks: ["console", "file"] 
    }
  ]
});

// 라이브러리 사용
const db = new Database("localhost", 5432, "user");
db.connect();

이러한 관심사 분리는 여러 이점을 제공합니다:

  1. 라이브러리 사용자가 로그 처리를 완전히 제어할 수 있음
  2. 라이브러리가 구현 세부 사항을 강요하지 않으면서 풍부한 로깅을 제공할 수 있음
  3. 애플리케이션 로깅 구성과 충돌할 위험이 없음
  4. 라이브러리는 내부적으로 "시끄러울" 수 있지만 애플리케이션이 필요에 따라 필터링할 수 있음

더 풍부한 로깅을 위한 컨텍스트

LogTape는 여러 로그 메시지에 걸쳐 일관된 속성을 추가하기 위한 컨텍스트 메커니즘을 제공합니다. 이는 시스템을 통한 요청 추적에 특히 가치가 있습니다:

명시적 컨텍스트

const logger = getLogger(["my-app", "api"]);

// 컨텍스트가 있는 로거 생성
const requestLogger = logger.with({
  requestId: "abc-123",
  userId: 42,
  endpoint: "/users"
});

// 이 로거의 모든 로그에는 컨텍스트 속성이 포함됨
requestLogger.info("Processing request");
requestLogger.debug("Validating input");
requestLogger.info("Request completed", { durationMs: 120 });

암시적 컨텍스트 (v0.7.0+)

함수 호출 간에 명시적 전달 없이 컨텍스트를 적용하고 싶은 경우:

import { getLogger, withContext } from "@logtape/logtape";

function handleRequest(req, res) {
  withContext({ 
    requestId: req.id, 
    userId: req.user?.id 
  }, () => {
    // 이 함수 내부와 이 함수가 호출하는 모든 함수의 로그에
    // 자동으로 컨텍스트 속성이 포함됨
    processRequest(req, res);
  });
}

function processRequest(req, res) {
  // 컨텍스트를 전달할 필요 없음 - 자동으로 사용 가능
  getLogger(["my-app", "processor"]).info("Processing data");
  
  // 컨텍스트를 상속받는 다른 함수 호출
  validateInput(req.body);
}

function validateInput(data) {
  // 이 로그도 requestId와 userId를 얻음
  getLogger(["my-app", "validator"]).debug("Validating input", { data });
}

이러한 암시적 컨텍스트 기능은 모든 함수 호출을 통해 컨텍스트를 수동으로 전달하지 않고도 여러 코드 계층을 통해 요청을 추적하는 데 매우 가치가 있습니다.

LogTape가 최선의 선택이 아닐 수 있는 경우

LogTape는 많은 사용 사례에 대해 매력적인 장점을 제공하지만, 모든 상황에서 최선의 선택은 아닙니다:

극단적인 성능 요구사항
애플리케이션이 초당 수만 개의 항목을 로깅하고 원시 성능이 최우선 순위인 경우, Pino와 같은 최적화된 로깅 처리량에 중점을 둔 특수 고성능 라이브러리가 더 적합할 수 있습니다.
광범위한 사전 구축 통합
사용자 정의 코드를 작성하지 않고 수많은 특정 시스템(Elasticsearch, Graylog 등)과의 즉각적인 통합이 필요한 경우, Winston의 풍부한 트랜스포트 생태계가 더 빠른 시작점을 제공할 수 있습니다.
특정 로깅 요구사항이 있는 레거시 시스템
Java 또는 다른 환경의 특정 로깅 패턴을 중심으로 구축된 시스템을 유지 관리하는 경우, Log4js와 같은 목적에 맞게 구축된 라이브러리가 더 친숙한 API를 제공할 수 있습니다.
최소한의 로깅 요구사항이 있는 웹 브라우저 전용 애플리케이션
레벨이 있는 기본 콘솔 출력만 원하는 매우 간단한 웹 브라우저 전용 로깅 요구사항의 경우, loglevel과 같은 더 간단한 라이브러리로도 충분할 수 있습니다.

결론

LogTape는 실제 개발 과제를 해결하는 독특한 기능 조합을 제공하여 혼잡한 JavaScript 로깅 환경에서 돋보입니다:

  • 제로 의존성으로 가볍고 안전한 기반 제공
  • 런타임 다양성으로 Node.js, Deno, Bun, 브라우저 및 엣지 함수 지원
  • 계층적 카테고리로 더 나은 로그 구성 및 필터링
  • 구조화된 로깅으로 향상된 분석 및 검색 가능성
  • 최소한의 상용구 코드로 간단한 확장 메커니즘
  • 관심사 분리를 존중하는 라이브러리 친화적 설계

애플리케이션이나 라이브러리를 구축하든, 여러 JavaScript 런타임에서 작업하든, 또는 단순히 깔끔하고 잘 설계된 로깅 솔루션을 찾고 있든, LogTape는 진지한 고려를 받을 만합니다. 그 사려 깊은 설계는 단순함과 강력한 기능 사이의 균형을 맞추며, JavaScript 로깅 라이브러리의 일반적인 함정을 피합니다.

자세한 정보와 상세한 문서는 LogTape의 공식 웹사이트를 방문하세요.

6

2 comments

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/0196e16d-24a5-719f-bdfe-091a2162cbd2 on your instance and reply to it.

1
1