JavaScript 라이브러리를 만들고 로깅이 필요하다면, LogTape를 좋아하게 될 것입니다

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub
JavaScript 라이브러리를 만드는 것은 섬세한 균형이 필요합니다. 유용한 기능을 제공하면서도 사용자의 선택과 제약을 존중해야 합니다. 많은 라이브러리가 디버깅, 모니터링, 사용자 지원을 위해 필요로 하는 로깅에 있어서 이러한 균형은 특히 어려워집니다.
JavaScript 생태계는 이 문제에 대해 다양한 접근 방식을 발전시켜 왔으며, 각각은 고유한 장단점을 가지고 있습니다. LogTape는 특별히 라이브러리 제작자를 위해 설계된 다른 방식을 제공합니다.
라이브러리 로깅의 현재 상태
이전에 라이브러리를 만들어 본 적이 있다면, 로깅 딜레마를 경험했을 것입니다. 사용자가 통합 문제를 디버깅하거나, 내부 상태 변화를 추적하거나, 성능 병목 현상에 대한 통찰력을 제공하는 데 도움이 되는 로깅 기능이 필요합니다. 하지만 이 기능을 어떻게 책임감 있게 추가할 수 있을까요?
현재 인기 있는 라이브러리들은 이 문제를 여러 방식으로 처리하고 있습니다:
- 디버그 접근법
- Express와 Socket.IO 같은 라이브러리는 경량의
debug
패키지를 사용하여 사용자가 환경 변수(DEBUG=express:*
)를 통해 로깅을 활성화할 수 있게 합니다. 이 방식은 잘 작동하지만 사용자의 기존 로깅 인프라와 통합되지 않는 별도의 로깅 시스템을 만듭니다. - 커스텀 로깅 시스템
- Mongoose와 Prisma 같은 라이브러리는 자체 로깅 메커니즘을 구축했습니다. Mongoose는
mongoose.set('debug', true)
를 제공하고, Prisma는 자체 로깅 구성을 사용합니다. 이러한 접근 방식은 작동하지만, 각 라이브러리마다 사용자가 별도로 배워야 하는 자체 로깅 API를 만듭니다. - 애플리케이션 중심 라이브러리
- winston, Pino, Bunyan은 강력한 로깅 솔루션이지만, 주로 라이브러리보다는 애플리케이션을 위해 설계되었습니다. 이들을 라이브러리에서 사용하면 상당한 의존성을 부과하고 사용자의 기존 로깅 선택과 충돌할 가능성이 있습니다.
- 로깅 없음
- 많은 라이브러리 제작자들은 복잡성을 완전히 피하고, 라이브러리를 침묵 상태로 두어 모든 관련자의 디버깅을 더 어렵게 만듭니다.
- 의존성 주입
- 일부 라이브러리는 구성 또는 생성자 매개변수를 통해 애플리케이션에서 로거 인스턴스를 받는 더 정교한 접근 방식을 채택합니다. 이는 관심사의 깔끔한 분리를 유지하고 라이브러리가 애플리케이션이 선택한 로깅 시스템을 사용할 수 있게 합니다. 그러나 이 패턴은 더 복잡한 API를 요구하고 사용자에게 로깅 의존성을 이해하고 구성하는 추가적인 부담을 줍니다.
각 접근 방식은 실제 문제에 대한 합리적인 해결책을 나타내지만, 핵심 긴장을 완전히 해결하지는 못합니다: 사용자에게 선택을 강요하지 않으면서 어떻게 가치 있는 진단 기능을 제공할 수 있을까요?
단편화 문제
라이브러리마다 로깅을 자체적으로 해결할 때 발생하는 또 다른 문제는 단편화입니다. 웹 프레임워크로 Express, 실시간 통신을 위한 Socket.IO, HTTP 요청을 위한 Axios, 데이터베이스 접근을 위한 Mongoose 및 기타 여러 특수 라이브러리를 사용할 수 있는 일반적인 Node.js 애플리케이션을 생각해보세요.
각 라이브러리는 잠재적으로 자체 로깅 접근 방식을 가지고 있습니다:
- Express는
DEBUG=express:*
사용 - Socket.IO는
DEBUG=socket.io:*
사용 - Mongoose는
mongoose.set('debug', true)
사용 - Axios는
axios-logger
또는 유사한 패키지를 사용할 수 있음 - Redis 클라이언트는 자체 디버그 구성을 가짐
- 인증 라이브러리는 종종 자체 로깅 메커니즘을 포함함
애플리케이션 개발자 관점에서 이는 관리 문제를 만듭니다. 개발자는 각각 고유한 구문, 기능 및 특성을 가진 여러 다른 로깅 시스템을 배우고 구성해야 합니다. 로그는 일관되지 않은 형식으로 다양한 출력에 분산되어 애플리케이션에서 무슨 일이 일어나고 있는지 통합된 뷰를 얻기 어렵게 만듭니다.
통합 부족은 또한 구조화된 로깅, 로그 상관관계, 중앙 집중식 로그 관리와 같은 강력한 기능을 사용 중인 모든 라이브러리에서 일관되게 구현하기가 훨씬 어려워진다는 것을 의미합니다.
LogTape의 접근 방식
LogTape는 "라이브러리 우선 설계"라고 불릴 수 있는 방식으로 이러한 문제를 해결하려고 시도합니다. 핵심 원칙은 간단하지만 잠재적으로 강력합니다: 로깅이 구성되지 않으면 아무 일도 일어나지 않습니다. 출력, 오류, 부작용이 없이 완전한 투명성만 있습니다.
이 접근 방식을 통해 원하지 않는 사용자에게 영향을 주지 않고도 라이브러리에 포괄적인 로깅을 추가할 수 있습니다. 사용자가 라이브러리를 가져와 코드를 실행할 때, 누군가 명시적으로 로깅을 구성할 때까지 LogTape의 로깅 호출은 본질적으로 아무 작업도 수행하지 않습니다. 라이브러리의 동작에 대한 통찰력을 원하는 사용자는 옵트인할 수 있으며, 원하지 않는 사용자는 완전히 영향을 받지 않습니다.
더 중요한 것은, 사용자가 로깅을 구성하기로 선택할 때 모든 LogTape 지원 라이브러리를 단일 통합 구성 시스템을 통해 관리할 수 있다는 것입니다. 이는 모든 라이브러리 로그에 대해 하나의 일관된 API, 하나의 로그 형식, 하나의 대상을 의미하면서도 어떤 라이브러리에서 무엇이 로깅되는지에 대한 세밀한 제어를 허용합니다.
Note
이 접근 방식은 완전히 새로운 것은 아닙니다—Python의 표준 logging
라이브러리에서 영감을 받았으며, 이는 통합된 로깅 생태계를 성공적으로 만들었습니다. Python에서는 Requests, SQLAlchemy, Django 컴포넌트와 같은 라이브러리가 모두 표준 로깅 프레임워크를 사용하여 개발자가 단일하고 일관된 시스템을 통해 모든 라이브러리 로깅을 구성할 수 있게 합니다. 이는 실용적이고 강력한 것으로 입증되어 애플리케이션 개발자를 위한 단순성을 유지하면서 전체 Python 생태계에서 풍부한 진단 기능을 가능하게 합니다.
// 라이브러리 코드에서 - 포함해도 완전히 안전함
import { getLogger } from "@logtape/logtape";
const logger = getLogger(["my-awesome-lib", "database"]);
export function connectToDatabase(config) {
logger.debug("데이터베이스 연결 시도 중", { config });
// ... 여러분의 로직
logger.info("데이터베이스 연결 설정됨");
}
의존성 고려사항
현대 JavaScript 개발에는 의존성에 대한 신중한 고려가 필요합니다. winston과 Pino 같은 인기 있는 로깅 라이브러리는 잘 유지되고 널리 신뢰받고 있지만, 자체 의존성 트리를 가지고 있습니다. 예를 들어, winston은 17개의 의존성을 포함하고, Pino는 1개를 포함합니다.
라이브러리 제작자에게 이는 고려사항을 만듭니다: 추가하는 모든 의존성은 사용자가 원하든 원하지 않든 사용자의 의존성이 됩니다. 이것이 반드시 문제가 되는 것은 아니지만(많은 우수한 라이브러리가 의존성을 가짐), 사용자를 대신하여 내리는 선택을 나타냅니다.
LogTape는 의존성이 전혀 없는 다른 접근 방식을 취합니다. 이는 단순한 철학적 선택이 아니라 라이브러리 사용자에게 실질적인 영향을 미칩니다. 사용자는 node_modules에 추가 패키지가 표시되지 않고, 로깅 관련 의존성에 대한 공급망 고려사항을 걱정할 필요가 없으며, 로깅 선택과 자신의 선택 사이에 잠재적인 버전 충돌에 직면하지 않을 것입니다.
압축 및 gzip 처리된 크기가 단 5.3KB로, LogTape는 번들에 최소한의 무게만 추가합니다. 설치 과정이 더 빨라지고, 의존성 트리가 더 깔끔하게 유지되며, 보안 감사는 라이브러리의 핵심 기능을 직접 제공하는 의존성에 집중됩니다.
호환성 체인 깨기
다음은 익숙할 수 있는 도전 과제입니다: 라이브러리가 ESM과 CommonJS 환경을 모두 지원하기를 원합니다. 아마도 일부 사용자는 CommonJS에 의존하는 레거시 Node.js 프로젝트에서 작업하고 있고, 다른 사용자는 최신 ESM 설정을 사용하거나 브라우저용으로 빌드하고 있을 수 있습니다.
의존성이 있을 때 이 도전 과제가 분명해집니다. ESM 모듈은 문제 없이 CommonJS 모듈을 가져올 수 있지만, 그 반대는 사실이 아닙니다—CommonJS 모듈은 ESM 전용 패키지를 require할 수 없습니다(적어도 Node.js 22+ 실험적 기능이 안정화될 때까지). 이는 비대칭적인 호환성 제약을 만듭니다.
라이브러리가 ESM 전용 패키지에 의존한다면, CommonJS 환경에서는 사용할 수 없기 때문에 라이브러리도 효과적으로 ESM 전용이 됩니다. 이는 체인에서 단 하나의 ESM 전용 의존성이라도 CommonJS 사용자 지원을 방해할 수 있음을 의미합니다.
LogTape는 ESM과 CommonJS를 완전히 지원하므로, 이러한 제한을 강제하는 약한 연결 고리가 되지 않습니다. 사용자가 레거시 Node.js 프로젝트, 최첨단 ESM 애플리케이션 또는 하이브리드 환경에서 작업하든 상관없이 LogTape는 그들의 설정에 원활하게 적응합니다.
더 중요한 것은, LogTape가 (단순히 CommonJS로 가져올 수 있는 것이 아니라) 네이티브 ESM 지원을 제공할 때, 최신 번들러에서 트리 쉐이킹을 가능하게 한다는 점입니다. 트리 쉐이킹은 번들러가 빌드 과정에서 사용되지 않는 코드를 제거할 수 있게 하지만, ESM만 제공하는 정적 import
/export
구조가 필요합니다. CommonJS 모듈은 ESM 프로젝트로 가져올 수 있지만, 종종 최적화할 수 없는 불투명한 블록으로 취급되어 최종 번들에 사용되지 않는 코드가 포함될 수 있습니다.
최소한의 영향을 목표로 하는 로깅 라이브러리의 경우, 특히 번들 크기가 중요한 애플리케이션에서 이러한 최적화 기능은 의미가 있을 수 있습니다.
범용 런타임 지원
오늘날 JavaScript 생태계는 인상적인 범위의 런타임 환경을 포괄합니다. 여러분의 라이브러리는 Node.js 서버, Deno 스크립트, Bun 애플리케이션, 웹 브라우저 또는 엣지 함수에서 실행될 수 있습니다. LogTape는 폴리필, 호환성 레이어 또는 런타임별 코드 없이 이러한 모든 환경에서 동일하게 작동합니다.
이러한 보편성은 로깅 선택이 사용자가 접할 수 있는 모든 환경에서 작동할지 걱정하는 대신 라이브러리의 핵심 기능에 집중할 수 있음을 의미합니다. 누군가가 여러분의 라이브러리를 Cloudflare Worker, Next.js 애플리케이션 또는 Deno CLI 도구로 가져오든, 로깅 동작은 일관되고 신뢰할 수 있게 유지됩니다.
타협 없는 성능
라이브러리 제작자들이 로깅에 대해 종종 갖는 한 가지 우려는 성능 영향입니다. 사용자가 여러분의 라이브러리를 고성능 애플리케이션으로 가져오면 어떻게 될까요? 메모리가 제한된 환경에서 실행 중이라면 어떨까요?
LogTape는 로깅이 비활성화되었을 때 놀라운 효율성으로 이 문제를 해결합니다. 구성되지 않은 LogTape 호출의 오버헤드는 사실상 제로로, 사용 가능한 다른 로깅 솔루션 중 가장 낮은 수준입니다. 이는 활성화하지 않는 사용자에게 성능 영향을 걱정하지 않고도 개발 및 디버깅 목적으로 라이브러리 전체에 상세한 로깅을 추가할 수 있음을 의미합니다.
로깅이 활성화되면, LogTape는 특히 개발 중 가장 일반적인 로깅 대상인 콘솔 출력에서 다른 라이브러리보다 일관되게 더 나은 성능을 보입니다.
네임스페이스 충돌 방지
동일한 애플리케이션을 공유하는 라이브러리들이 모두 동일한 네임스페이스로 출력할 때 로깅 혼란을 만들 수 있습니다. LogTape의 계층적 카테고리 시스템은 라이브러리가 자체 네임스페이스를 사용하도록 권장함으로써 이 문제를 우아하게 해결합니다.
여러분의 라이브러리는 ["my-awesome-lib", "database"]
또는 ["my-awesome-lib", "validation"]
과 같은 카테고리를 사용하여 로그가 다른 라이브러리 및 메인 애플리케이션과 명확하게 분리되도록 할 수 있습니다. LogTape를 구성하는 사용자는 다른 라이브러리와 해당 라이브러리 내의 다른 구성 요소에 대해 독립적으로 로깅 수준을 제어할 수 있습니다.
그냥 작동하는 개발자 경험
LogTape는 처음부터 TypeScript로 구축되어 TypeScript 기반 라이브러리가 추가 의존성이나 타입 패키지 없이 완전한 타입 안전성을 얻을 수 있습니다. API는 자연스럽고 현대적이며, 현대 JavaScript 개발 관행과 잘 통합되는 템플릿 리터럴과 구조화된 로깅 패턴을 모두 지원합니다.
// 템플릿 리터럴 스타일 - 자연스럽게 느껴짐
logger.info`사용자 ${userId}가 액션 ${action}을 수행함`;
// 구조화된 로깅 - 모니터링에 적합
logger.info("사용자 액션 완료됨", { userId, action, duration });
실용적인 통합
실제로 라이브러리에서 LogTape를 사용하는 것은 놀랍도록 간단합니다. 로거를 가져오고, 적절하게 네임스페이스가 지정된 카테고리를 만들고, 의미가 있는 곳에 로깅하면 됩니다. 구성, 설정, 복잡한 초기화 시퀀스가 필요 없습니다.
import { getLogger } from "@logtape/logtape";
const logger = getLogger(["my-lib", "api"]);
export async function fetchUserData(userId) {
logger.debug("사용자 데이터 가져오는 중", { userId });
try {
const response = await api.get(`/users/${userId}`);
logger.info("사용자 데이터 성공적으로 검색됨", {
userId,
status: response.status
});
return response.data;
} catch (error) {
logger.error("사용자 데이터 가져오기 실패", { userId, error });
throw error;
}
}
이러한 로그를 보고 싶은 사용자를 위한 구성도 똑같이 간단합니다:
import { configure, getConsoleSink } from "@logtape/logtape";
await configure({
sinks: { console: getConsoleSink() },
loggers: [
{ category: ["my-lib"], lowestLevel: "info", sinks: ["console"] }
]
});
전환 연결하기
잠재적 사용자가 이미 다른 로깅 시스템에 투자한 경우, LogTape는 winston과 Pino와 같은 인기 있는 라이브러리를 위한 어댑터를 제공합니다. 이를 통해 LogTape 지원 라이브러리가 기존 로깅 인프라와 통합되어 애플리케이션이 이미 사용 중인 시스템을 통해 로그를 라우팅할 수 있습니다.
이러한 어댑터의 존재는 솔직한 진실을 드러냅니다: LogTape는 아직 JavaScript 생태계에서 널리 채택된 표준이 아닙니다. 대부분의 애플리케이션은 여전히 확립된 로깅 라이브러리를 중심으로 구축되어 있으며, 사용자에게 로깅 접근 방식을 완전히 재구성하도록 요청하는 것은 비현실적일 것입니다. 어댑터는 실용적인 타협을 나타냅니다—라이브러리 제작자가 사용자의 기존 투자와 선호도를 존중하면서 LogTape의 라이브러리 친화적인 설계를 활용할 수 있게 합니다.
이 접근 방식은 현대적이고 의존성이 없는 로깅 API를 라이브러리 제작자에게 제공하면서도 채택에 대한 마찰을 줄입니다. 아마도 시간이 지남에 따라 더 많은 라이브러리가 이 패턴을 채택하고 더 많은 개발자가 그 이점을 경험하면서 이러한 어댑터의 필요성이 줄어들 수 있습니다. 하지만 지금은 LogTape의 비전과 생태계의 현재 현실 사이의 실용적인 다리 역할을 합니다.
고려할 가치가 있는 선택
궁극적으로, 라이브러리에 LogTape를 선택하는 것은 라이브러리와 애플리케이션 간의 관계에 대한 특정 철학을 나타냅니다. 이는 선택을 보존하면서 기능을 제공하고, 강요를 피하면서 통찰력을 제공하는 것에 관한 것입니다.
debug
패키지를 사용하든, 애플리케이션 중심 로거를 사용하든, 커스텀 솔루션을 사용하든 전통적인 접근 방식은 각각 장점이 있으며 커뮤니티에 잘 기여해 왔습니다. LogTape는 단순히 또 다른 옵션을 제공합니다: JavaScript 생태계에서 라이브러리가 차지하는 독특한 위치를 위해 특별히 설계된 옵션입니다.
라이브러리 제작자에게 이 접근 방식은 몇 가지 실용적인 이점을 제공할 수 있습니다. 여러분의 라이브러리는 개발, 디버깅 및 사용자 지원을 위한 상세한 로깅을 얻는 동시에, 사용자는 이러한 기능을 사용할지 여부와 방법에 대한 완전한 자율성을 유지합니다.
더 넓은 이점은 JavaScript 생태계 전반에 걸쳐 더 응집력 있는 로깅 경험일 수 있습니다—라이브러리가 애플리케이션이 채택하기로 선택한 로깅 전략과 원활하게 통합되는 풍부한 진단 정보를 제공할 수 있는 경험입니다.
모든 의존성 결정이 영향을 미치는 세계에서, LogTape는 고려할 가치가 있는 접근 방식을 제공합니다: 사용자의 선호도와 기존 선택을 존중하면서 라이브러리의 기능을 향상시키는 방법입니다.