React - 자바스크립트 비동기

milkbottle·2023년 12월 9일
0

React

목록 보기
2/33
post-thumbnail

비동기란 무엇인가

우리가 인터넷에 정보를 검색하면 그 결과를 기다리고, 결과를 로딩하면 화면에 뷰가 출력되는 것을 경험해본 적이 있을 것이다.
앞의 과정에 종속되어서 결과를 기다리고, 뒤의 과정이 실행되는 것은 비일비재하다.

즉 위와 같은 일이 빈번하다는 것이다.
급식을 먹으려면 줄을서야하고, 내 앞의 사람이 다 사라지만 식판을 꺼내들고, 식판을 들었으면 배식받고...

하지만, 굳이 종속적이지 않다면 동시에 실행하면 어떨까?

이렇게 말이다. 숨을 쉬면서, 벨로그 글을 쓰고 이 두가지일은 종속성이 없기때문에 동시에 실행이 가능하다.
그러면 실행시간도 단축되고 얼마나 편할까? 그래서 자바스크립트에서는 비동기라는 개념을 도입하였다.

그런데 자바스크립트는 싱글스레드잖아!

맞다. 자바스크립트는 싱글스레드 언어이다. 하나의 시간대에 한 종류의 일을 처리한다는 말이다.
수강신청 사이트를 예로 들면, 버튼을 누르고 이에 대한 결과를 기다리고, 화면을 갱신해야한다. 그런데 현재 시간은 계속 1초마다 흐르는 것을 표현해야한다. 두 개를 동시에 어떻게 하는 것일까?
어떻게 이를 가능하게 한 것일까?

비동기를 위한 장치들

콜스택(Call Stack)

자바스크립트는 요청이 들어오면 콜스택에 저장한다. 그리고 그 스택에 있는 요청을 Pop 하여 처리한다. 싱글쓰레드이기때문에, 콜스택은 1개이다.

태스크 큐(Task Queue)

이벤트가 발생할 때 실행될 콜백 함수가 저장되는 공간

마이크로태스크 큐(Microtask Queue)

콜백 함수 중 process.nextTick(), Promise객체의 callback, async function, queueMicroTask 에 해당하는 것들이 적재된다.

매크로태스크 큐(Macrotask Queue)

콜백 함수 중 setTimeout() 특정 시간 이후 실행, setInterval() 특정 시간을 주기로 반복 실행 , setImmediate() 이거는 아직 잘 모르겠다...
이 3가지에 대한 것을 매크로태스크 큐에 저장한다고 한다.

이벤트 루프(Event Loop)

위의 콜스택과 2가지의 태스크 큐를 합친 총 3개를 감시한다.
감시하다가 콜스택이 비어있으면 태스크 큐의 일을 콜스택으로 넘겨준다.

비동기 참고 사진자료

function fifth() { }
function fourth() { fifth() }
function third() { fourth() }
function second() { third() }
function first() { second() }

first();

function promiseFunc(){
  return new Promise(function(res, rej)     
  {// Doing something!
    res(1);
  });
}
console.log("2");
promiseFunc().then(console.log);
console.log("3")

Promise

이런 비동기 작업을 처리하기 위해 Promise를 만들었다.

const promise = new Promise((resolve, reject) => {
  if (/* 비동기 성공 시 */) {
    resolve('resolve');
  }
  else { /* 비동기 실패 시 */
    reject('reject');
  }
});

상태

pending: 비동기 처리가 아직 수행되지 않은 상태
fulfilled: 비동기 처리가 수행된 상태 (성공)
rejected: 비동기 처리가 수행된 상태 (실패)
settled: 비동기 처리가 수행된 상태 (성공 또는 실패)

후속 처리 메소드

단순히 프로미스만으로 로직을 구현하면, 이후에 어떤 행동을 해야할지 정의할 필요가 있다.
예를 들어, https://api.test.io/index.html 에 GET 요청을 한다고 하자.

const req = new Promise((resolve, reject) => {
  var url = "https://api.test.io/index.html";
  var res = 요청하는함수(url);
  if (res == 성공) {
    resolve('resolve');
  }
  else {
    reject('reject');
  }
});

여기서 성공을 했을 때와, 실패를 했을 때의 핸들링을 프로미스 안에서 할 수도 있겠지만, 후속 처리 메소드를 통해도 구현이 가능하다.

then

then 메소드는 두 개의 콜백 함수를 파라미터로 가진다.
첫 번째 콜백 함수는 성공(fulfilled, resolve 함수가 호출된 경우)시에 실행된다.
두 번째 콜백 함수는 실패(rejected, reject 함수가 호출된 경우)시에 실행된다.
then 메소드는 기본적으로 프로미스를 반환한다.

catch

catch 메소드는 비동기 처리 혹은 then 메소드 실행 중 발생한 에러(예외)가 발생하면 호출된다.
catch 메소드 역시 프로미스를 반환한다.

const req = new Promise((resolve, reject) => {
  var url = "https://api.test.io/index.html";
  var res = 요청하는함수(url);
  if (res == 성공) {
    resolve('resolve');
  }
  else {
    reject('reject');
  }
});

req.then(
    (data) => {
      console.log('성공: ', data);
    },
    (error) => {
      console.log('실패: ', error);
    }
  )
  .catch((error) => {
    console.error('에러 핸들링: ', error);
  });

then에는 2개의 함수가 들어갈 수도, 1개가 들어갈 수도 있다. 1개라면 성공에 대한 메시지(Promise가 resolve), 2개라면 실패에 대한 메시지(Promise가 reject)가 담길 수 있다.
위 코드에서 요청하는함수(url)에서 어떠한 에러가 throw 된다면, catch 메소드에서 해당 오류를 잡아 핸들링을 할 수 있다.

콜백지옥


개발자들이라면 자주본 짤...
아도겐을 처맞은 코드이다. 그런데 이를 Promise로 해결할 수 있다고하지만, 사실 Promise를 사용해도 then을 남용하면.....

function asyncOperation(value) {
  return new Promise((resolve) => {
    // 간단한 비동기 작업 시뮬레이션
    setTimeout(() => {
      console.log('작업 완료:', value);
      resolve(value + 1);
    }, 1000);
  });
}

// 무한정 실행되는 프로미스 지옥...
asyncOperation(1)
  .then((result) => {
    return asyncOperation(result);
  })
  .then((result) => {
    return asyncOperation(result);
  })
  .then((result) => {
    return asyncOperation(result);
  })
  .then((result) => {
    // 이어서 계속해서 then을 쌓을 수 있음
    return asyncOperation(result);
  })
  // ...

// 이어지는 then 계속 쌓는 부분은 생략

가독성이 엄청 떨어진다.... 이를 해결하기 위해 async/await가 등장했다.
참고로 위는 callback hell의 중 하나인 promise hell이라고 부른다.

async/await

async는 태스크큐에 작업이 올라가는 코드가 존재하는 함수에 키워드로 사용한다.
await는 해당 작업을 기다리는 역할을 한다.

또한 async로 선언한 함수는 자동으로 Promise 객체를 반환한다.

async function f() {
  return 1;
}

f().then(alert); // 1
async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1

위 두 코드는 똑같이 Promise 객체를 반환하기 때문에 후속 처리 메소드인 then의 사용이 가능하다.

예제

// 비동기로 데이터를 가져오는 함수
function fetchData(url) {
  return new Promise((resolve, reject) => {
    // 간단한 비동기 작업 시뮬레이션
    setTimeout(() => {
      const data = `Data from ${url}`;
      resolve(data);
    }, 1000);
  });
}

// 비동기 함수를 사용하는 함수
async function fetchDataAndPrint() {
  try {
    // 비동기로 데이터를 가져오기 위해 await 사용
    const result1 = await fetchData('https://example.com/api/data1');
    console.log(result1);

    // 두 번째 비동기 작업, 여기서도 await 사용
    const result2 = await fetchData('https://example.com/api/data2');
    console.log(result2);

    // 세 번째 비동기 작업
    const result3 = await fetchData('https://example.com/api/data3');
    console.log(result3);

    // 이어지는 작업들...
  } catch (error) {
    console.error('에러 발생: ', error);
  }
}

// fetchDataAndPrint 함수 호출
fetchDataAndPrint();

요청을 하는 fetchData(url) 함수를 async로 선언된 fetchDataAndPrint() 함수가 await를 하며 요청하는 것을 볼 수 있다.
이렇게 되면, then이나 catch없이도 await 만으로 표현이 가능하다.

화살표 함수로 괄호가 더 생겨서 못생겨진 코드를 안 봐도 된다는 말이다!!

그렇지만 적재적소에 async/await와 then/catch를 사용해야한다.

참고자료

0개의 댓글