CHAPDO

개발자들이 주고받는 은밀한 쪽지 : 디버깅

2025-01-08
디버깅console.logdebugloglevel

🙃 들어가며

당시 타입 에러로 인해 분기 처리를 해야 하는 상황이었다. 상황을 조금 더 구체적으로 말하자면 TypeScript의 타입 정보가 구조 분해 할당 과정에서 손실되어 타입 에러가 발생하고 있었다.

물론 해결 방법으로는 구조 분해를 하지 않고 props 전체를 사용하는 방법 등이 있지만, 현행 코드 구조를 유지해야하는 걸림돌이 있었다. 팀원들과 의논한 결과 결정한 해결책으로는 __DEV__ 로 개발자에게만 경고를 해주는 방식이었다.

완벽한 해결책은 아니었지만, 이처럼 배포 환경에는 노출이 안 되지만 개발자들 간에 서로 은밀한 쪽지라고 해야할까? 나에게 디버깅은 그런 존재였다.

🙃 A console.log in the package #116

그러다가 내가 좋아하는 shadergradient 라이브러리에 있는 이슈를 발견했다.

이슈의 내용은, 이슈 제보자는 코드베이스에 console.log("material (onInit)", A);가 남아있어서 디버깅할 때 콘솔이 지저분해지기 때문에 이를 제거해주기를 요청했다.

하지만 메인테이너 입장에서는 해당 로그는 성능 측정을 위해 중요한 정보를 담고 있어서 완전히 제거하긴 어려운 상황이었다.

 

그래서 처음 해당 이슈를 봤을 때는, 되게 쉽게 해결이 가능하다고 보았다. 앞서 들어가기에서 언급했던 경험을 토대로 __DEV__ 환경 변수를 사용해서 개발 환경에서만 로그가 출력되도록 수정하면 되겠다고 생각을 했다. 또한 console.log 역시 console.debug로 변경하여 더 적절한 로깅 레벨을 사용했다고 보았다.

 

하지만 아래와 같은 리뷰를 받게 되었다.

Javascript
I was thinking about some methods to hide & show logs with special controls. like loglevel

 

내가 선택한 __DEV__ 환경변수를 사용하는 방식은 정적이다는 문제를 안고 있었다.

__DEV__ 환경변수는 빌드 시점에 결정되어 바이너리에 포함되므로, 런타임에서 동적으로 로그 레벨을 조정하거나 특정 상황에서만 로그를 활성화하는 등의 유연한 대응이 불가능하다. 이는 특히 프로덕션 환경에서 문제 해결을 위해 임시로 로깅이 필요한 상황에서 큰 제약이 될 수 있다.

 

그래서 추천해준 loglevel을 비롯하여 다른 디버깅 라이브러리를 찾아보게 되었다.

 

🙃 loglevel 라이브러리

log.trace(), log.debug(), log.info(), log.warn(), log.error()

 

loglevel을 보면서 인상깊었던 점은 loglevel을 남길 수 있다는 점이었다. 말장난같이 되어 버렸지만, 즉 로그의 중요도를 나타낼 수 있는 것이다. 처음에는 이게 왜 필요한가 싶었는데 코드 리뷰를 떠올리니까 존재의 당위성을 인정하게 되었다. 코드 리뷰를 떠올려보면, 리뷰가 하도 많아서 어떤 것부터 반영해야할지 막막할 때가 있다. 그러면 p1같이 중요도를 리뷰어가 남겨주면 리뷰 반영 우선순위를 정할 수 있게 된다. 이와 같은 상선에서 로그에서도 레벨을 통해 우선순위를 정해주는 것이다.

1차적으로 로그 종류로 분류하고 2차로는 개발자의 기준에 따라 해당 종류의 레벨을 정해서 궁극적으로 배포시 노출 여부를 결정하는 것이다.

 

그래서 코드를 자세히 살펴보면 (Logger 함수 위주)

Javascript
var inheritedLevel; // 부모로부터 상속받은 레벨 var defaultLevel; // 기본 레벨 var userLevel; // 사용자가 설정한 레벨 self.levels = { "TRACE": 0, // 가장 상세한 로그 "DEBUG": 1, // 디버깅용 로그 "INFO": 2, // 정보성 로그 "WARN": 3, // 경고 "ERROR": 4, // 에러 "SILENT": 5 // 로깅 비활성화 };

이렇게 레벨 종류(inheritedLevel, defaultLevel, userLevel)가 있는데

 

Javascript
// 현재 로그 레벨 가져오기 self.getLevel = function () { if (userLevel != null) return userLevel; // 1순위 if (defaultLevel != null) return defaultLevel; // 2순위 return inheritedLevel; // 3순위 }; // 로그 레벨 설정하기 self.setLevel = function (level, persist) { userLevel = normalizeLevel(level); // 레벨 값 정규화 if (persist !== false) { // 기본값 true persistLevelIfPossible(userLevel); // localStorage/쿠키에 저장 } }; // 모든 로그 활성화 self.enableAll = function(persist) { self.setLevel(self.levels.TRACE, persist); }; // 모든 로그 비활성화 self.disableAll = function(persist) { self.setLevel(self.levels.SILENT, persist); };

현재 로그 레벨 가져오기에서 알 수 있듯이 userLevel이 1순위, defaultLevel이 2순위, inheritedLevel이 3순위가 된다.

 

Javascript
var storageKey = "loglevel"; if (typeof name === "string") { storageKey += ":" + name; // 예: "loglevel:myLogger" }

그리고 이 코드에서 localStorage나 쿠키에 로그 레벨을 저장해서 페이지 새로고침해도 설정이 유지되게 함을 확인할 수 있다. 각 로거마다 고유한 storageKey를 가지고 이를 통해 설정을 유지하는 것이다.

 

Javascript
// 기본 사용 const logger = new Logger('CHAPDO'); logger.setLevel('INFO'); // INFO 레벨 이상만 출력 logger.trace('상세 정보'); // 출력 안됨 logger.debug('디버그 정보'); // 출력 안됨 logger.info('일반 정보'); // 출력됨 logger.warn('경고'); // 출력됨 logger.error('에러'); // 출력됨 // 개발 환경 logger.enableAll(); // 모든 로그 보기 // 프로덕션 환경 logger.setLevel('ERROR'); // 에러만 보기 // 또는 logger.disableAll(); // 모든 로그 숨기기

예를 들어 이렇게 CHAPDO라고 공유한 storageKey를 생성하면 설정을 계속 유지할 수 있고

내가 레벨을 INFO로 두면 trace, debug는 무시되고 그 위 레벨인 info를 포함한 warn과 error는 노출이 되는 것이다.

 

loglevel 라이브러리만 보면 안 될 거 같아서 다른 라이브러리를 하나 더 선정해서 살펴봤다. 이게 되게 어려웠던 게 사실 나는 평소에 개발할 때 console.log로 거의 확인을 하고 코드 올릴 때는 지우는 방식을 하다보니 다른 사람들은 어떻게 하는지 디버깅을 활발히 이용하는지를 몰라서 궁금했다.

 

🙃 Debug 패턴 vs Log Level 패턴

다른 디버깅 라이브러리도 살펴봤는데, debug가 있었다.

그래서 갑자기 혼란스러웠던게 그러면 console.log랑 debug가 완전 별개의 개념인가, 개념적으로 흔들렸다.

 

살펴보니

Javascript
console.log -> debug -> loglevel 단순 출력 -> 네임스페이스 기반 필터링 -> 레벨 기반 체계적 관리

내가 주로 사용하는 console.log는 간단한 값 확인, 임시 디버깅 용도이고,

특정 기능/모듈별 로그 관리가 필요한 경우에는 debug가 중요도 기반의 체계적인 로그 관리가 필요한 경우에는 loglevel이 적절하다는 것을 새로 알게 되었다.

 

돌고돌아 다시 본론으로 돌아오면

Javascript
console.log("material (onInit)", A); // 문제가 된 코드

이 코드는 성능 측정을 위해 필요하다. 하지만 일반 사용자들의 콘솔을 더럽히는 문제가 발생하지만, 그렇다고 무턱대고 완전히 제거할 수는 없는 상황인 것이다.

 

그러면 이런 상황에서는 debug 패턴이 적절할까? 아니면 log level 패턴이 적절할까?

 

나는 debug 패턴이 적합하다고 보았다.

성능 측정이 필요할 때만과 같이, 선택적 활성화가 가능하고 문맥 기반 그룹화가 가능하기 때문이다. 반면에 log level 패턴의 경우 레벨 기반 구분의 모호함이 있다고 보았다. 본질적인 문제는 “성능 측정”인데 이를 ERROR로 둘 지 아니면 INFO인지 구분하기 애매하고, 실제로는 “성능 측정”이라는 문맥이 중요하다고 보았기 때문이다.

 

정리하자면 debug 패턴은 ‘무엇을’ 로깅할지에 초점을 두고, log level 패턴은 ‘얼마나 중요한지’에 초점을 둔다고 보았다. 이 이슈에서는 "성능 측정"이라는 "무엇을"이 더 중요한 기준이므로, debug 패턴이 더 적합한 해결책이라고 판단했다.

 

🙃 debug 라이브러리

 

자세한 코드를 살펴보면

Javascript
function setup(env) { createDebug.debug = createDebug; createDebug.default = createDebug; // ... 다른 메서드들 설정 }

이 setup 함수를 중심으로 구성되며 디버깅 인스턴스를 생성하고 관리하는 모든 핵심 기능을 포함하는 것을 알 수 있다.

 

그리고 debug 라이브러리의 특징인 네임스페이스와 색상이 있다.

Javascript
function selectColor(namespace) { let hash = 0; for (let i = 0; i < namespace.length; i++) { hash = ((hash << 5) - hash) + namespace.charCodeAt(i); hash |= 0; // 32비트 정수로 변환 } return createDebug.colors[Math.abs(hash) % createDebug.colors.length]; }

각 네임스페이스에 고유한 색상을 할당하고 함수를 사용해 네임스페이스 문자열을 기반으로 색상을 결정한다.

 

Javascript
function createDebug(namespace) { let prevTime; function debug(...args) { if (!debug.enabled) return; const curr = Number(new Date()); const ms = curr - (prevTime || curr); // ... 로깅 로직 } // ... 속성 설정 return debug; }

각 네임스페이스별로 새로운 디버거 함수를 생성하고 시간 차이를 추적하여 로그 간의 시간 간격을 표시한다.

 

🙃 debug 라이브러리 기반으로, 나만의 debug 구현하기

무작정 debug 라이브러리를 설치해서 해당 이슈를 해결할 수는 없다. 왜냐하면 라이브러리 설치는, 곧 의존성 추가이기 때문에 신중할 필요가 있기 때문이다.

 

그래서 debug 패턴을 기반으로 debug 유틸을 구현하기로 했다.

 

Javascript
type DebugCategory = 'performance' | 'render' const debugState: { [key in DebugCategory]: boolean } = { performance: true, render: true } export const debug = { enable: (category: DebugCategory) => { debugState[category] = true }, disable: (category: DebugCategory) => { debugState[category] = false }, enableAll: () => { Object.keys(debugState).forEach(key => { debugState[key as DebugCategory] = true }) }, disableAll: () => { Object.keys(debugState).forEach(key => { debugState[key as DebugCategory] = false }) }, performance: (...args: any[]) => { if (debugState.performance) { console.log('[Performance]', ...args) } }, render: (...args: any[]) => { if (debugState.render) { console.log('[Render]', ...args) } } }

특정 카테고리만 활성화/비활성화하거나, 전체 활성화/비활성화, 카테고리별 활성화/비활성화를 하게 하였다.

 

초기값은 true이기 때문에 아래 이미지처럼 처음 브라우저에 접속하면 디버깅을 볼 수 있다.

 

Javascript
declare global { interface Window { debug: typeof debug } } if (typeof window !== 'undefined') { window.debug = debug }

debug 객체가 전역 스코프에서 접근 가능하게 해서

브라우저 콘솔에서 직접 입력해서 로그를 제어할 수 있게 했다.

 

물론 페이지 새로고침하면 설정이 초기화되기는 하지만, 간단하게 디버깅이 필요한 수준에서는 괜찮다고 보았다.