안녕하세요 Lannstark 입니다.

console에 대해 공부를 하다 알게된 기능 중 console.trace라는 기능이 있었습니다.

console.trace는 해당 코드가 실행되는 순간의 stack trace를 출력해주었죠. 하지만 때로는 단순 stack trace 출력을 원하는 것이 아니라, stack trace를 가져와 이리저리 가공을 해야할 때가 있습니다.

이번 포스팅에서는 stack trace가 무엇인지, node에서는 stack trace를 어떻게 가져올 수 있는지 알아보도록 하겠습니다.

stack trace란?

위키 피디아에서는 stack trace를 이렇게 정의하고 있습니다.

A report of the active stack frames at a certain point in time during the execution of a program

해석 해보죠.
stack trace는 프로그램이 실행 중인 특정 시점에서 활성화되어 있는 stack frame의 보고서이다.

여기서 stack frame이란 무엇일까요?

컴퓨터 공학이나 전산을 전공으로 하셨다면, 메모리의 구조에 대해 배우셨을 겁니다. 프로세스가 OS로부터 메모리의 특정 영역을 할당 받으면, 그 메모리는 다시 4가지 세부 영역으로 구분되죠. 코드(텍스트) 영역, 데이터 영역, 힙 영역, 스택 영역입니다.

여기서 말하는 스택 영역은 함수가 호출 될 때마다 커지고, 그 안에 함수의 지역 변수나 지금 함수가 끝나면 돌아갈 주소, 이전 함수가 필요로 하는 파라미터 등이 있죠.

네 바로 이것입니다. stack frame이란 현재 쌓여 있는 스택 영역을 말하는 것입니다.

UWM Edu에서는 이렇게 정의하고 있네요.

The stack frame, also known as activation record is the collection of all data on the stack associated with one subprogram call.

stack frame은 subprogram call과 연관된 스택의 모든 데이터의 집합이다.

subprogarm은 쉽게 함수라고 생각하시면 됩니다.


예를 들어 보죠.

function fun1() {
  console.log("fun1")
  fun2()
}

function fun2() {
  console.log("fun2")
  fun3()
}

function fun3() {
  console.log("fun3")
  console.trace()
}

fun1()

위의 코드는 제일 처음 fun1()를 부르고, fun1()fun2()를, fun2()fun3()를 부릅니다. 그렇다면, 스택상에는 제일 아래 fun1에 대한 데이터가 존재하고 그 다음 fun2, 마지막으로 fun3가 있겠죠.

실제 출력 결과는 이렇습니다.

fun1
fun2
fun3
Trace
    at fun3 (/Users/lannstark/workplace/Test/console.js:13:11)
    at fun2 (/Users/lannstark/workplace/Test/console.js:8:3)
    at fun1 (/Users/lannstark/workplace/Test/console.js:3:3)
    at Object.<anonymous> (/Users/lannstark/workplace/Test/console.js:16:1)
    at Module._compile (internal/modules/cjs/loader.js:688:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10)
    at Module.load (internal/modules/cjs/loader.js:598:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:537:12)
    at Function.Module._load (internal/modules/cjs/loader.js:529:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:741:12)

fun1, fun2, fun3는 각각의 함수가 호출된 순서대로 출력이 되었고요, 그리고 fun3에서의 stack trace를 출력하면 역으로 그 순서가 나오게 됩니다. fun3, fun2, fun1, 그리고 이 node 파일을 실행시키기 위한 (아마도) 노드 엔진 코드의 실행순서까지요

이렇게 정리할 수 있겠군요

stack trace란 특정 시점에서, 그 시점까지 오는데 거쳐온 함수들의 순서 있는 집합이다.

사실 여러분은 이 stack trace를 많이 보셨습니다.
에러가 나게되면 항상 이 stack trace를 보여주기 때문이지요.

실제로 stack trace를 읽는 능력은 디버깅 측면에서 중요한 역량 중 하나입니다. 그에 관한 좋은 REF를 소개합니다.

stack trace 가져오기

이제 node에서 stack trace를 가져와 봅시다.

node의 Error 클래스에 내부 필드 중 stack이라는 값이 있습니다. 이 stack값에는 String형식으로 stack trace가 들어 있죠.

이 stack 값을 이용하면 stack trace를 가져올 수 있습니다.

function fun1() {
  fun2()
}

function fun2() {
  fun3()
}

function fun3() {
  fun4()
}

function fun4() {
  console.log((new Error()).stack)
}


fun1()

이 코드는 아래의 결과를 보여줍니다.

Error
    at fun4 (/Users/lannstark/workplace/Test/error.js:16:16)
    at fun3 (/Users/lannstark/workplace/Test/error.js:12:3)
    at fun2 (/Users/lannstark/workplace/Test/error.js:8:3)
    at fun1 (/Users/lannstark/workplace/Test/error.js:4:3)
    at Object.<anonymous> (/Users/lannstark/workplace/Test/error.js:20:1)
    at Module._compile (internal/modules/cjs/loader.js:688:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10)
    at Module.load (internal/modules/cjs/loader.js:598:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:537:12)
    at Function.Module._load (internal/modules/cjs/loader.js:529:3)

여기서 두 가지 흥미로운 점이 있습니다.

  1. 최대 10개의 stack trace만 볼 수 있다.
  2. 이 String을 파싱하는 것은 쉬운 일이 아니다. 다른 방법이 없을까?

stack trace 개수 변경

이 10개라는 특정 값은 default 값으로 변경할 수 있습니다.

바로 Error.stackTraceLimit이라는 필드의 값을 변경해서요.

Error.stackTraceLimit = 20

function fun1() {
  fun2()
}

function fun2() {
  fun3()
}

function fun3() {
  fun4()
}

function fun4() {
  console.log((new Error()).stack)
}


fun1()

이 코드를 실행하면 전보다 긴 stack trace를 얻을 수 있습니다.

참고로, Error.stackTraceLimit에 음수나 숫자가 아닌 값을 넣으면, stack trace가 나오지 않습니다.

긴 stack trace에서 특정 정보 추출하기

(new Error()).stack을 활용하면 stack trace를 String으로 가져올 수 있습니다. 하지만, 저렇게 길게 생긴 String 타입의 stack trace을 가공하는 것은 쉬운 일이 아닙니다.

다행스럽게도 V8 엔진은 stack trace를 쉽게 다룰 수 있는 방법을 제공합니다.

바로, Error.prepareStackTrace의 함수를 바꾸는 것이지요. Error.prepareStackTracestack trace를 최종적으로 반환하기 전에 호출되는 함수로, 원래 값은 undefined입니다.

하지만 Error.prepareStackTrace(err, stadck) => { return stack }을 넣어주게 되면, CallSite 객체의 배열이 나옵니다. CallSite 객체는 원래 stack trace 한줄 한줄에 대응되는 객체로, getFileName, getLineNumber등 해당 줄에 있는 정보를 가져오는 메소드들이 존재합니다.

const arrayPrepareStackTrace = (err, stack) => { return stack }

function fun1() {
  fun2()
}

function fun2() {
  fun3()
}

function fun3() {
  fun4()
}

function fun4() {
  const priorPrepareStackTrace = Error.prepareStackTrace
  Error.prepareStackTrace = arrayPrepareStackTrace
  const stack = (new Error()).stack
  Error.prepareStackTrace = priorPrepareStackTrace 
  console.log(stack)
  // [1] //
}

fun1()

따라서 위와 같은 예제를 실행시킨다면, 아래와 같은 결과가 나오게 됩니다.

[ CallSite {},
  CallSite {},
  CallSite {},
  CallSite {},
  CallSite {},
  CallSite {},
  CallSite {},
  CallSite {},
  CallSite {},
  CallSite {} ]

첫 번째 CallSite는 fun4에 관한 것일거고, 두 번째 CallSite는 fun3 … 이런 식이겠죠

위에서 priorPrepareStackTrace를 만들어줬다 다시 대입 해준 이유는, 이 프로그램 다른 코드에서 Error.prepareStackTrace를 다르게 이용하고 있을 수 있게 때문입니다.

CallSite에서 정보를 가져와보죠

// [1] //에 다음과 같은 코드를 추가하겠습니다.

console.log(stack[0].getFunctionName())
console.log(stack[0].getFileName())
console.log(stack[0].getLineNumber())

console.log(stack[1].getFunctionName())
console.log(stack[1].getFileName())
console.log(stack[1].getLineNumber())

역시 이런 결과가 나옵니다.

fun4
/Users/lannstark/workplace/Test/error.js
18

fun3
/Users/lannstark/workplace/Test/error.js
12

이 외의 다양한 API가 있으니 자세한 내용은 여기를 확인해주세요.

마무리

본 포스트에서는 stack trace가 무엇인지, node에서 어떻게 stack trace를 가져오고 정보를 추출할 수 있는지 알아보았습니다.

긴 글 읽어주셔서 감사합니다!

관련 포스트

JS console 탐구 (2)