[ JS ] - 비동기 1

200원짜리개발자·2024년 6월 17일
0

FrontEnd

목록 보기
26/29
post-thumbnail

제로베이스 자바스크립트 기초개념 비동기 부분 정리
축약된 부분이 존재할 수 있습니다.

비동기

개요

동기

코드는 작성된 순서대로 실행되며, 하나의 작업이 끝나기 전에는 다음 작업이 시작되지 않습니다.

console.log(1);
console.log(2);
alert("확인");
console.log(3);
console.time("Loop!");
for (let i = 0; i < 10000000; i++) {}
console.timeEnd("Loop!");
console.log(4);

이런식으로 전 코드가 실행되기전까지는 뒤에 코드가 실행되지 않는다.

비동기

코드는 작성된 순서대로 실행되지만, 특정 작업이 끝나기 전에 다음 작업이 시작될 수 있습니다.

console.log(1);
console.log(2);
console.log(3);
console.time("Loop!");
// 비동기 함수
setTimeout(() => {
  for (let i = 0; i < 10000000; i++) {}
  console.timeEnd("Loop!");
}, 0);
console.log(4);
console.log(1);
const h1El = document.querySelector("h1");
h1El.addEventListener("click", () => {
  console.log("클릭");
});
console.log(2);
console.log(1);
fetch("https://api....")
  .then((res) => res.json())
  .then((data) => console.log(data));
console.log(2);

콜백과 콜백 지옥

function timer() {
  setTimeout(() => {
    console.log(1);
  }, 2000);
}
  
timer();
console.log(2);

이렇게 존재할 때 숫자1을 2보다 먼저 출력시키고 싶다고 한다면 어떻게 해야할까?
물론 setTimeout안에 console.log(2)를 넣을수도 있겠지만, 만약 모듈화가 되어있어서 건드릴 수 없다면? 그럴 때 콜백을 사용하면 된다.

function timer(callback) {
  setTimeout(() => {
    console.log(1);
    callback();
  }, 2000);
}
  
timer(() => {
  console.log(2);
});

이 콜백 패턴의 핵심은 우리가 비동기 함수를 호출할 때, 콜백 함수를 전달해줘서 콜백 함수가 정확히 어디서 실행될지 지정해주는 것이 핵심이다.

하지만, 단점이 존재하는데 바로 콜백 지옥이다.

function renderImage(callback) {
  const imgEl = document.createElement("img");
  imgEl.src = "https://picsum.photos/3000/2000";
  imgEl.addEventListener("load", () => {
    document.body.append(imgEl);
    callback();
  });
}
renderImage(() => {
  console.log("Done 1");
});
renderImage(() => {
  console.log("Done 2");
});
renderImage(() => {
  console.log("Done 3");
});
renderImage(() => {
  console.log("Done 4");
})

우리가 코드를 이런식으로 작성하게 된다면, Done의 순서가 없이 출력이 될 것이다.
왜냐하면, 첫 번째가 가장 먼저 도착할 것이라는 보장이 없기 때문이다.
인터넷 속도에 따라서 로드되는 속도가 서로 다르기 때문이다.
즉, 출발은 순서대로 해도 도착은 순서대로 된다는 보장이 없다.

그래서 이 문제점을 해결하기 위해 동기적으로 코드를 바꾸면..

function renderImage(callback) {
  const imgEl = document.createElement("img");
  imgEl.src = "https://picsum.photos/3000/2000";
  imgEl.addEventListener("load", () => {
    document.body.append(imgEl);
    callback();
  });
}
renderImage(() => {
  console.log("Done 1");
  renderImage(() => {
    console.log("Done 2");
    renderImage(() => {
      console.log("Done 3");
      renderImage(() => {
        console.log("Done 4");
      });
    });
  });
})

그럼 이런식의 코드가 만들어지는데 마치 콜백들이 개미지옥같이 들여쓰기가 계속 되는 모습을 콜백 지옥이라고 한다.

이것을 개선하기 위해서는 promise라는 클래스를 사용할 수 있다.
promise는 다음시간에 배울 것이기에 일단 수정부터 해보겠다.

function renderImage() 
  return new Promise((resolve) => {
    const imgEl = document.createElement("img");
    imgEl.src = "https://picsum.photos/3000/2000";
    imgEl.addEventListener("load", () => {
      document.body.append(imgEl);
      resolve();
    });
  });
}
renderImage()
  .then(() => {
    console.log("Done 1");
    return renderImage();
  })
  .then(() => {
    console.log("Done 2");
    return renderImage();
  })
  .then(() => {
    console.log("Done 3");
    return renderImage();
  })
  .then(() => {
    console.log("Done 4");
  });

이런식으로 promise를 사용하게 된다면, 콜백 지옥에서 벗어나 좀 더 가독성 좋게 코드를 짤 수 있다.

Promise

비동기 작업의 완료나 실패 지점을 지정하고 그 결과를 반환할 수 있습니다.`

function timer(cb) {
  setTimeout(() => {
    console.log(1);
    cb("Done!");
  });
}
  
timer((msg) => {
  console.log(msg);
  console.log(2);
});

이 코드를 promise를 사용한 코드로 바꿔본다면

function timer() {
  return new Promise((resolve, reject) => {
    if (error) {
      // reject 호출시 resolve 호출 안됨
      reject();
    }
    setTimeout(() => {
      console.log(1);
      // resolve 호출시 reject 호출 안됨
      resolve("Done!");
    });
  });
}
  
timer()
  .then((msg) => {
    console.log(msg);
    console.log(2);
    return timer();
  })
  .then((msg) => {
    console.log(msg);
    console.log(2);
    return timer();
  })
  .then((msg) => {
    console.log(msg);
    console.log(2);
  });
  ```
resolve와 reject는 반대되는 개념이라고 생각하면 된다.
reject는 추후에 다시 다루게 될 것이다.
```js
function loadImage(src) {
  return new Promise((resolve) => {
    const imgEl = document.createElement("img");
    imgEl.src = src;
    imgEl.addEventListener("load", () => {
      resolve(imgEl);
    });
  });
}
loadImage("https://picsum.photos/3000/2000").then((imgEl) => {
  document.body.append(imgEl);
  console.log("Done");
});
loadImage("https://picsum.photos/100/200").then((imgEl) => {
  console.log(imgEl);
})

그리고 위에 사진을 불러오는 코드를 좀 더 promise를 사용해서 활용성있게 만들어줄 수도 있습니다.
promise라는게 약속을 나타내는 이름이다 보니 콜백 함수가 어떤 시점에 실행 된다는 것을 약속한다는 의미를 지니고 있고, 그때 약속이 이행되었으면 then메소드를 호출하겠다는 것이다.

다음에는 async와 await에 대해서 배워볼 것이다.
then보다 최신 기술이기에 배워두면 좋다.

Async & Await

const h1El = document.querySelector("h1");
const ulEl = document.createElement("ul");
document.body.append(ulEl);
  
h1El.addEventListener("click", () => {
  ulEl.textContent = "Loading...";
  // promise instance가 반환이 된다.
  fetch("https://api.heropy.dev/v0/users")
    .then((res) => res.json())
    .then((data) => {
      const { users } = data;
      const liEls = users.map((user) => {
        const liEl = document.createElement("li");
        liEl.textContent = user.name;
        const imgEl = document.createElement("img");
        imgEl.src = user.photo?.url || "https://heropy.dev/favicon.png";
        liEl.prepend(imgEl);
        return liEl;
      });
      ulEl.textContent = "";
      ulEl.append(...liEls);
    });
})

일단 이런식으로 기본 코드가 있을 때,

const h1El = document.querySelector("h1");
const ulEl = document.createElement("ul");
document.body.append(ulEl);
  
h1El.addEventListener("click", async () => {
  ulEl.textContent = "Loading...";
  // promise instance가 반환이 된다.
  // await로 데이터를 가져오는 것을 기다림 (promise instance에만 사용가능)
  const res = await fetch("https://api.heropy.dev/v0/users");
  // 데이터 분석이 끝나는 것을 기다림
  const data = await res.json(); // promise instance 반환함
  const { users } = data;
  const liEls = users.map((user) => {
    const liEl = document.createElement("li");
    liEl.textContent = user.name;
    const imgEl = document.createElement("img");
    imgEl.src = user.photo?.url || "https://heropy.dev/favicon.png";
    liEl.prepend(imgEl);
    return liEl;
  });
  ulEl.textContent = "";
  ulEl.append(...liEls);
})

async와 await을 사용해서 위 코드처럼도 만들어줄 수 있다.
await은 무조건 promise instance에만 사용할 수 있고 await을 사용하는 가장 가까운 함수에 async가 있어야 한다.

예외 처리

fetch("https://api.heropy.dev/v0/users"); 만약 이런식으로 서버로 데이터를 요청하는 코드를 작성하였을 때, 우리가 글자를 잘 못 작성하거나 서버가 고장나있는 상태라면 오류가 나게될 것이다. 이러한 예외 상황들이 있을 때, 처리하는 코드를 만드는 것도 중요하다고 할 수 있다.

try~catch

이 때, 우리는 try~catch를 사용해서 예외 처리를 해줄 수 있다.

const h1El = document.querySelector("h1");
const ulEl = document.createElement("ul");
document.body.append(ulEl);
  
h1El.addEventListener("click", async () => {
  ulEl.textContent = "Loading...";
  // promise instance가 반환이 된다.
  try {
    const res = await fetch("https://api.heropy.dev/v0/users");
    const data = await res.json();
    const { users } = data;
    const liEls = users.map((user) => {
      const liEl = document.createElement("li");
      liEl.textContent = user.name;
      const imgEl = document.createElement("img")
      imgEl.src = user.photo?.url || "https://heropy.dev/favicon.png";
      liEl.prepend(imgEl);
      return liEl
    });
    ulEl.textContent = "";
    ulEl.append(...liEls);
  } catch (error) {
    console.log(error);
  }
});

이런식으로 try에서 시도를 해보다가 에러가 발생하면 catch부분에서 에러를 받아서 코드를 실행할 수 있다.

또한, try나 catch에 둘 다 넣어야 하는 코드가 있다면 finally 구문에 넣어주면 된다.

try {
	// 시도 해보다가 에러나면 그 아래부터 실행안하고 catch로 넘어감
} catch(error){
	// 에러나면 실행됨
} finally {
	// 에러가 나든 안나든 실행됨
}

그리고 여기서 reject를 말해볼 건데
만약, img를 불러오다가 error가 나게 되면 resolve밖에 없는 promise는 에러를 띄우지 않고 계속 약속을 이행하겠다라고만 하기에 우리는 reject라는 것들 추가해 addListener로 error라는 이벤트를 추가해서 reject를 실행시켜줄 수 있다.
그리고 reject에서 new Error라는 객체를 넘겨서 catch의 error에서 받을 수 있다.

reject(new Error("이미지를 로드할 수 없어.."))

그러면 이행과 거부. 예외 처리 부분을 한 번 정리해보자

// 이행과 거부, 예외 처리
  
//매개 변수
// resolve - 약속을 이행하는 함수(정상 처리)
// reject - 약속을 거부하는 함수(에러 상황)
  
// 용어 정리
// Pending - 약속이 이행되거나 거부되기 전 상태
// Fulfilled - 약속이 이행된 상태
// Rejected - 약속이 거부된 상태
function loadImage(src) {
  // Pending...
  return new Promise((resolve, reject) => {
    if (!src) {
      reject(new Error("이미지 경로가 필요해요")); // Rejected
    }
    const imgEl = document.createElement("img");
    imgEl.src = src;
    imgEl.addEventListener("load", () => {
      resolve(imgEl); // Fulfilled
    });
    imgEl.addEventListener("error", () => {
      reject(new Error("이미지를 불러올 수 없어요")); // Rejected
    });
  });
}
  
// .then() /.catch() /.finally()
// - 약속이 이행되었을 때 호출(then)하거나,
// - 약속이 거부되있을 때 호출(catch)하거나,
// - 이행 및 거부와 상관없이 항상 호출(finally)하는 메소드를 제공할 수 있습니다.
loadImage("https://picsum.photo/300")
  .then((imgEl) => {
    document.body.append(imgEl);
  })
  .catch((error) => {
    console.log(error.message);
  })
  .finally(() => {
    console.log("Done");
  });
  
// try / catch / finally
// - 에러(예외)가 발생할 수 있는 코드의 실행을 시도(try)하고,
// - 에러가 발생하면 시도를 종료해 에러를 잡아내며(catch),
// - 에러 여부와 상관없이 항상 실행(finally)하는 코드를 정의할 수 있습니다.
(async () => {
  try {
    const imgEl = await loadImage("https://picsum.photo/300");
    document.body.append(imgEl);
  } catch (error) {
    console.log(error.message);
  } finally {
    console.log("Done!");
  }
})();

이렇게 이행과 거부 부분을 정리할 수 있을 것 같다.

profile
고3, 프론트엔드

0개의 댓글