본문 바로가기

JavaScript

비동기

[22.12.20 수정]

 

동기와 비동기

동기(Synchronous) : 순차적으로 코드를 실핼하는 방법

console.log(1);

console.log(2);

console.log(3);

// 1 -> 2 -> 3 출력

동기 방식은 코드를 입력한 순서대로 순차적으로 실행되어 위의 콘솔 명령도 순차적으로 실핼되어 위의 결과가 나오게 됩니다.

 

 

 

비동기(Asynchronous) : 순차적으로 코드가 실행되지 않는 방법

console.log(1);

setTimeout(() => {
  console.log(2);
}, 1000);

console.log(3);

// 1 -> 3 -> 2 출력

비동기의 대표적인 setTimeout() 함수의 인자로 기존의 콘솔 명령을 전달해주게 되면, 코드가 순차적으로 실행되지 않고 setTimeout() 함수의 인자로 전달한 콘솔 명령이 1초 뒤에 실행되어 위의 결과가 나오게 됩니다.

 

const btnEl = document.querySelector("h1");
btnEl.addEventListener("click", () => {
  console.log("Click");
});

console.log("Hello world");

위의 코드 결과는 "Hello world"가 콘솔에 찍히고, "Click"은 콘솔에 찍히지 않고 btnEl을 클릭했을 경우에만 찍히게 됩니다.

 

이는 Event의 인자로 주어진 함수도 비동기 방식으로 작동하는 것을 의미합니다.

 

 

fetch 함수는 sever에 데이터를 요청(request)할 때 사용하며, 해당 server에서는 요청에 대해 응답(resoponse)을 보내게 됩니다.

 

이는 네트워크의 자원 즉, 인터넷을 사용하여 요청을 보내고 응답을 전달받는 것인데 이때 인터넷의 속도가 느리면 요청과 응답의 속도 또한 느려지게 될 것입니다.

 

결과적으로 server에 요청과 응답을 보내는 것은 일정한 시간이 필요한 작업인데 요청/응답 과정이 동기적으로 작동하게 된다면 응답을 받기 전까지 아래의 코드들은 실행되지 못 하고 대기 상태로 기다려야될 것입니다.

 

때문의 위의 코드 결과는 콜솔 명령은 실행되고, fetch 함수는 비동기적으로 작동하는 결과를 확인할 수 있게 됩니다.

 

 

 

 

비동기 제어

우선 비동기 코드를 관리하는 콜백 패턴에 대해 살펴보도록 하겠습니다.

 

[ 콜백 ] : 콜백 이란 함수의 인수로 전달되는 또 다른 함수

 

const a = () => console.log(1);
const b = () => console.log(2);

a();
b();

// 1 -> 2 출력

 

함수의 호출 순서에 의해 위의 결과가 나오는 동기 코드입니다.

 

const a = () =>
  setTimeout(() => {
    console.log(1);
  }, 1000);
const b = () => console.log(2);

a();
b();

// 2 -> 1 출력

앞서 언급한 setTimeout() 함수의 사용으로 함수 a는 비동기 코드가 되었으며 위의 결과가 나오게 됩니다.

(함수 a가 실행된 후 함수 b는 함수 a의 실행문이 실행되기를 기다리지 않고 실행됩니다.)

 

이 때 1이 먼저 출력되고 2가 출력되게 하고 싶은 경우 아래와 같은 방법으로 해당 결과를 나오게 할 수 있습니다.

const a = (callback) =>
  setTimeout(() => {
    console.log(1);
    callback();
  }, 1000);
const b = () => console.log(2);

a(() => {
  b();
});

// 1 -> 2 출력

함수 a의 인수로 콜백 함수를 전달하게 되면 콘솔 1이 실행 된 후 인수로 전달 받은 콜백 함수의 실행문이 실행되게 됩니다.

 

 

 

 


Promise

 

앞서 설명한 비동기 코드 제어를 위한 콜백 패턴은 아래의 코드와 같이 여러 개의 콜백 패턴이 될 수도 있습니다.

const taskA = (a, b, cb) => {
  setTimeout(() => {
    const res = a + b;
    cb(res);
  }, 3000);
};

const taskB = (a, cb) => {
  setTimeout(() => {
    const res = a * 2;
    cb(res);
  }, 1000);
};

const taskC = (a, cb) => {
  setTimeout(() => {
    const res = a * -1;
    cb(res);
  }, 2000);
};

taskA(4, 5, (a_res) => {
  console.log(`A result : ${a_res}`);
  taskB(a_res, (b_res) => {
    console.log(`B result : ${b_res}`);
    taskC(b_res, (c_res) => {
      console.log(`C result : ${c_res}`);
    });
  });
});

위의 예시 코드처럼 콜백 함수의 콜백 함수의 콜백 함수를 넣어 비동기 처리의 결과를 또 다른 비동기 처리의 값으로 전달할 수 있습니다.

 

중첩된 세 번의 콜백 함수지만 가독성이 많이 떨어지는 것을 확인할 수 있습니다.

 

만약 중첩된 콜백 함수가 10개, 20개로 많아지게 된다면 콜백 함수가 끝도 없이 이어지는 지옥을 만나게 될 것입니다.

 

이렇게 비동기 처리의 결과를 또 다른 비동기 처리의 값으로 전달하는 로직이 끝도 없이 길어지게 되는 것을 흔히 콜백 지옥이라고 합니다.

 

이때 사용할 수 있는 JavaScript의 비동기 담당 객체가 Promise입니다.

 

Promise를 사용하여 비동기 처리의 결괏값을 핸들링하는 방법을 살펴보겠습니다.

 

 


 

Promise를 사용하면 콜백 함수를 줄지어 전달하지 않아도 되어 보다 쉽고 빠르고, 직관적으로 비동기 처리를 할 수 있습니다.

 

비동기 작업이 가질 수 있는 3가지 상태

Pending은 현재 비동기 작업이 진행 중이거나 작업이 시작할 수 없는 문제가 발생했음을 의미합니다.

 

Fulfilled는 비동기 작업이 의도한 대로 정상적으로 완료되었음을 의미합니다.

 

Rejected는 비동기 작업이 어떠한 이유로 인해 실패했음을 의미합니다.

 

이유로는 서버가 응답을 하지 않는다거나 시간이 너무 오래 걸려 자동으로 취소되는 것을 들 수 있습니다.

 

이렇게 비동기 작업은 3가지의 상태를 가지며, 한 번 실패하면 해당 작업은 그렇게 끝이 나게 됩니다.

 

Pending 상태에서 Fulfilled 상태로 변화하는 과정을 resolve라고 하며 Rejected 상태로 변화하는 과정을 reject라고 합니다.

 

 

비동기 작업이 성공했음을 알리는 resolve과정실패했음을 알리는 reject과정을 예시 코드와 함께 살펴보겠습니다.

function isPositive(number, resolve, reject) {
  setTimeout(() => {
    // 만약 전달 받은 인자가 숫자가 아니라면 비동기작업은 실패
    if (typeof number === "number") {
      // 비동기 작업 성공 -> resolve
      resolve(number >= 0 ? "양수" : "음수");
    } else {
      // 비동기 작업 실패 -> reject
      reject("주어진 값이 숫자형 값이 아닙니다.");
    }
  }, 2000);
}

isPositive(
  10,
  (res) => console.log(`성공적으로 수행되었습니다. : ${res}`),
  (err) => console.log(`실패하였습니다. : ${err}`)
);

// 성공적으로 수행되었습니다. : 양수 출력

비동기 함수 setTimeout에 전달받은 전달받은 인자가 숫자인지 아닌지, 숫자이면 음수인지 양수인지 판별하는 조건문을 넣어주었습니다.

 

전달받은 인자가 숫자라면 비동기 작업이 성공했음을 알리는 resolve이고, 숫자가 아니라면 비동기 작업이 실패했음을 알리는 reject입니다.

 

매개변수 resolve, reject에는 콜백 함수이며 resolve는 결괏값을 전달받고, reject는 실패 내용을 전달받습니다.

 

 

이번에는 Promise를 사용한 비동기 처리를 예시 코드와 살펴보겠습니다.

function isPositiveP(number) {
  const executor = (resolve, reject) => {
    // 실행자
    setTimeout(() => {
      if (typeof number === "number") {
        console.log(number);
        // 비동기 작업 성공 -> resolve
        resolve(number >= 0 ? "양수" : "음수");
      } else {
        // 비동기 작업 실패 -> reject
        reject("주어진 값이 숫자형 값이 아닙니다.");
      }
    }, 2000);
  };

  const asyncTask = new Promise(executor);
}

isPositiveP(100); // 100 출력

 

함수 isPositiveP를 생성해주었고 동작인 위의 함수 isPositive와 똑같이 동작하도록 만들어주었습니다.

 

함수 isPositiveP는 number 매개변수에 인자를 똑같이 받아서 2초 뒤에 비동기적으로 인자로 받은 값이 양수인지 음수인지 

 

숫자 타입이 아니라면 rejeect를 시키도록 해주었습니다.

 

함수 isPositiveP에 있는 함수 executor는 두 개의 인자를 받으며 resolve는 비동기 작업이 성공했을 때의 콜백 함수,

 

reject는 비동기 작업이 실패했을 때의 콜백 함수를 전달받습니다.

 

다음으로 함수 isPositiveP에서 비동기 작업을 실행시키기 위해 Promise를 생성해줍니다.

 

new 키워드를 사용하여 Promise 객체를 생성하면서 생성자로 비동기 작업의 실질적인 실행자 함수 executor를 넘겨주게 되면

 

자동으로 함수 executor가 수행이 되게 됩니다.

 

그리고 생성한 Promise 객체를 반환하게 해 주면 함수 isPositiveP는 Promise 객체를 반환해주는 함수가 되게 됩니다.

 

Promise 객체를 반환해준다는 것은 해당 함수는 비동기 작업을 하고 그 작업의 결과를 Promise 객체를 반환받아서 사용할 수 있는 

 

함수를 의미하게 됩니다.

 

 

Promise 객체의 비동기 처리를 사용하는 방법은 아래의 예시 코드와 같습니다.

const respones = isPositiveP(100);

respones
  .then((res) => {
    console.log(`작업 성공 : ${res}`);
  })
  .catch((err) => {
    console.log(`작업 실패 : ${err}`);
  });
  
  // 100 출력 
  // 작업 성공 : 양수 출력

Promise 객체의 메서드인 then()과 catch()를 사용하게 되면 resolve를 수행했을 때 then()에서 전달한 결괏값을 콜백 함수에서 받아줄 수 있게 되며

 

reject를 수행했을 때 catch()에서 실패 내용을 콜백 함수에서 받을 수 있게 됩니다.

 

 

 


마지막으로 콜백 지옥을 탈출하는 방법을 예시 코드와 함께 살펴보겠습니다.

 

function taskA(a, b, cb) {
  setTimeout(() => {
    const res = a + b;
    cb(res);
  }, 3000);
}

function taskB(a, cb) {
  setTimeout(() => {
    const res = a * 2;
    cb(res);
  }, 1000);
}

function taskC(a, cb) {
  setTimeout(() => {
    const res = a * -1;
    cb(res);
  }, 2000);
}

taskA(3, 4, (a_res) => {
  console.log(`taskA : ${a_res}`);
  taskB(a_res, (b_res) => {
    console.log(`taskB : ${b_res}`);
    taskC(b_res, (c_res) => {
      console.log(`taskC : ${c_res}`);
    });
  });
});

위의 예시 코드처럼 이렇게 콜백이 안쪽으로 계속 길어지는 것을 콜백 지옥이라고 했습니다.

 

콜백 지옥을 해결하기 위해 taskA, taskB, taskC를 Promise를 이용하여 비동기 처리를 하게 수정해보겠습니다.

 

function taskA(a, b) {
  const executorA = (resolve, reject) => {
    setTimeout(() => {
      const res = a + b;
      resolve(res);
    }, 3000);
  };

  return new Promise(executorA);
}

function taskB(a) {
  const executorB = (resolve, reject) => {
    setTimeout(() => {
      const res = a * 2;
      resolve(res);
    }, 1000);
  };

  return new Promise(executorB);
}

function taskC(a) {
  const executorC = (resolve, reject) => {
    setTimeout(() => {
      const res = a * -1;
      resolve(res);
    }, 2000);
  };

  return new Promise(executorC);
}

함수 executor를 생성해주고 resolve와 reject를 이용해주게 되기 때문에

 

taskA, taskB, taskC의 매개 변수 cb, 즉 콜백 함수를 인자로 받는 부분을 제거를 해주게 됩니다.

 

이렇게 Promise 객체를 반환하게 변경해준 이유는 위에서 설명드린 것처럼 어떤 함수가 Promise 객체를 반환해준다는 것은 

 

해당 함수는 비동기 처리를 하고, 반환한 Promise 객체를 이용하여 비동기 처리의 결괏값을 메서드 then()과 catch()로 이용할 수 있게 

 

만들겠다는 의미입니다.

 

taskA(5, 2).then((a_res) => {
  console.log(`A RESULT : ${a_res}`);
  taskB(a_res).then((b_res) => {
    console.log(` B RESULT : ${b_res}`);
    taskC(b_res).then((c_res) => {
      console.log(`C RESULT : ${c_res}`);
    });
  });
});

하지만 콜백 지옥 여전히 존재하게 됩니다.

 

이는 then() 메서드를 콜백 함수를 사용하듯이 사용했기 때문입니다.

 

taskA(5, 2)
  .then((a_res) => {
    console.log(`A RESULT : ${a_res}`);
    return taskB(a_res);
  })
  .then((b_res) => {
    console.log(`B RESULT : ${b_res}`);
    return taskC(b_res);
  })
  .then((c_res) => {
    console.log(`C RESULT : ${c_res}`);
  });

taskA의 resolve 해주는 then() 메서드 뒤에 taskB를 return 해주게 변경해주었습니다.

 

이렇게 해주시면 then() 메서드에 전달된 콜백 함수가 수행되면서 마지막에 taskB를 호출하여 결괏값을 반환하게 됩니다.

 

나머지도 똑같이 변경해주었습니다. 

 

이렇게 then() 메서드를 계속해서 이어 붙이는 것을 then 체이닝이라고 합니다.

 

 

 

Async 와 Await

콜백 패턴으로 인한 콜백지옥을 피하기 위한 또 다른 방법으로 Async 와 Await가 있습니다.

 

const a = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(1);
      resolve();
    }, 1000);
  });
};

const b = () => console.log(2);

a().then(() => b());

해당 코드는 Promise 클래스를 사용한 순서가 보장되는 비동기 코드로, 순서 보장을 위해 then() 메서드 체이닝을 사용하고 있습니다.

 

 

const a = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(1);
      resolve();
    }, 1000);
  });
};

const b = () => console.log(2);

const wrap = async () => {
  await a(); // a 함수의 실행이 끝날 때까지 기다렸다가
  b(); // b 함수 실행
};

wrap();

then() 메서드를 사용하지 않고 비동기 함수 앞에 await을 붙이게 되면 해당 함수가 끝날 때 까지 기다렸다가 다음 코드가 실행되게 됩니다.

 

* await은 promise를 반환하는 함수 앞에만 사용이 가능합니다.

* await은 async로 감싼 함수 내부에서 사용이 가능합니다.

 

짧고 간단한 로직이긴 하지만 then() 메서드를 사용한 것보다 직관적으로 확인할 수 있지만, await은 async로 감싼 함수 내부에서만 사용이 가능하다는 단점이 있습니다.

 

* 많은 경우 로직 작성 시 함수 내부에서 작성하게 되기 때문에 async / await을 활용한 비동기 코드 제어를 많이 활용하게 됩니다.

 

 

fetch

[ fetch ] -  네트워크를 통해 리소스의 요청(Request) 및 응답(Response)을 처리할 수 있으며, Promise 인스턴스를 반환하고

첫 번째 인자로 주소, 두 번째 인자로 옵션을 전달받습니다.

 

해당 API에 fetch 함수를 이용하여 리소스 요청을 보내면 Promise 인스턴스를 반환하는 것을 확인할 수 있습니다.

 

때문에 fetch 함수에는 then 메서드를 사용할 수 있으며, then 메서드에 인자로 주어진 콜백함수의 res 라는 매개변수로 콘솔을 출력하면 Response 라는 이름의 객체가 출력되는 것을 확인할 수 있습니다.

 

해당 객체에서 데이터를 추출하기 위해서는  fetch 함수의 응답 결과에 json 메서드를 호출을 해주어야 하며 콘솔을 출력하면 pending(대기) 상태의 Promise 인스턴스가 반환된 것을 확인할 수 있습니다..

 

pending 상태의 Promise 인스턴스에 then 메서드로 json 이라는 매개변수로 콘솔을 출력하면 API로부터 전달 받은 결과를 확인할 수 있습니다.

 

fetch 함수에서도 async / await 사용이 가능하며, then 메서드를 사용한 결과와 동일한 결과를 확인할 수 있습니다.

 

fetch 함수의 두 번째 인자로 옵션을 전달할 수 있으며, 옵션은 아래와 같습니다.

  • method : 값으로 "GET", "POST", "DELETE", "PUT" 사용이 가능하며, "GET"은 어떠한 값을 얻을 때 사용합니다. 일반적인 경우 "GET"은 별도로 작성하지 않아도 작동이 됩니다.  method의 값으로는 서버에서 요구하는 값을 적절하게 전달해주어야 합니다.
  • headers : server로 전송하는 요청에 대한 정보를 담으며, 대표적으로 "Content-Type" : "application/json" 이라는 json이라는 데이터 포맷으로 통신을 한다는 정보를 담아 전송할 수 있습니다.
  • body :  server로 전송하는 요청에 대한 데이터를 담으며, 전송하는 데이터는 문자화를 시켜 전송해주어야합니다.

 

 

 

 

 

 

'JavaScript' 카테고리의 다른 글

Console  (0) 2022.12.22
Event 제어  (0) 2022.12.22
JavaScript 라이브러리  (0) 2022.12.06
map()  (0) 2022.10.01
Math.max() 와 Math.min()  (2) 2022.10.01