Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

문자열은 Error가 아닙니다.

오랜만에 포스팅합니다. 블로그를 시작한 뒤로 이렇게 긴 공백을 가진 적이 없는데 바쁜 일이 좀 있어서 도저히 블로그에 포스팅할 여력이 없었습니다. 머리도 좀 식힐 겸 간만에 시간을 내서 포스팅을 합니다. 이 글은 LearnBoost의 CTO인 Guillermo Rauch가 올린 A String is not an Error을 허락받고 번역한 글입니다. Rauch는 Socket.IO를 만든 사람이기도 합니다.




별로 좋지 않음 패턴임에도 node.js 모듈에서 많이 사용하는 패턴을 이야기하려고 합니다. 이 패턴은 node.js보다는 적게 쓰이지만, 브라우저 자바스크립트에서도 쓰입니다. 다음 예제는 이 패턴을 보여줍니다.


// A:
function myFunction () {
  if (somethingWrong) {
    throw 'This is my error'
  }
    return allGood;
}


// B: "fn(err, …)"으로 정의된 Node.js 스타일 비동기 콜백
function myFunction (callback) {
  doSomethingAsync(function () {
    // …
    if (somethingWrong) {
      callback('This is my error')
    } else {
      callback(null, …);
    }
  });
}

위의 두 예제에서 오류 대신 문자열을 전달함으로써 모듈 간의 상호운용성이 줄어들었습니다. 문자열을 사용하면 instanceof Error로 검사하는 API 계약을 깨뜨리는 데다가 오류의 자세한 정보도 알기 어렵습니다. 뒤에서 설명하겠지만, 최신 자바스크립트 엔진의 Error 객체에는 유용한 속성을 많이 있으며 생성자에 전달된 메시지도 포함됩니다.



stack 속성

Error 객체를 사용할 때 가장 좋은 점은 어디서 만들어졌는지 자동으로 추적한다는 점입니다. 추적하는 방법에 대한 메카니즘은 자바스크립트 엔진과 브라우저 구현에 따라 다릅니다.


V8 (Node.js)
V8의 방법은 node.js로 개발하는 개발자에게 직접적으로 영향을 미칩니다. V8의 방법은 StackTraceApi 문서에 잘 설명되어 있습니다. 간단히 Error 객체를 초기화하고 객체의 stack 속성을 검사함으로써 동작방식을 확인할 수 있습니다.


// error.js
var err = new Error();
console.log(typeof err.stack);
console.log(err.stack);


∞ node error.js
string
Error
    at Object.<anonymous> (/private/tmp/error.js:2:11)
    at Module._compile (module.js:411:26)
    at Object..js (module.js:417:10)
    at Module.load (module.js:343:31)
    at Function._load (module.js:302:12)
    at Array.0 (module.js:430:10)
    at EventEmitter._tickCallback (node.js:126:26)


이 예제처럼 throwing하거나 다른 함수로 전달하지 않았지만, V8이 객체가 어디서 생성되고 어떻게 전달되었는지 정확하게 알려줍니다. 기본적으로 V8은 스택 트레이스의 크기를 10프레임으로 제한하는데 실행 중 Error.stackTraceLimit로 제한값을 바꿀 수 있습니다.


Error.stackTraceLimit = 0; // disables it
Error.stackTraceLimit = Infinity; // disables any limit


Custom Error
스택 컬렉션을 차단하기 위해 네이티브 Error 객체를 확장하려면 captureStackTrace 함수를 호출한다. 다음 예제는 Mongoose ODM에서 가져온 예제입니다.


function MongooseError (msg) {
  Error.call(this);
  Error.captureStackTrace(this, arguments.callee);
  this.message = msg;
  this.name = 'MongooseError';
};

MongooseError.prototype.__proto__ = Error.prototype;

MongooseError 생성자 호출을 스택에서 감추어서 불필요한 정보를 없애기 위해 captureStackTrace에 두 번째 아규먼트를 전달했다.


stack 문자열을 넘어서...
앞의 예제에서 의도적으로 stack 속성의 typeof를 출력했습니다. 출력된 결과처럼 읽기 쉽게 포매팅 된 일반적인 문자열입니다. 자바처럼 V8도 스택에서 완전한 런타임 내부접근(runtime introspection)을 허용합니다. 문자열 대신에 CallSite 배열에 접근할 수 있고 CallSite는 객체의 범위(this)를 포함해서 함수호출에 대한 가능한 한 많은 정보를 보관하고 있습니다. 다음 예제에서 CallSite배열에 접근하고 확인해보기 위해 prepareStackTrace함수를 오버라이드했습니다. 각 CallSite에서 getFileName 등의 함수를 호출했습니다.(이용 가능한 메서드는 V8 문서에서 확인할 수 있습니다.)


function a () {
  b();
}

function b () {
  var err = new Error;

  Error.prepareStackTrace = function (err, stack) {
    return stack;
  };

  Error.captureStackTrace(err, b);

  err.stack.forEach(function (frame) {
    console.error(' call: %s:%d - %s'
      , frame.getFileName()
      , frame.getLineNumber()
      , frame.getFunctionName());
  });
}

a();

물론 이 모든 기능은 문자열을 전달할 때는 사용할 수 없으므로 디버깅 작업도 급격히 줄어듭니다. 실제 사용할 때는 오버라이드한 후에 원래의 prepareStackTrace로 복구하는 것이 좋습니다. 다행히 TJ Holowaychuck이 이 작업을 쉽게 해주는 간단한 callsite모듈을 공개했습니다./


브라우저
V8 문서에 다음과 같이 나와 있습니다.

여기서 설명한 API는 V8에 한정되고 다른 자바스크립트 구현에서는 지원되지 않습니다. 대부분의 구현은 error.stack 속성을 제공하지만, 스택 트레이스의 형식은 여기서 설명한 형식과 다릅니다.

  • 파이어폭스는 error.stack를 자신만의 형식으로 노출합니다.
  • 오페라는 error.stacktrace를 자신만의 형식으로 노출합니다.
  • IE는 stack이 없습니다.
error.stack이 브라우저마다 다른 문제는 javascript-stacktrace가 해결해 줍니다. javascript-stacktrace는 모든 브라우저에서 출력된 스택 트레이스를 같은 형식으로 만들어 줍니다. 예를 들어 IE에서는(그리고 이전 버전의 사파리) 함수의 caller 속성을 재귀적으로 찾으면서 toString을 출력하고 함수의 이름을 파싱합니다.


var currentFunction = arguments.callee.caller;
while (currentFunction) {
  var fn = currentFunction.toString();
  var fname = fn.substring(fn.indexOf(&amp;quot;function&amp;quot;) + 8, fn.indexOf('')) || 'anonymous';
  callstack.push(fname);
  currentFunction = currentFunction.caller;
}



결론
  • 비동기 I/O(node.js나 다른 곳에서)를 사용하면 throwing을 사용할 수 없으므로 Errors는 적절한 스택 트레이스 컬렉션을 위한 유일한 방법입니다.
  • V8 브라우저 환경에서도 Errors를 초기화하는 것이 좋습니다. API는 모든 브라우저에서 존재하고 V8이 제공하는 확장된 API는 미래에는 대부분 엔진에서 이용 가능할 것입니다.
  • 오류를 위해서 문자열을 던지거나 전달했다면 이제는 바꿔 보세요.
이 글에서 초반에 제시했던 예제는 다음과 같이 재작성 할 수 있습니다.


// A:
function myFunction () {
  if (somethingWrong) {
    throw new Error('This is my error')
  }
  return allGood;
}


// B: "fn(err, …)"으로 정의된 Node.js 스타일 비동기 콜백
function myFunction (callback) {
  doSomethingAsync(function () {
    // …
    if (somethingWrong) {
      callback(new Error('This is my error'))
    } else {
      callback(null, …);
    }
  });
}




덧) 원 글의 댓글을 보면 Error의 크로스 브라우징 이슈를 해결하기 위한 javascript-stacktrace라이브러리의 방법은 function name() { }같은 정의에서는 가능하지만 var name = function() {}에서는 안 된다는 이야기가 있습니다. 저는 뭐 노드 때문에 관심 가지는 거라서....
2011/12/30 18:10 2011/12/30 18:10