콜백과 프라미스

Y·2020년 9월 12일
0

자바스크립트

목록 보기
18/20

콜백


자바스크립트 호스트 환경(자바스크립트가 구동되는 환경)이 제공하는 여러 함수를 사용하면, 비동기 동작을 스케쥴링 할 수 있다. 원하는 때에 동작시킬 수 있다는 말이다. 이중 setTimeout이 대표적이다.

src에 있는 스크립트를 읽어오는 함수 loadScript(src) 역시 비동기 처리다.

function loadScript(src) {
  // <script> 태그생성, 페이지에 태그를 추가
  // 태그가 페이지에 추가되면 src에 있는 스크립트를 로딩하고 실행
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}

loadScript(path)로 실행된 스크립트는 비동기적으로 실행된다.
로딩은 되어도 실행 자체는 함수자체가 끝나야하기 때문이다.
따라서, loadScript(path) 아래에 있는 코드들은 스크립트 로딩이 종료되는 것을 기다리지 않는다.

가령 path = /my/script.js라고 가정한다면, loadScript(path)를 호출하자마자 script.js 내부의 함수들을 호출한다면 에러가 발생할 수 있다. 브라우저도 스크립트를 읽어오는 데 시간이 필요하기 때문이다. 따라서, 브라우저가 스크립트를 모두 읽은 후에 내부함수를 호출하는 방식이 되어야 한다.

loadScript의 두 번째 인수로 스크립트 로딩이 끝난 후 실행될 콜백(callback)함수를 추가하여 해결할 수 있다.

function loadScript(src,callback) {
  let script = document.createElement('script');
  script.src = src;
  
  script.onload = () => callback(script);
  
  document.head.append(script);
}

새롭게 불러온 스크립트에 있는 함수를 콜백 함수 안에서 호출하면 원하는 대로 외부 스크립타 안의 함수를 사용할 수 있게 된다.

loacScript(path,function(){
  newFunction();
  ...
});

이런식으로 두 번째 인수로 전달된 함수는 원하는 동작이( 위 예제에서는 외부 스크립트를 불러오는것) 이 완료되었을 때 실행된다.

이러한 방식을 콜백 기반 비동기 프로그래밍이라고 한다. 무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 콜백을 인수로 반드시 제공해야 한다.

콜백 속 콜백


스크립트가 2가지 있는 경우, 2개의 스크립트를 순차적으로 불러오는 방법을 구현해보자.

  • 콜백 속 콜백
loadScript('/my/script.js', function(script) {

  alert(`${script.src}loaded. next.`);

  loadScript('/my/script2.js', function(script) {
    alert(`script2 loaded.`);
  });

});

이런 방식은, 동작이 많은경우에 비효율적이다.

에러 핸들링


로딩이 실패할 경우에 에러를 추적할 수 있게 콜백 함수를 구현해보자.

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`error occured while loading ${src}.`));

  document.head.append(script);
}

loadScript는 스크립트 로딩에 성공하면, callback(null,script), 실패시 callback(error)을 호출한다.

if else 구문을 이용하여 다음과 같이 사용한다. 이를 오류 우선 콜백이라고 한다.

loadScript(path, function(error, script) {
  if (error) {
    // Error
  } else {
    // success
  }
});
  • callback의 첫 번째 파라미터는 에러를 위해 남겨둔다. 에러가 발생하면, `callback(error) 호출

  • 두 번째 파라미터는 성공적으로 실행되었을때 사용된다.

콜백 지옥


콜백을 무작위로 남발하게 되었을 때의 상황을 콜백 지옥이라고 한다. 콜백 지옥을 피하기 위해서는 다음과 같은 방법을 사용할 수 있다.

loadScript('1.js', step1);

function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', step2);
  }
}

function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('3.js', step3);
  }
}

function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // 모든 스크립트가 로딩되면 다른 동작을 수행합니다. (*)
  }
};

이런식으로 코딩하면, 콜백지옥을 피할 수 있다. 이보다 더 개선된 형태가 바로
promise를 사용하는 것이다.

프라미스(Promise)


let promise = new Promise(function(resolve,rejevt){
  // executor
});
                          

new Promise에 전달되는 함수를 executor(실행자,실행함수)라고 부른다. executornew Promise가 만들어질 때 자동으로 실행되며, 결과를 만들어내는 제작 코드를 포함한다.

executor의 파라미터 resolvereject는 자바스크립트가 자체적으로 제공하는 콜백이므로 , 따로 신경쓸 필요 없다.
하지만, 어찌 되었든간에 파라미터로 넘겨준 콜백 중 하나를 반드시 호출해야 한다.

  • resolve(value) - 일이 성공적으로 끝난 경우, 그 결과를 나타내는 value와 함께 호출

  • reject(error) - 에러 발생 시 에러 객체를 나타내는 error와 함께 호출

즉, 위에서 구현한 콜백 처럼 new Promise의 생성과 함께 executor가 실행되며 실행 성공 여부에 따라 resolvereject를 호출한다.

promise 객체


new Promise가 생성하여 반환하는 promise 객체의 내부 프로퍼티는 다음과 같다.

  • state - 디폴트로 "pending" 이었다가 resolve가 호출되면 "fulfilled", reject가 호출되면 "rejected"가 할당된다.

  • result - 디폴트로 undefined이었다가, resolve 호출시 value가, reject(error) 호출시 error가 할당된다.

executorpromise의 상태를 다음과 같이 변화시키다.

// `resolve(value)`호출
{
  state: "fulfilled"
  result : value
}

// `reject(error)` 호출
{
  state : "rejected"
  result : error
}

다음의 예시를 보자.
setTimeout을 이용하여 executor를 시간이 걸리게 끔 만들었다.

let promise = new Promise(function(resolve,reject){
  setTimeout(() => resolve("done"),1000);
  // 1초 뒤 "done"
});

위에서 언급하였듯, new Promise에 의해 executor는 자동으로 호출된다.
executor가 호출되면, 1초 뒤에 resolve("done") 이 호출되며 다음과 promise객체는 다음과 같은 상태가 된다.

{
  state : "fulfilled"
  result : "done"
}

resolve가 호출된 promisefulfilled promise라고 부른다.

다음은 에러가 발생하는 상황을 보자.

let promise = new Promise(function(resolve, reject) {
 setTimeout(() => reject(new Error("Error")), 1000);
 // 1초 뒤 에러와 함께 실행 종료
});

1초 후 reject(...)가 호출되면 promise객체는 다음과 같다.

{
  state : "rejected"
  result : error
}

이처럼 성공 또는 실패한 promisesettled promise라 부르며, 그와 반대로 성공 또는 실패 어느것도 이루어지지 않은 promisepending promise라고 한다.

  • 프라미스는 성공 또는 실패만 한다.

resolve(value) 또는 reject(error) 가 호출되면, 그 이후에 등장하는
executorresolve(value),reject(error)은 무시된다.

  • Error 객체 사용

reject(error)Error 객체 또는 Error를 상속받은 객체를 인수로 사용하는 것이 좋다.

  • resolve , reject함수 즉시 호출하기

executor는 대개 무언가를 비동기적으로 수행하는데, 즉시 호출 또한 가능하다.

then


.then은 프라미스에서 가장 중요하며, 기본적인 메서드이다.

promise.then(
  function(result) {...},
  function(error){...}

.then의 첫 번째 파라미터는 프라미스가 이행될 때 실행되는 함수이고 여기서 실행 결과를 받는다.

.then의 두 번째 파라미터는 프라미스가 거절될 때 실행되는 함수이며, 여기서 에러를 받는다.

  • 프라미스 이행 성공
let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
});

// resolve 함수는 .then의 첫 번째 파라미터(함수)를 실행
promise.then(
  result => alert(result), // 1초 후 "done!"을 출력
  error => alert(error) // 무시
);
  • 프라미스 거절
let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

// reject 함수는 .then의 두 번째 파라미터(함수)를 실행
promise.then(
  result => alert(result), // 무시
  error => alert(error) // 1초 후 "Error: 에러 발생!"를 출력
);

catch


에러처리만 하고 싶을때 .then(null,handleErrorFunc)의 형태로 구현 할 수 있다. 또, .catch(handleErrorFunc)을 써도 된다. .then을 이용하는것과 같은 방식으로 동작하며, 문법적 편의를 위한 용도이다.

finally


결과가 어떻든 마무리가 필요할 때 사용한다.

new Promise((resolve, reject) => {
  setTimeout(() => resolve("결과"), 2000)
})
  .finally(() => alert("프라미스가 준비되었습니다."))
  .then(result => alert(result)); // <-- .then에서 result를 다룰 수 있음

처리된 프라미스


promise"pending"상태일 때, .then/catch/finally 핸들러는 promise가 처리되길 기다리고, 이미 처리된 상태이면 핸들러는 즉각 실행된다.

예시


위의 loadScript 를 프라미스로 바꿔보자.


function loadScript(src) {
  return new Promise(function(resolve,reject){
    let script = document.createElement("script");
    script.src = src;
    
    script.onload = () => resolve(script);
    script.onerror = () => reject(new Error(`Error occured`));
    
    document.head.append(script);
  });
}

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");

promise.then(
  script => console.log(`${script.src}`),
  error => console.log(`${error.message}`)
  );
promise.then(script => console.log(` another handler `))
profile
연세대학교 산업공학과 웹개발 JavaScript

0개의 댓글