안녕하세요 Lannstark 입니다

자바 스크립트와 관련해 다루어 볼만한 여러 주제 중에, 제일 먼저 선택한 주제는 console 입니다.

자바스크립트를 사용해 봤다면 console.log, console.error 그리고 가끔 쓸모 있는 console.timer 등등은 무척 익숙합니다. 그런데 저는 문득 이런 궁금증들이 생겼습니다.

  1. console이란 무엇이고, console 객체는 어디에 정의되어 있을까?
    JS의 타입이나 문법 등은 ECMAScript 명세서에 정의가 되어 있잖아요. 그렇다면 console도 어딘가에 정의가 되어 있을 거란 말이죠. 어디일까요?

  2. console의 다른 기능들은 없을까?

  3. node에서 console.log를 하면 단색으로 로그가 나오게 된다.
    색을 변경할 수는 없을까? 변경할 수 있다면 어떻게 할 수 있을까?

  4. pm2는 어떻게 console.logconsole.error를 구분해서 file로 로그를 옮길 수 있을까?

  5. node로 만들어진 백엔드에서 파일에 로그를 남긴다고 생각해보면,
    1) console에 남기고 pm2가 잡아가 file에 기록하거나
    2) 로그 모듈들의 file appender를 이용할 수도 있는데 성능의 차이는 없을까?

본 글과 앞으로 작성될 글은 console과 관련된 위의 궁금증들을 해결하며 중간중간 작성되고 있습니다.

저 역시 공부를 하고 조사를 해가며 글을 작성하기 때문에, 틀린 내용이 있다면 언제든지 지적 부탁드리겠습니다!!

  • 사용한 OS : macOS Mojave 10.14.2
  • node version : v10.13.0
  • chrome version : 72.0.3626.109(공식 빌드) (64비트)

이번 글에서는 1번2번에 초점을 맞추어 보겠습니다.
아래 내용의 문체는 작성의 편리함을 위해 바뀐 것이니 이해 부탁드립니다.


console이란?

일반적인 console의 뜻은 무엇인지, terminal은 무엇이고, 둘의 차이점은 무엇인지는 다른 포스트 - 작성 예정에서 다룰 예정이다.

여기서는 JS에서 console이 무엇이고, 어디에 정의가 되어 있는건지 알아보자.

ECMAScript Specification

console이 무엇인지 정확히 알기 위해 ES 명세를 찾아 봤으나 놀랍게도 console과 관련된 내용을 찾지 못했다.

MDN

MDN에서는 JS console에 대해 이렇게 설명하고 있다.

The console object provides access to the browser’s debugging console. The specifics of how it works varies from browser to browser, but there is de facto set of features that are typically privided.

해석을 해보면, 콘솔 객체는 브라우저 디버깅 콘솔로의 접근을 제공한다. 동작 원리는 브라우저마다 다르지만, 사실상 전형적으로 제공되는 특징은 존재한다.

w3school

w3school 역시 MDN과 설명이 같다.

The console object provides access to the browser’s debugging console.

MDN과 설명이 완전 동일하다. 콘솔 객체는 브라우저 디버깅 콘솔로의 접근을 제공한다.

node.js

node에서는 이렇게 말하고 있다.

The console module provides a simple debugging console that is similar to the JavsScript console mechanism provided by web browsers.

console 모듈은 웹 브라우저에 의해 제공되는 JS console 메커니즘과 유사하며 간단한 디버깅 콘솔을 제공한다.

정리해보면…

console은 JS 엔진 별로 detail한 기능은 조금씩 다르지만 디버깅을 위해 존재하는 객체라고 할 수 있다. 사실 나는 console이 단순 출력을 담당한다고 알고 있었는데, 출력 기능은 디버깅을 위한 여러 기능 중 일부일 뿐이었다.

그리고 MDN 설명에 따르면, 콘솔 객체는 사실상 공통적으로 제공되는 특징이 존재한다고 했다. 그 특징이 명세된 사이트가 바로 여기이다. WHATWG는 Web Hypertext Application Technology Working Group의 약자로 HTML 및 관련 기술들을 발전시키는데 관심이 있는 사람들의 모임이라고 한다. (이름 완전 길다…)


console의 기능들

제일 공식에 가까운 명세가 무엇인지 알았으니 console의 메소드들을 하나씩 알아보자.

console 객체에는 크게 4가지 종류의 기능이 있다.

  1. Logging 기능
  2. Counting 기능
  3. Grouping 기능
  4. Timing 기능

Logging 기능은 우리에게 익숙한 console.log 친구들과 console.log 친척들로 나누어 살펴보도록 하겠다.

console.log 친구들

console.log와 99% 동일한 기능을 가진 메소드는 console.log를 포함하여 총 5가지이다. 5가지 함수 모두 무엇인가를 출력해준다.

  • console.error
  • console.warn
  • console.info
  • console.log
  • console.debug

chrome console에서 각각을 실행시켜보면 이렇게 생겼다.

stream 관점에서 무슨 차이인지는, node에서의 console에서 자세히 알아보기로 하고… console뒤에 붙는 메소드 이름이 곧 log의 severity라는 것을 확인할 수 있다.

명세에 따르면 5가지 메소드 설명도 99% 유사하다.

Perform Logger(“log”, data)

이런 식이다.

여기서 Logger란 추상 연산(Abstract operations)으로, console.log 친구들이 불리면 실행되는 진짜 내부 로직을 말한다. (비슷하게, JS type system에는 ToInteger 추상 연산이 있다. 타입 체계를 다룰 때 자세히 설명하도록 하겠다.)

여기서는 추상 연산 = 진짜 내부 로직 정도로 생각하고 Logger 추상 연산에 대해 알아보자

Logger 추상연산

Logger(logLevel, args)

logLevel이란 console.log이면 “log”, console.error이면 “error”를 말한다.

args란 console.log(“1234”)에서 문자열 1234를 말하고, console.log(“%s %d”, “1234”, 1234)에서 문자열 %s %d, 문자열 1234, 숫자 1234를 말한다. 즉, Logger가 불릴 때 넘어오는 모든 arguments를 말한다.

Logger 추상 연산 로직을 살펴보자.

  1. args의 길이가 0이면 return
  2. args[0]을 first라 정의하고, 나머지를 rest라 정의한다. rest의 길이가 0이라면 Printer 추상 연산 호출한다.
    Printer(logLevel, first)
  3. first에 formatter 기호가 없다면, Printer 추상연산을 호출한다.
    Printer(logLevel, args)
  4. 그렇지 않다면, Formatter 추상 연산과 Printer 추상연산을 호출한다.
    Printer(logLevel, Formatter(args))

간단하다.

Printer 추상 연산 로직과 Formatter 추상 연산 로직도 살펴보자.

Printer 추상 연산

Printer(logLevel, args[, options])

Printer 추상 연산은 구현하기에 따라 아주 달라질 수 있다. args로 넘어오는 것들은 아래와 같다.

  • 모든 타입의 JS 객체
  • 구현에 따라 출력할 수 있는 표현물. 예를 들어, stack trace나 group있다.
  • generic JS object formatting 또는 optimal useful formatting

generic JS object formatting과 optimal useful formatting에 대해 잠깐 설명하자면, 둘 모두 객체를 표현하는 방법에 대한 것이다. 예를 들어, 객체 obj가 있다고 하자.

const obj = {
  a: 2,
  b: 3,
  c: 4
}

Chrome에서 obj를 generic JS object formatting로 표현하면 이렇다.

Chrome에서 obj를 optimal userful formatting로 표현하면 이렇다.

Printer 추상 연산의 기능은 아주 간단하다. console에 args를 출력하면 된다. 만약 출력을 하려 했지만, console이 열려 있지 않다면 buffer에 쌓아두어야 하고 buffer의 용량은 100개의 출력물을 쌓을 정도라고 한다.

단순히 args를 출력하는 것이 아니라, 동시에 graphic 적인 요소를 추가할 수도 있다고 한다. chrome console에서 console.error를 하면 빨간 세모 표시가 나오는 것처럼 말이다.

여기까지 이해했다면 console.log와 관련해 몇 가지 헷갈렸던 점을 논리적으로 설명할 수 있게 된다.

case 1) console.log(1, 3)
Logger 연산에 넘어온 args는 [1, 3]이다. 이때 args[0]이 first이고, args[1]이 rest이다. rest의 길이가 0은 아니지만, first에 formatter 기호가 없으므로 Printer 연산으로 넘어가게 되고 Printer 연산은 넘어온 args를 모두 출력한다.

출력 : 1 3

case 2) console.log("%s %d", "Lannstark", 12, 34)
Logger 연산에 넘어온 args는 [“%s %d”, “Hello”, 12, 34]이다. 이때 args[0]이 first이고 args[1] ~ args[3]이 rest이다. rest의 길이가 0이 아니고, first에 formatter 기호가 있으므로 Formatter 연산으로 넘어가게 된다.

출력 : Lannstark 12 34

case 3) console.log(12, "%s", "Lannstark")
Logger 연산에 넘어온 args는 [12, “%s”, “Lannstark”]이다. 이때 args[0]이 fisrt이고 args[1], args[2]가 rest이다. rest의 길이가 0은 아니지만, fisrt에 formatter 기호가 없으므로 Printer 연산으로 넘어가게 되고 Printer 연산은 넘어온 args를 모두 출력한다.

출력 : 12 '%s' 'Lannstark'

Formatter 추상 연산

Formatter(args)

Formatter는 재귀적으로 돌아가는 추상 연산이다. 로직을 살펴보자.

  1. args중 첫 번째를 target, 두 번째를 second라고 한다.
  2. 첫 번째 문자열을 왼쪽부터 훑으며 specifier(%s %d등)를 찾는다.
    만약 %s를 찾았다면, String(second)를 대입한다.
    만약 %d나 %i를 찾았고, second의 type이 Symbol이 아니라면 parseInt(second, 10)을 대입한다.
    만약 %f를 찾았고, second의 type이 Symbol이 아니라면 parseFloat(second)를 대입한다.
    만약 %o를 찾았다면, second를 optimally useful formatting으로 바꾸어 대입한다.
    만약 %O를 찾았다면, second를 generic JS object formatting으로 바꾸어 대입한다.
    %c를 찾았다면, second를 CSS로 생각하여 대입한다.
    대입된 target과 나머지 args를 합쳐서 새로운 args 배열을 만들고, result라 부른다.
  3. 만약 대입된 target에 format specifier가 없다면 return result
  4. result의 크기가 1이라면, result에 남은 element가 대입된 target 하나라면 return result
  5. 그렇지 않다면 Formatter 추상 연산을 호출한다.
    Formatter(result)

추상 연산 로직에서 눈여겨 볼 점은 세 가지이다.

첫째

문자열이 대입되는 과정은 String()을 사용하고, 숫자가 대입되는 과정은 paresIntparseFloat를 사용한다. 역시나 JS 답게 console.log("%d %s", "1app3", 1.23)이 잘 돌아간다는 뜻이다.

하지만 명세일 뿐. 실제로 테스트 해보니 node와 chrome 모두 NaN 1.23이 출력되었다.

parseInt("1app3", 10)은 1이 나옴에도 불구하고 말이다.

또 한 가지 신기한 점은, node에서 console.log("%d", "1")은 1이 잘 출력되고, chrome에서 console.log("%d", "1")은 NaN이 나온다는 점이다. JS 엔진에 따라 형 변환 조건 및 구현이 다른가보다.

둘째

node에서는 %o와 %O 구분없이 모두 optimally useful formatting이 적용된다.

const obj = { a: 1, b: 2}
console.log("%o", obj) // { a: 1, b: 2 }
console.log("%O", obj) // { a: 1, b: 2 }

셋째

“%c”를 사용하면 CSS를 대입할 수 있다. chrome에서는 잘 작동 되었는데 node에는 작동되지 않았다.

크롬

node

console.log("%cHello", "color: red;")
// %cHello

그래서 찾아보니 node 에서 출력을 할 때 색을 입히고 싶다면 터미널의 색을 직접 조정해야 한다! 아니면, util 모듈을 이용할 수도 있다고 한다. 아마 util 모듈도 colors나 cli-color, chalk 모듈들처럼 터미널의 색을 직접 조정하는 기능의 래퍼가 아닐까 싶다.

여기서부터는 node 포스터로 빼서 작성하도록 하겠다.

마무리 및 관련 posts

오늘은 console 객체가 무엇인지, 명세는 어디에 작성되어 있는지, console.log가 JS 엔진 내부에서 어떤 로직으로 작동하는지 알아 보았습니다. 같은 V8 엔진을 기반으로한 chrome과 node의 구현 차이점도 살짝 다뤄봤고요.

이제

  1. console.log의 사촌들과, counting 기능, grouping 기능, timing 기능을 알아보고
  2. node에서의 console 객체와 출력할 때 색을 입히는 방법을 알아본 후
  3. pm2 관련 성능 비교까지만 하면

궁금했던 점들은 어느 정도 해결 될 것 같네요. ㅎ…

다음 포스팅은 1)번에 대한 이야기가 될 것 같습니다.

추가로 궁금한 점이나, 틀린 내용이 있으시면 언제든지 댓글 달아 주세요!

좋은 하루 되세요. 감사합니다. ^^

JS console 탐구 (2)

node의 console 객체, node 출력 글자 색 변경하기

JS 문자열 성능, pm2 로깅 성능 분석