JS 비동기 역사

박민형·2023년 2월 3일
0
post-thumbnail

📌 JS 비동기 역사를 알아본 이유

  • 비동기 작업을 위해 Promise, async, await을 자주 사용하는데 이 기술들에 대한 기본 개념 숙지와 더불어 역사라는 단어가 나온 만큼 왜 기술에 있어 변화가 있어야 하는지 알고 싶었다.
  • 특정 기술을 도입한 이유를 안다는 것은 이전 기술의 개념과 문제점, 현재 기술의 개념 및 보완점에 대해 안다는 것이라 생각한다.

📌 사전 개념 인지

javacript는 싱글스레드 언어

  • 코드를 동기적으로 처리
  • 만약 5초가 걸리는 API 요청을 하게 된다면 5초 동안은 다른일을 하지 못하기 때문에 비효율적인 문제 발생
  • 이러한 문제점을 해결할 수 있는 방법이 비동기 기술

📌 Callback

// Cabllback을 이용한 방법
const countUp = (count, callback) => {
  setTimeout(() => callback(count + 1), 1000);
};

countUp(1, (count) => {
  countUp(count, (count) => {
    countUp(count, (count) => {
      countUp(count, (count) => {
        // 위에서 callback 함수를 호출하는 곳에서 아무것도 할 수 없으므로
        // 여기서 다른 작업을 처리해야함
        console.log(count);
      });
    });
  });
});

Callback을 이용해 비동기 처리를 할 경우 문제점

  • 콜백 지옥
  • 비동기 처리 결과를 외부에 반환하지 못함
  • 가독성이 떨어짐
  • 에러가 발생하기 쉽지만 그 에러를 추적하기는 어려움
  • 위의 이유들로 인해 유지/보수가 어려움

📌 Promise

// Promise를 이용한 방법
const countUpPromise = (count) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (count) resolve(count + 1);
      else reject(new Error("not Count"));
    }, 1000);
  });
};

const promiseReturnValue = countUpPromise(1)
  .then((count) => countUpPromise(count))
  .then((count) => countUpPromise(count))
  .then((count) => countUpPromise(count))
  .then((count) => console.log(count))
  .catch((err) => console.log(err));

console.log(promiseReturnValue);

Callback에서 보완된 점

  • 콜백 지옥 에서 벗어남
  • Callback 에서는 비동기 처리만 했지만 Promise는 일급 객체로써 대기(pending)와 성공(fulfilled)과 실패(rejected) 일급 값을 통해 비동기 처리와 더불어 값 반환도 할 수 있음
  • Callback을 통해 네트워크 통신을 할 때 문제 발생에 대비해 예외 처리 코드를 콜백 함수마다 넣어주게되면 가독성 및 유지/보수 어려움 => Promise의 메서드를 통해 문제점 해결할 수 있음

Promise를 이용해 비동기 처리를 할 경우 문제점

  • 여전히 체인 속에 작업이 묶여 있음
  • 비동기 작업의 순서 보장이 안됨
// Promise의 문제점
const fetchComment = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      fetch("https://jsonplaceholder.typicode.com/comments")
        .then((res) => res.json())
        .then((data) => resolve(data));
    }, 3000);
  });
};

const fetchPost = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      fetch("https://jsonplaceholder.typicode.com/posts")
        .then((res) => res.json())
        .then((data) => resolve(data));
    }, 1000);
  });
};

// 코멘트 먼저 불러오고 싶은데 post가 먼저 fetch 된다.
fetchComment().then((data) => console.log("commentData", data));
fetchPost().then((data) => console.log("postData", data));

📌 generator

function* genFunc() {
  try {
    setTimeout(() => {
      fetch("https://jsonplaceholder.typicode.com/comments")
        .then((res) => res.json())
        .then((data) => {
          console.log(data);
        });
    }, 3000);
    yield 1;

    setTimeout(() => {
      fetch("https://jsonplaceholder.typicode.com/posts")
        .then((res) => res.json())
        .then((data) => {
          console.log(data);
        });
    }, 1000);
  } catch (e) {
    console.error(e);
  }
  yield 2;
}

const generator = genFunc();
generator.next();

Promise에서 보완된 점

  • 함수의 제어권을 함수가 독점하는 것이 아니라 함수 호출자에게 양도(yield)

generator를 이용해 비동기 처리를 할 경우 문제점

  • 값 반환 및 제어권을 넘겨받으려면 next() 필요

generator와 같이 사용하면 좋은 것 => co 라이브러의 co, wrap 함수

// co 함수에 제너레이터를 인수로 넘기면 제너레이터를 마지막까지 실행
// 실행결과로 Promise 반환
co(function* () {
    const id = yield getId('010-1234-5678');
    const email = yield getEmail(id);
    const name = yield getName(email);
    return yield order(name, 'coffee');
}).then(result => {
    console.log(result);
});

// wrap 함수를 이용해 제너레이터 함수를 Promise를 반환하는 함수로 변환
const orderCoffee = co.wrap(function *() {
    const id = yield getId('010-1234-5678');
    const email = yield getEmail(id);
    const name = yield getName(email);
    return yield order(name, 'coffee');
});

orderCoffee.then(result => {
    console.log(result);
});

📌 async, await

// async, await
const fetchCommentAsync = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      fetch("https://jsonplaceholder.typicode.com/comments")
        .then((res) => res.json())
        .then((data) => resolve(data));
    }, 3000);
  });
};

const fetchPostAsync = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      fetch("https://jsonplaceholder.typicode.com/posts")
        .then((res) => res.json())
        .then((data) => resolve(data));
    }, 1000);
  });
};

const renderData = async () => {
  console.log(await fetchCommentAsync());
  console.log(await fetchPostAsync());
};

renderData();
// co 라이브러리 wrap 대신 async 사용
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function getPizza() {
  await delay(2000);
  return "pizza";
}

async function getChicken() {
  await delay(1000);
  return "chicken";
}

async function pickFoods() {
  try {
    const pizza = await getPizza();
    const chicken = await getChicken();
    return `${pizza} + ${chicken}`;
  } catch(){
  }
}

pickFoods().then(console.log);

generate에서 보완된 점

  • async는 `co 라이브러리의 wrap 함수 역할
  • await는 yield 역할 => 값을 반환하고 제어권을 넘긴다.
  • 비동기 작업이지만 동기 작업처럼 소스코드를 작성할 수 있어 가독성이 좋다.

📌 그 외 참조하면 좋은 것

적절한 promise 사용

  • async, await 대신 promise를 사용해도 될때가 있다.
  • 해당 소스코드는 순서보장을 하지 않아도 되므로 promise의 all 함수를 사용하면 좀더 간결하게 표현할 수 있어 가독성이 좋아질 것 같다.(멘토님 추천)

📌 참조

비동기 역사 출처

콜백 지옥 출처

일급 객체 출처

co 라이브러리 및 co, wrap 함수 출처

0개의 댓글