JS 비동기 (PurpleCode Study)

동기와 비동기

  • 동기: 작업이 끝날 때까지 기다리는 동안 중지 상태가 디고, 해당 작업이 끝나야 비로서 그 다음 예정된 작업을 할 수 있습니다.

  • 비동기 처리: 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고, 다음 코드를 먼저 실행하는 자바스크립트의 특성을 의미합니다.
    흐름이 멈추지 않기 때문에 동시에 여러가지 작업을 처리할 수 있고, 기다리는 과정에서 다른 함수도 호출할 수 있습니다.

비동기 처리의 사례들

  1. 제이쿼리의ajax
  2. setTimeout()

콜백 함수로 비동기 처리 방식의 문제점 해결하기

콜백 함수를 사용하여 특정 로직이 끝났을 때 원하는 동작을 실행할 수 있습니다.

콜백 함수의 문제점: 콜백 지옥

  • 가독성이 매우 떨어지고, 로직을 변경하기도 힘듭니다.

콜백 지옥을 해결하는 방법:

Promise

프로미스는 자바스크립트 비동기 처리에 사용되는 객체입니다.

const getData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ name: "김지원"});
    }, [5000]);
  });
};

getData().then(userData => console.log(userData));

Promise의 3가지 상태

new Promise()로 프로미스를 생성하고 종료될 때까지 3가지의 상태를 갖습니다.

  • Pending(대기): 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행): 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패): 비동기 처리가 실패하거나 오류가 발생한 상태
  1. new Promise() 메서드를 호출하면 대기상태가 됩니다.
 new Promise();
  1. 메서드를 호출할 때 콜백 함수를 선언할 수 있고, 콜백함수의 인자는 resolve, reject입니다.
 new Promise((resolve, reject) => {} );
  1. 여기서 콜백함수의 인자 resolve를 실행하면 이행상태가 됩니다.

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

 new Promise((resolve, reject) => {
   resolve();
 });
  1. 이행 상태가 되면 then()을 이용하여 처리 결과 값을 받을 수 있습니다.
 const getId = () => {
   const userId = { id:"annie1004619", name="김지원" };
  
   return new Promise((resolve, reject) => {
     resolve(userId);
   });
 };

 getId().then(userId => console.log(userId));
  1. new Promise()로 프로미스 객체를 생성하고 reject를 호출하면 실패상태가 됩니다.

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

 new Promise((resolve, reject) => {
   reject();
 });
  1. 그리고 실패 상태가 될 경우 catch()로 받을 수 있습니다.
 const getId = () => {
   const userId = { id: "annie1004619", name: "김지원" };
  
   return new Promise((resolve, reject) =>{
     reject(new Error("request is failed"));
   });
 };

 getId()
 .then(userId => console.log(userId))
 .catch(err => console.log(err));

종합한 간단한 프로미스 코드

const getId = () =>{
  const userId = { id: "annie1004619", name: "김지원" };
  
  return new Promise((resolve, reject)=>{
    if(userId){
      resolve(userId);
    }
    
    reject(new Error("Request is failed"));
  });
};

getId()
  .then(userId => console.log(userId));
  .catch(err => console.log(err));

여러 개의 프로미스를 연결하기 (Promise Chaining)

프로미스의 또 다른 특징은 여러 개의 프로미스를 연결하여 사용할 수 있다는 점 입니다.

new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve(1);
  }, 2000);
})
  .then(function(result) {
    console.log(result); // 1
    return result + 10;
  })
  .then(function(result) {
    console.log(result); // 11
    return result + 20;
  })
  .then(function(result) {
    console.log(result); // 31
  });
  1. 2초 후 최초 프라미스가 실행됩니다.
  2. 이후 .then 핸들러가 호출됩니다.
  3. 2에서 반환한 값은 다음 .then 핸들러에 전달됩니다.
  4. 이런 과정이 계속 이어집니다.

문제 1) 프라미스: then vs catch
두 코드 조각이 동일하게 동작할까요?
promise.then(f1).catch(f2);
promise.then(f1, f2);

답은 같지 않다!

promise
  .then(f1)
  .catch(f2);

f1에서 에러가 밸생하면 위 코드에서는 .catch에서 에러가 처리됩니다.

promise
  .then(f1, f2);

하지만 위 코드에서는f1에서 발생한 에러를 처리하지 못합니다.

문제 2) 프로미스로 지연 만들기
내장 함수 setTimeout은 콜백을 사용합니다.
프라미스를 기반으로 하는 동일 기능 함수를 만들어 보세요!

function delay(ms){
 return new Promise(resolve => setTimeout(resolve, ms));
}

delay(3000).then(()=>alert('3초 후 실행'));

resolve가 인수 없이 호출되었다. 함수 delay는 지연 확인 용이기 때문에 반환 값이 필요 없습니다.

async & await

: 자바스크립트 비동기 처리 패턴 중에서 가장 최근에 나온 문법입니다.

기존의 비동기 처리 방식인 콜백함수와 프로미스의 단점을 보완하고 순차적으로 읽으면서 사고할 수 있는 코드를 작성할 수 있게 도와줍니다.

async & await의 기본 문법

async function 함수명() {
  await 비동기_처리_메서드_명();
}

const 함수명 = async () => {
  await 비동기_처리_메서드_명();
};

주의 해야할 점: 비동기 처리 메서드가 꼭 프로미스를 반환해야 await가 의도한 대로 동작합니다.

const fetchMenu = () => {
  return new Promise(function(resolve, reject) {
    let Menu = ["아이스 아메리카노","카페 라떼","토피넛 라떼"];
    resolve(items);
  });
};

async function loadUsers() {
  let data = await fetchMenu();
  console.log(data); //["아이스 아메리카노","카페 라떼","토피넛 라떼"]
}

async & await 실용 예제

async & await은 여러개의 비동기 처리 코드를 다룰 때 유용합니다.

const fetchMenuType = () =>{
  return new Promise((resolve, reject) => {
    resolver("milk beverage");
  });
};

const fetchMenuName = () =>{
  return new Promise((resolve, reject) => {
    resolve("녹차 라떼");
  });
};

const getData = async ()=> {
  const menuType = await fetchMenuType();
  
  if(menuType === "milk beverage"){
    const menuName = await fetchMenuName();
    console.log(menuName);//녹차 라떼
  }
};

async & await 예외 처리

예외 처리는 try catch로 할 수 있습니다.

const getData = async ()=> {
  const menuType = await fetchMenuType();
  
  if(menuType === "milk beverage"){
    const menuName = await fetchMenuName();
    console.log(menuName);//녹차 라떼
  }
}catch (e) {
  console.log(e);
}
};

실행하다가 발생한 네트워크 오류 뿐 아니라 간단한 타입 오류 등 일반적인 오류까지도 catch로 잡아낼 수 있습니다.

프로미스 API

Promise 클래스에는 2가지 정적 메서드가 남아있습니다.

1. Promise.all:
여러 개의 프라미스를 동시에 실행시키고 모든 프라미스가 준비될 때까지 기다린다고 했을 때(복수의 URL에 동시에 요청을 보내고, 다운로드가 모두 완료된 후에 콘텐츠를 처리할 때 이런 상황이 발생합니다.) Promise.all을 사용할 수 있습니다.

사용법
Promise.all([...promises]);

Promise.all은 요소 전체가 프라미스인 배열을 받고 새로운 프라미스를 반환합니다.

배열 안 프라미스가 모두 처리되면 새로운 프라미스가 이행되는데, 배열 안 프라미스의 결괏값을 담은 배열이 새로운 프라미스의result가 됩니다.

EX) Promise.all은 3초 후에 처리되고 반환되는 프라미스의 result는 배열 [1,2,3]이 됩니다.

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), [3000])),
  new Promise(resolve => setTimeout(() => resolve(2), [2000])),
  new Promise(resolve => setTimeout(() => resolve(3), [1000]))
]).then(console.log); // 1,2,3

배열 result의 요소 순소는 Promise.all에 전달되는 프라미스 순서와 상응합니다.
Promise.all의 첫번째 프라미스는 가장 늦게 이행되더라도 처리 결과는 배열의 첫 번째 요소에 저장됩니다.

EX )GitHub 유저네임이 담긴 배열을 사용해 사용자 정보를 가져오는 예시를 살펴봅시다 (id를 기준으로 장바구니 목록을 불러올 때도 같은 로직을 사용할 수 있습니다).

let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
  .then(responses => {
    // 모든 응답이 성공적으로 이행되었습니다.
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // 모든 url의 응답코드가 200입니다.
    }

    return responses;
  })
  // 응답 메시지가 담긴 배열을 response.json()로 매핑해, 내용을 읽습니다.
  .then(responses => Promise.all(responses.map(r => r.json())))
  // JSON 형태의 응답 메시지는 파싱 되어 배열 'users'에 저장됩니다.
  .then(users => users.forEach(user => alert(user.name)));

2. Promise.allSettled:
Promise.all과 차이가 있는 메서드 입니다.
먼저 Promise.all은 프라미스가 하나라도 거절되면 전체를 거절합니다. 따라서 프라미스 결과가 모두 필요할 때 같이 모 아니면 도 일 경우 유용하게 사용됩니다.

반면에 Promise.allSettled는 모든 프라미스가 처리될 때까지 기다립니다.
반환되는 배열은 다음과 같은 요소를 가집니다.

  • 응답이 성공할 경우: {status: "fulfilled", value: result}
  • 에러가 발생할 경우: {status: "rejected", reason: error}

fetch를 사용해 여러 사람의 정보를 가져오고 있다고 해봅시다. 여러 요청 중 하나가 실패해도 다른 요청 결과는 여전히 있어야 합니다.

이럴 때 Promise.allSettled를 사용할 수 있습니다.

좋은 웹페이지 즐겨찾기