(Feat : console 탐구 세 번째 이야기)

안녕하세요 Lannstark 입니다.

JS의 console 탐구 세 번째 포스트네요.

첫 번째와 두 번째 포스트에서는 JS console에 대해서 알아보았죠.

이번 포스트에서는 node의 console 객체에 대해 알아볼까 합니다.
본문을 읽으시다가 의아하거나 더 궁금한 점이 생기신다면 공식 Docu를 확인해 주세요.

그럼 시작합니다!

도큐와 코드를 읽은 노드 버전은 v11.11.0입니다.

Console

node의 console 모듈은 웹 브라우저의 JS console 메커니즘과 비슷한 간단한 debugging console을 제공하며, 두 개의 component를 export한다.

  • Console 클래스
  • global console 인스턴스

Console 클래스부터 살펴보자.

Console Class

Console 클래스는 output stream을 설정할 수 있는 간단한 로거를 만드는데 사용되며, 다음 두 방법 중 하나로 가져올 수 있다.

const { Console } = require("console")
const { Console } = console

새로운 Console 인스턴스는 new 연산자를 사용해 만들 수 있는데, 이때 총 5가지 옵션이 있다.

  • stdout : log나 info를 출력할 쓰기 가능한 stream
  • stderr : warning이나 error를 출력할 쓰기 가능한 stream
  • ignoreErrors
  • colorMode
  • inspectOptions

ignoreErrors 옵션은 underlying stream으로 출력할 때 에러를 무시할지 무시하지 않을지 결정하는 옵션으로, Default 값은 true 이다.

const { Console } = require("console")

const options = {
  stdout: process.stdout,
  stderr: process.stderr,
  ignoreErrors: false
}
const logger = new Console(options)

logger.error(new Error("new Error!"))
console.error(new Error("new Error!"))
Error: new Error!
  ... (stack trace)
Error: new Error!
  ... (stack trace)

같은 출력이 나온다.. .log로도 해봤는데 같은 출력이 나왔다.

underlying stream이 무엇인지 찾아도 나오지 않고, ignoreErrors 옵션을 false로 바꾸어도 별다른 차이가 없어서 정확히 어떤 역할을 하는지 잘 모르겠다. (혹시 아시는 분은 알려주시면 감사드리겠습니다…)

node 구현상은 이런 차이가 있다. REF

  if (ignoreErrors === false) return stream.write(string);

  // There may be an error occurring synchronously (e.g. for files or TTYs
  // on POSIX systems) or asynchronously (e.g. pipes on POSIX systems), so
  // handle both situations.
  try {
    // Add and later remove a noop error handler to catch synchronous errors.
    stream.once('error', noop);

    stream.write(string, errorHandler);
  } catch (e) {
    // Console is a debugging utility, so it swallowing errors is not desirable
    // even in edge cases such as low stack space.
    if (isStackOverflowError(e))
      throw e;
    // Sorry, there's no proper way to pass along the error here.
  } finally {
    stream.removeListener('error', noop);
  }

colorMode 옵션은 Console 인스턴스에 대한 color support를 설정한다. 값을 true로 준다면 값들을 검사하는 동시에 coloring 할 수 있게 된다. 값을 auto로 설정한다면, isTTY 값이나 반응하는 스트림의getColorDepth()값에 따라 color support가 결정된다. 이 옵션은 inspectOptions.colors가 설정되어 있으면 사용할 수 없다. Default 값은 auto이다.

colorMode를 false로 설정하면, 색이 나오지 않는다. 여기서 말하는 색은 자동으로 나오는 색을 말한다.

(숫자는 노란색, 심볼은 초록색 등등..)

inspectOptions는 해당 Console 인스턴스에스 util.inspect 메소드를 호출 할 때 사용되는 옵션이다. node 콘솔 객체에서는 util.inspect가 딱 두 번 사용된다. console.dirconsole.table

때문에 일반적으로 inspectOptions은 굳이 설정할 필요가 없다고 생각한다.


이렇게 놓고보면, 사실 global consoleConsole 인스턴스의 특수한 경우에 불과하다.

const console = new Console({
  stdout: process.stdout,
  stderr: process.stderr
})

여기서 드는 궁금증.. process.stdout을 변경하면 console 출력 위치도 바뀔 것인가? (= console 인스턴스가 가지고 있는 stdout은 레퍼런스인가 값인가?)

const fs = require("fs")
const output = fs.createWriteStream("./log")

process.stdout = output

console.log("PROCESS stdout changed")

위의 예제는 터미널(process.stdout 기본값)에 아주 잘 출력되었다.

console 인스턴스가 가지고 있는 출력 스트림은 레퍼런스가 아니라 값인걸로 판명…

메소드

명세에서 명시된 메소드들은 모두 존재한다. console.log, console.info, console.dir, console.warn, console.warn부터 console.assert, console.clear, console.count, console.countReset, console.dir, console.dirxml, console.group, console.groupEnd, console.table, console.time, console.timeEnd, console.timeLog, console.trace 까지… 와 쓰느라 힘들었다. 동작 역시 명세에 나와 있는 것과 거의 유사하다.

다른 점은 포맷팅을 할 때 util.format() 함수가 사용되는데, 명세에 나와 있는 specifier 외에 다른 specifier들이 있다는 점과, console.errorconsole.dir에서 util.inspect() 함수를 사용한다는 점이다.

추가로, Console 객체에는 Inspector only methods라고 해서, --inspector 옵션을 주었을 때만 결과가 출력되는 method들이 있다.

inspector란 일종의 디버깅 툴이다. 궁금하다면 여기를 참고하는 것을 추천한다.

console.profile(label)

console.profile() 메소드는 JS CPU profile을 주어진 label과 함께 시작한다.

console.profileEnd(label)

console.profileEnd() 메소드는 JS CPU profiling 세션을 끝내고, 결과를 inspector의 Profile 패널에 내보낸다.

사용해보니 이런식으로 결과가 나오게 된다.

console.timeStamp(label)

console.timestamp() 메소드는 inspector의 Timeline 패널에 이벤트를 추가한다.

포맷팅

명세에 적힌 Formatter 추상 연산을 node에서는 util.format()이 담당하고 있다. util.format() 함수의 내부 구현은 여기에서 확인할 수 있다.

util.format이 지원하는 placeholder(specifier와 같은 뜻으로 사용된 용어)는 총 8가지이다.

  • %s : String
  • %d : Number or BigInt
  • %i : Integer or BigInt
  • %f : Floating point value
  • %j : JSON. 만약에 환형 참조 객체라면 [Circular]으로 표시된다.
  • %o : Object, generic JavaScript object formatting. util.inspect() 함수를 { showHidden: true, showProxy: true }옵션으로 실행시킨 결과와 유사하다. non-enumerable 속성까지 모두 보여준다.
  • %O : Object, generic JavaScript object formatting. util.inspect() 함수를 아무 옵션 없이 실행시킨 결과와 유사하다. non-enumerable 속성까지 보여주지는 않는다.
  • %% : %을 표시하기 위한 기호

util.inspect()

위에서 계속 언급되는 메소드 util.inspect()이다. 도대체 어떤 역할을 하는 메소드일까?
util.inspect() 메소드는 디버깅을 위하여 object를 문자열 표현으로 바꾸어주는 역할을 한다. 이게 무슨말인가 하면, 아래의 코드를 보자.

const obj = {
  a: {
    b: {
      c: {
        d: 2
      }
    }
  }
}

console.log(obj)

위 코드의 실행 결과는 아래와 같다. (더 엄밀하게는 [Object] 라는 글자가 청록색으로 보일 것이다.)

{ a: { b: { c: [Object] } } }

console.log로 객체 obj를 출력할 수 있게 해주지만, 끝까지 보여주지는 않는 것이다. 그럼 끝까지 보려면 어떻게 해야할까?

이럴 때 사용할 수 있는 함수가 바로 util.inspector이다.

const inspectOptions = {
  showHidden: true,
  depth: 4,
  colors: false
}

console.log(util.inspect(obj, inspectOptions))

이번엔, 2가 노란색으로 보이지 않고 객체가 끝까지 출력될 것이다.

{ a: { b: { c: { d: 2 } } } }

왜냐하면 util.inspectobj를 출력할 수 있게 문자열로 바꾸며, hidden property를 모두 보여주라고 했고, 객체의 depth가 4일 때까지는 계속 확인하라고 했고, 색칠하는 것을 하지 마라고 했기 때문이다.

util.inspect 옵션에는 다음과 같은 항목들이 있다.

  • showHidden : Default값은 false이다. true로 설정되면 object의 non-enumerable 속성들까지 모두 출력하도록 해준다. Object 속성의 enumerable과 관련되서는 여기에 설명되어 있다 - 추가 예정
  • depth : object를 포맷팅 할 때 최대로 들어가는 depth를 설정한다. Default 값은 2이다.
  • colors : Default 값은 false이다. true로 설정되면 ANSI color code를 적용한다. 이 colors는 default 값이 있는데, util.inspect.colors로 default 설정을 바꿀 수 있다.
  • customInspect : Default 값은 true이다. false로 설정되면 util.inspect.custom(depth, opts) 함수를 호출하지 않는다.
  • showProxy : Default값은 false이다. true로 설정되면 Proxy inspection이 target과 handler를 포함한다.
  • maxArrayLength : 최대로 표시할 배열의 원소 개수 default 값은 100이다.
  • breakLength : 객체를 출력할 때 한 줄에 몇 단어까지 출력할지 결정한다. default값은 60이며 Infinity로 설정할 경우 한줄로 출력된다.
  • compact : false로 설정할 경우, object key 각각은 새로운 줄에 보여지게 된다. 숫자 n으로 설정할 경우 n개의 object key-value가 한 줄에 출력되는데 이때 breakLength에 영향을 받는다. Default는 true이다.
  • sorted : true로 설정된다면 default sort가 사용된다. 함수를 설정한다면 그 함수가 정렬에 사용되게 된다.
  • getters : true로 설정되면, getters가 inspect된다. get으로 설정되면 setter와 대응되지 않는 getter가 inspect된다. set으로 설정되면 setter에 대응되는 getter가 inspect된다. Default 값은 false이다. (자주 사용되는 옵션은 아닌듯 하다)

자주 사용할만한 옵션으로는 showHidden, depth, maxArrayLength, breakLength, compact가 있을 듯 하다.

출력 색 변경

그렇다면 슬슬 궁금해진다.

노드는 어떻게 숫자와 불리언을 노란색으로 출력하고, null을 진하게 출력하는 것일까?

여기 ANSI라는 개념이 있다. ANSI란, 텍스트 모드의 환경에서 콘솔 터미널의 제어 코드를 화용해 다양한 색상의 문자를 표시하는 방법이다.

그리고, ANSI escape code라는 것이 있다. ANSI escape code는 터미널 cursor의 위치, 색깔, 그리고 기타 다른 옵션들을 조정할 수 있다.

ANSI escape code를 활용하면 출력할 때 터미널의 색을 조절할 수 있다. 이런 방법이다. console.log(문자열)을 색깔을 바꿔 출력하고 싶다면 console.log(터미널 글자색을 바꾸는 ANSI escape code + 문자열 + 터미널 글자색을 원래대로 되돌리는 ANSI escape code)를 출력하는 것이다.

위키피디아에 따르면, ESC[숫자m이 터미널의 글자색 혹은 글자 모양(기울임, 진하게 등) 변경하는 ANSI escape code이다. 숫자에 따라 색이나 글자 모양이 바뀐는데 그 예시를 몇 가지만 들자면 아래와 같다.

0 : 원래대로 되돌림
1 : 진하게
3 : 기울게
30 : 검정색
31 : 빨간색
32 : 초록색
33 : 노란색
34 : 파란색
35 : 심홍색
36 : 청록색
37 : 흰색
38 : 색을 RGB로 직접 지정할 수 있다. ESC[38;2;R;G;Bm

ESC 입력은 Unicode escpae sequence혹은 Hexadecimal escape sequence를 이용해야 한다.

ESC는 유니코드에서 27번이므로 \u001b\x1b 입력하면 된다.

console.log("\u001b[34mHello \u001b[0mWorld")

위 코드의 출력 결과는 아래 그림과 같다.

실제 색을 칠해주는 유명 모듈인 colors 역시 같은 방법으로 색을 입히고 있다.

Object.keys(codes).forEach(function(key) {
  var val = codes[key];
  var style = styles[key] = [];
  style.open = '\u001b[' + val[0] + 'm';
  style.close = '\u001b[' + val[1] + 'm';
});

코드 REF

마무리 및 관련 포스트

본 포스트에서는 node에서 console 객체가 무엇인지, 어떤 메소드들이 있는지 JS console과는 어떻게 다른지 알아보았습니다.

또한, 그 과정에서 나온 util.inspect의 다양한 활용법에 대해 살펴보았습니다.

터미널에 결과를 출력할 때 나오는 글자의 색을 바꾸는 방법 역시 알아 보았습니다.

이 글이 누군가에게 도움이 되길 바라며, 긴 글 읽어주셔서 감사합니다 ㅎㅎ

JS console 탐구 (1)
JS console 탐구 (2)