에러 전파

devAnderson·2022년 1월 17일
0

TIL

목록 보기
27/103

1. 사전지식 ( AST와 실행 컨텍스트)

  • 자바스크립트 코드는 우선 V8과 같은 자바스크립트 엔진의 안에 있는 어휘분석기(scanner)을 통해 토크나이징이 이루어지고, 분류된 토큰들에 문제가 없다면 구문분석기(parser)에 전달되어 AST를 형성한다
  • AST는 기계가 이해할 수 있는 바이트 코드로 전환되고(v8의 경우 Ignition 이라는 interpreter에 의해)
  • 바이트 코드는 v8 기준, turbofan이라고 하는 optimizer에 의해서 machine code로 전환된다.

    machine code는 CPU에 의해 직접적으로 연산이 될 수 있는 기계수준의 언어를 뜻한다.

  • 이 기계어가 javascript 엔진이 빌려다쓰는 CPU의 기능으로 연산이 되고 있을 때 생성이 되어 바이트코드로서 실행되기 전에 만들어지는 것이 Execution context(실행컨텍스트) 이다.
  • Execution context가 만들어질 때에는, 우선 Memory allocation phase라고 하는 변수환경 설정과, this 정의, scope 정의가 이루어지는 단계를 거친다.
  • 그 후 execution phase에 할당 및 실행을 위에서부터 순차적으로 진행하기 시작한다. 만약 함수실행이 존재한다면, 이때 함수를 위한 지역 컨텍스트가 만들어지면서 이 컨텍스트가 콜스텍으로 전달된다.
  • 만약 해당 실행 함수가 webAPI 가 이미 등록하고 있는 "비동기" 적 작업이라면, 이것을 따로 등록한 후 이벤트 루프에 따라 콜스택이 비워질 때마다 들어가서 실행되게 된다.

2. 콜백 패턴

  • 일반적으로 비동기 함수는 콜스텍이 비워져 있을 때만 실행되므로, 동기적 함수가 실행할 때에는 비동기 함수가 리턴하는 결과물을 받아볼 수가 없다

  • 예를들어, 비동기 작업을 포함하는 생성자 XMLHttpRequest을 예로 들어본다면

function getData (url) {
   let result;

   const xhr = new XMLHttpRequest();
   xhr.open('GET', url);
   xhr.send();

   xhr.onload = () => {
      if(xhr.status === 200) {
           result = JSON.parse(xhr.response);
      }
   }
  
   return result;
}

console.log(getData("https://test.com");
  • 전역 컨텍스트에서 console의 메소드인 log를 찾고, 그 안에 있는 getData의 실행 구문을 진행했을 때
  • getData 함수에 실행 컨텍스트가 생기면서 memory allocation phase 단계에서 변수정의(url, result), this바인딩 (undefined, strict mode 기준), scope 정의가 이루어진다.
  • 그 후 execution phase에서 url 할당 ("https://test.com"), result 할당(undefined), xhr.open 실행, xhr.send 실행, xhr.onload 실행, return문 실행 순서로 이루어지는데
  • 문제는 xhr.onload 메소드가 비동기 작업이다
  • 이 비동기 작업은 해당 실행 컨택스트에서 빠져나와 webAPI의 task que로 등록되고 함수의 실행 컨텍스트는 return문을 실행 후 종료되므로
  • result에 있던 undefined를 리턴하며 종료된다.
  • 따라서, 해당 비동기 값을 가지고 무언가를 처리해야 한다면 그 후속처리는 비동기 함수의 내부에서만 수행해야 한다.
  • 이를 위해서 쓰는 방법이 주로 콜백 방식이었다
function getData (url, callback) {
   const xhr = new XMLHttpRequest();
   xhr.open('GET', url);
   xhr.send();

   xhr.onload = () => {
      if(xhr.status === 200) {
           callback(JSON.parse(xhr.response));
      }
   }
}

getData("https://test.com", console.log);
  • 콜백방식의 문제는, 만약에 이러한 비동기적인 처리가 연쇄적으로 발생해야 할 경우, 예를 들어 특정 작업이 끝난 후 또다른 비동기 작업을 해야 한다면 또다시 그 작업 내부에 콜백을 보내야 하므로 이른바 "콜백 헬" 이라는 상황이 발생하게 된다

  • 또한, 콜백 패턴의 가장 심각한 문제는 에러처리가 어렵다는 것이다.

  • 예를들어, 주로 에러를 잡기 위해 사용하는 패턴인 try ... catch 구문을 예시로 들자면

try{
  setTimeOut(() => { throw new Error("error!"); }, 1000);
}
catch(err){
   console.log(err);
}
  • error 전파는 콜스텍의 기준으로 에러를 발생시킨 컨텍스트로부터 하위로 전달된다.

  • 하지만 setTimeOut은 이미 webAPI에 콜백을 전달하고 스텍에서 없어진 상태다.

  • 따라서, 콜백함수가 실행컨텍스트를 만들고, 그것이 콜스텍에 넣어져서 실행될 때에는 이미 팝되어 사라진 상태고,

  • 콜백함수가 throw를 통해 에러객체를 전달해도 setTimeOut은 그것을 받지 않는다.

  • catch는 이미 사라진 setTimeOut에 대해서 에러를 잡지 않았으므로 결과적으로 catch가 적용되지 않는것과 같다.

  • 이처럼 콜백 패턴은 에러에 대한 핸들링이 어렵게 만드므로 지양해야 한다

3. 프로미스의 등장

  • ES6부터 표준화되어 도입된 생성자 함수 Promise로 객체를 생성하면 비동기적인 작업을 손쉽게 처리하도록 도와준다

  • 이 프로미스 생성자는 콜백함수를 인자로 받는데, 이때 인자에는 자동적으로 resolve, reject 함수를 차례로 전달받는다.

  • 이때 인자로 전달받게 되는 두 함수는 사실 Promise 생성자 함수에 static으로 정의되어 있는 함수이다.

  • 두 함수의 처리에 따라서, 해당 프로미스 객체의 슬롯, PromiseState과 PromiseResult가 달라지며

  • 해당 PromiseResult를 프로토타입 슬롯에 존재하는 메소드 "then"에 인자로 들어오는 콜백함수의 인자로 전달받는다.

  • 만약 에러를 발생하여 reject가 실행된다면, catch에 첫번째 인자인 콜백함수의 인자로 해당 PromiseResult 슬롯의 값이 전달된다.

  • 이렇게 then을 통한 처리를 이용하여 콜백지옥이 아닌 then 체이닝을 통한 비동기 처리들을 단계별로 정리가 가능하다

  • 만약 resolve된 결과값을 얻고 싶다면 async ... await을 이용한 방식으로 비동기 작업이 마치기를 기다리게 만든 후 로직을 처리할 수도 있다.

    참고로 async가 붙게 되는 함수는 자동적으로 리턴하는 결과값을 Promise 객체화 시켜서 리턴하므로, 사용시 주의를 요한다

reference

how to V8 work
what is the execution context

profile
자라나라 프론트엔드 개발새싹!

0개의 댓글