[JavaScript] 비동기,동기,Promise,async 공부하기


1️⃣ 목표

  1. 동기와 비동기 프로그래밍 이해
  2. 콜백 함수를 이용하여 비동기를 처리하여 사용하기
  3. Promise사용법 배우기
  4. Promise의 비동기기능을 사용해보기(login폼)
  5. async사용법 배우기

2️⃣ 동기와 비동기

(1) 동기(Synchronous)

  • 동기형 함수들만 있으면 단순히 작성된 순서로 코드가 실행됩니다.
console.log(1);
console.log(2);
console.log(3);
1
2
3

(2) 비동기(Asynchronous)

  • 만약 하나의 함수가 처리하는데 시간이 오래걸린다면 그다음번 코드는 끝날때까지 대기 하게 됩니다.
  • 실제로 서버에서 클라이언트의 입출력(IO)에서 지연이 많이 발생한다고 합니다. 만약 클라이언트의 입출력 빈도가 더 늘어난다면 서버는 무한대기상태에 빠지게 될 것입니다.
  • 이때 비동기프로그래밍을 이용하면 좀 더 효율적으로 처리할 수 있습니다. (예를들어 멀티쓰레드 방식으로 동시에 처리)
  • 하지만 자바스크립트싱글쓰레드로 처리합니다. 그대신에 웹 브라우저에서 제공하는 API를 이용하면 웹에 있는 js엔진에서 비동기적으로 처리해줍니다.
  • 웹API의 대표적인 예로 setTimeout()콜백 함수가 있습니다.
console.log(1);
setTimeout(() => console.log(2), 1000);
console.log(3);
1
3
2

3️⃣ call back 함수로 비동기 처리하기

(1) 비동기를 동기 프로그래밍 처리하기

  • 하지만 비동기로 처리된 후의 값이 필요한 경우에는 동기적으로 처리해줄 수 밖에 없습니다.
  • 이때 콜백함수를 이용하면되는데 처리를 실패했을때 처리도 고려하여 코드를 구성하여야 합니다.
  • 간단한 예시로 Popcorn메이커를 만들어 봤습니다.

(2) call back함수 예시(Popcorn메이커)

class Cooker {
  inspectFood(food, available, onError) {
    console.log("getting food...");
    setTimeout(() => {
      if (food === "🌽" || food === "🥔") {
        available(food);
      } else {
        onError(new Error("no food available"));
      }
    }, 2000);
  }
  makePopcorn(food, available, onError) {
    console.log("cooking...");
    setTimeout(() => {
      if (food === "🌽") {
        available(`${food} => 🍿`);
      } else {
        onError(new Error(`${food} => ❌`));
      }
    }, 2000);
  }
}
  • 위의 코드는 다음의 메소드를 가진 Cooker 클래스입니다.
    1. 식재료유효한 식재료인지 검사하는 inspectFood()메소드
    2. 식재료팝콘을 만드는 makePopcorn()메소드

(3) Popcorn메이커(call back) 사용 해보기

const cooker = new Cooker();
const CORN = "🌽";
const POTATO = "🥔";
const APPLE = "🍎";

const food = CORN; // 사용자가 임의의 음식 설정

cooker.inspectFood(
  food,
  (availableFood) => {
    cooker.makePopcorn(
      availableFood,
      (availablePopcorn) => {
        console.log(availablePopcorn);
      },
      (error) => {
        console.log(error);
      }
    );
  },
  (error) => {
    console.log(error);
  }
);
  • 위에서 🌽 와 🥔 의 경우 출력을 보면 재료를 검사할때까지 기다린 후 요리를 시작 합니다.

(4) Popcorn메이커(call back)의 단점

  • 위에서 파악한 Popcorn메이커는 사용하는데 있어서 가독성이 너무 떨어집니다. (callback함수의 중첩…)
  • Popcorn메이커콜백함수가 겨우 2단계로만 사용됐는데도 불구하고 코드를 읽기가 쉽지않습니다. 대규모 프로젝트에서는 비동기 처리가 좀 더 깊게 구현할때가 있을 텐데 만약 이런식으로 콜백함수를 사용한다면 코드를 알아보기는 커녕 유지보수조차 할 수 없을 것 입니다.

4️⃣ Promise

(1) Promise기본사용

  • Promise를 사용하면 콜백함수보다 더 깔끔하게 호출하여 사용할 수 있습니다.
/* 방법 1 */
sampleFunc(param) {
    return new Promise((resolve, reject) => {
        /* 비동기함수 생략 */
        /* 성공시 */
        resolve(a);
        /* 실패시 */
        reject(b);
    })
}

/* 방법 2 */
const sample = new Promise((resolve, reject) => {
    /* 생략 */
})
  • Promise는 Javascript에서 기본적으로 제공하는 오브젝트입니다. 위처럼 두가지방법으로 사용이 가능한데 방법 2의 방법은 비추천하는 선언방법 입니다. 그 이유는 방법 2선언과 동시에 Promise가 호출되어 실행되기 때문입니다. (만약 홈페이지를 열때마다 promise의 비동기과정이 일어나면서 데이터를 읽어오면 매우 비효율적이기 때문)
  • Promise비동기 진행 중 일때는 pending(대기)상태에 있다가 비동기 처리가 완료되면 fulfilled(이행)상태로 되고 비동기 처리가 실패하면 rejected(실패)상태가 됩니다.
  • 간단하게 처리성공시 resolve처리실패시 reject콜백함수를 줍니다.
  • 어떻게보면 콜백함수로 만든 것과 별차이가 없어보이지만 consumer(사용)하는 방법에서 큰차이가 나며 Promise가 압도적으로 깔끔합니다.

(2) Popcorn메이커 Promise로 변환하기

class Cooker {
  inspectFood(food) {
    return new Promise((resolve, reject) => {
      console.log("getting food...");
      setTimeout(() => {
        if (food === "🌽" || food === "🥔") {
          resolve(food);
        } else {
          reject(new Error("no food available"));
        }
      }, 1000);
    });
  }

  makePopcorn(food) {
    return new Promise((resolve, reject) => {
      console.log("cooking...");
      setTimeout(() => {
        if (food === "🌽") {
          resolve(`${food} => 🍿`);
        } else {
          reject(new Error(`${food} => ❌`));
        }
      }, 1000);
    });
  }
}
  • 콜백함수복잡도면에서는 별차이가 없어보입니다. 이번엔 사용(consumer)코드를 보겠습니다.

< 콜백함수 consumers>

callback_consumers

< 프로미스 consumers>

promise_consumers
  • 2단계구성된 비동기처리지만 프로미스(promise)눈에 띄게 깔끔함을 알 수 있습니다.
  • 프로미스(Promise)Promise 오브젝트를 반환하기 때문에 연달아서 then, catch, finally와 같은 Promise관련 메소드를 사용할 수 있습니다.
  • 먼저 then앞선처리가 성공하면 호출됩니다.
  • catchreject를 감지하여 에러값을 처리합니다.
  • .finally성공유무를 떠나서 반드시 호출됩니다. (try-catch구문에서 쓰임과 비슷)

5️⃣ Promise(비동기 함수) 보충

(1) Promise 의미 다시 생각해보기

  • Popcorn메이커비동기적 처리에 대한 좋은 예시가 아닌 것같습니다. 단순히 Promise로 구현한 것이 callback함수로 작성한 것보다 사용하는데 있어서 깔끔한 것을 보여주는 예시일 뿐입니다.
  • 사실 Promise를 우리말로 직역하면 “약속”이라는 뜻으로 “내가 비동기적으로 처리할태니 너는하던거해, 대신에 나중에 반환값을 줄 것을 약속할게!”로 이해할 수 있습니다.
  • 위에서도 언급했듯이 Promise가 가질 수 있는 상태는 pending(대기), fulfilled(이행), rejected(실패) 세가지 입니다. resolve, reject를 정의해주지 않으면 pending상태에 있을것이며, 성공유무에 따라 fulfilled나 rejected상태가 될 것입니다.
  • 하지만 이런식으로 상태를 가질 수 있게하려면 Promisee를 선언해주어야합니다. 대신 이 곳 에서 말한 이유로 함수안에서 promise를 선언하여 사용해야합니다.

(2) 로그인폼 예시

  • promise를 의미있게 사용하는 예시를 보여주기 위해 로그인폼을 만들어 봤습니다.
  • 아래 코드처럼 사용자 정보를 읽어오는 함수로써 promise를 사용했습니다. (실제 데이터를 읽어오는 것처럼만 흉내)
function login(id, password) {
  return new Promise((resolve, reject) => {
    if (id === "kirkim" && password === "111") {
      setTimeout(() => {
        resolve(`<사용자 정보>\n아이디: ${id}\n이름: 김기림\n국적: 대한민국`);
      }, 4000);
    } else {
      reject(new Error(`login fail!`));
    }
  });
}
  • 작동방식은 다음과 같습니다.
    1. ID와 Password를 입력하여 로그인
    2. 유효한 로그인이면 사이트가 접속과 동시에 Promise 함수를 호출하여 정보를 비동기적으로 읽도록함
    3. 정보를 읽어오면 “logout”, “정보보기”버튼이 생성됨
    4. “정보보기”버튼을 누르면 Promise 함수로 읽어온 데이터를 바로 볼 수 있습니다.

👉🏻 (ID: kirkim, PW: 111)

6️⃣ Async

(1) Async사용

function sampleFunc() {
    return new Promise((resolve, reject) => {
        resolve("success);
    })
}
async function sampleFunc() {
  return "success";
}
  • function앞에 "async"를 붙이면 자동으로 promise처럼 동작하게 만들어줍니다.
async function s1() {
    /* 코드 생략 */
}

async function s2() {
    /* 코드 생략 */
}

async function sampleFunc() {
    try (
        const value1 = await s1();
        const value2 = await s2();
    ) catch (err) {
        throw (err);
    }
}
  • await를 붙이면 비동기함수가 완료될때까지 기다려줍니다. 단, async가 붙은 함수 내부에서만 사용이 가능합니다.
  • 결론적으로 위와같이 비동기함수를 동기함수처럼 작성하여 사용할 수 있습니다.
  • 오류같은경우 try, catch구문을 이용하면 됩니다. (더욱이 try-catch식의 코드가 좀더 익숙하기까지함)

(2) Async가 필요할 때?

  • async를 사용하면 비동기함수를 평소에 익숙한식으로 코드를 작성하여 사용할 수 있습니다.
  • 만약 Promise함수를 연달아서 사용된다면 then이 연속적으로 오기 때문에 코드가 지저분해질 수 있습니다.
  • 다음의 극단적인 예로 Promiseasync를 비교해 보겠습니다.

< promise >

promise

< async >

async
  • 위는 promise, async에 맞게 함수들을 구현해준 모습입니다. 이 함수들은 어디까지나 임시로 구현한 것이기 때문에 작성 방법정도만 비교하면 될 것 같습니다.
  • 중점적으로 비교해야될 부분은 위의 비동기함수들을 최종적으로 사용한 다음의 코드들입니다.

< promise >

promise

< async >

async
  • 두코드 모두 비동기함수들이 연쇄적으로 이어진거의 똑같이 동작하는 코드입니다.
  • 한눈에봐도 async를 이용한 쪽이 사용하기가 더 깔끔합니다.
  • 그러나 이 예시async의 사용이 유리할 경우일 뿐이고, 분명 promise를 사용하는게 더 좋은 경우도 있기 때문에 “상황에 따라 잘 선택해서 사용하는 것이 중요”합니다.

(3) async 비동기적으로 사용하기

  • async도 미리 선언시켜놓는 방법으로 사용하면 비동기적으로 사용할 수 있습니다.
function sleep(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
  });
}

async function func1() {
  await sleep(1000);
  return `1`;
}
async function func2(a) {
  await sleep(1000);
  return `2`;
}
async function func3() {
  await sleep(1000);
  return `3`;
}
async function func4() {
  await sleep(1000);
  return `4`;
}

async function usePromiseChain() {
  const a1 = func1();
  const a2 = func2();
  const a3 = func3();
  const a4 = func4();

  const c1 = await a1;
  const c2 = await a2;
  const c3 = await a3;
  const c4 = await a4;
  console.log(c1, c2, c3, c4);
}

usePromiseChain();
  • 위와 같이 a변수들에 async함수들을 미리 선언하여 사용했습니다. (선언과 동시에 비동기처리 시작)
  • 주의할 점은 동기적으로 사용하기 이전에 await로 비동기과정을 기다려주는 작업이 필요합니다. (console.log로 출력하기 이전에 c1,c2,c3,c4 데이터들을 읽어올때까지 기다려야함)
  • 만약 비동기작업들이 서로 연관된게 아니라면 위의 경우처럼 미리 선언하여 사용하는 것이 좋고 그것이 “비동기프로그래밍을 하는 이유”라고 생각합니다.




© 2021.02. by kirim

Powered by kkrim