[JavaScript] 비동기 처리

이건·2023년 12월 28일
0

Front-end

목록 보기
15/15
post-thumbnail

동기와 비동기

JavaScript의 실행 컨텍스트는 동기적(synchronous)이며, 코드는 작성된 순서대로 실행된다. 그러나 JavaScript는 비동기적(asynchronous) 처리를 지원하는 몇 가지 기능을 제공한다.
비동기적 처리는 특정 코드를 실행 완료를 기다리지 않도 다음 코드를 실행하는 방식을 말한다. 예를 들어, 웹 어플리케이션에서 서버로 데이터를 요청하면서 동시에 다른 작업을 계속 진행하는 것이 비동기 처리의 한 예시다.

  • 동기(Synchronous): 코드가 작성된 순서에 따라 한 줄씩 차례대로 실행된다. 한 작업이 끝나야 다음 작업이 시작한다.
console.log('1');
console.log('2');
console.log('3');
  • 비동기(Asynchronous): 특정 코드의 실행을 지연시키는 경우, 나머지 코드는 대기하지 않고 즉시 실행된다.
console.log('1');
setTimeout(() => console.log('2'), 1000); // 1초 후 '2' 출력
console.log('3'); // 즉시 '3' 출력

콜백 함수(Callback Function)

콜백 함수는 다른 함수에 인자로 넘겨지는 함수로, 그 함수의 실행이 끝난 후 실행된다. 콜백 함수는 주로 비동기 작업의 완료 후 필요한 작업을 수행하기 위해 사용된다.

  • 동기 콜백(Synchrounous Callback): 즉시 실행되는 콜백 함수
function printImmediately(print) {
  print();
}
printImmediately(() => console.log('hello')); // 즉시 'hello' 출력
  • 비동기 콜백(Asynchronous Callback): 특정 작업(예: 시간 지연, 네트워크 요청) 후에 실행되는 콜백 함수이다.
function printWithDelay(print, timeout) {
  setTimeout(print, timeout);
}
printWithDelay(() => console.log('async callback'), 2000); // 2초 후 'async callback' 출력

콜백 지옥(Callback Hell)

콜백 지옥은 콜백 함수를 과도하게 중첩 사용하여 코드의 가독성과 유지 보수성이 떨어지는 상황을 말한다. 코드가 계단처럼 보이며, 각 단계마다 중첩된 콜백 함수가 존재한다.

  • 콜백 지옥 예
class UserStorage {
  loginUser(id, password, onSuccess, onError) {
    setTimeout(() => {
      if (
        (id === 'ellie' && password === 'dream') ||
        (id === 'coder' && password === 'academy')
      ) {
        onSuccess(id);
      } else {
        onError(new Error('not found'));
      }
    }, 2000);
  }

  getRoles(user, onSuccess, onError) {
    setTimeout(() => {
      if (user === 'ellie') {
        onSuccess({ name: 'ellie', role: 'admin' });
      } else {
        onError(new Error('no access'));
      }
    }, 1000);
  }
}

const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your passrod');
userStorage.loginUser(
  id,
  password,
  user => {
    userStorage.getRoles(
      user,
      userWithRole => {
        alert(
          `Hello ${userWithRole.name}, you have a ${userWithRole.role} role`
        );
      },
      error => {
        console.log(error);
      }
    );
  },
  error => {
    console.log(error);
  }
);

위 코드는 먼저 사용자 로그인을 시도하고(loginUser), 그 후 사용자 역할을 가져오는 (getRoles) 과정을 거친다. 각 단계마다 콜백 함수를 사용하여 다음 단계를 실행한다. 이러한 중첩은 코드의 복잡성을 증가시키고, 에러 처리와 유지 보수를 어렵게 만든다.


Promise

Promise는 JavaScript에서 비동기 작업을 편리하게 처리할 수 있도록 하는 객체다. 주로 네트워크 요청이나 파일 읽기와 같이 시간이 걸리는 작업을 처리할 때 사용된다. Promise는 다음 세 가지 상태를 가진다.

  1. Pending(대기): 초기 상태, 아직 결과가 정해지지 않은 상태
  2. Fulfilled(이행): 작업이 성공적으로 완료된 상태
  3. Rejected(거부): 작업이 실패한 상태

Producer

Promise 객체는 new Promise로 생성되며, 이 때 실행자(executor) 함수가 자동으로 실행된다. 이 함수는 resolvereject 두 가지 인자를 받는다.

const promise = new Promise((resolve, reject) => {
  // doing some heavy work (network, read files)
  console.log('doing something...');
  setTimeout(() => {
    resolve('ellie');
    // reject(new Error('no network'));
  }, 2000);
});
  • 여기서 setTimeout은 2초 후에 resolve 함수를 호출하여 Promise를 이행(fulfilled) 상태로 만든다. resolved가 호출되면 Promise는 성공적으로 결과 값을 가진 상태가 된다.
  • 반대로 reject를 호출하면 Promise는 실패하여 거부(rejected) 상태가 된다.

Consumer

  • then: Promise가 성공적으로 수행되어 이행 상태가 되면 then이 호출된다. then은 Promise가 반환한 데이터를 받아 처리한다.
  • catch: Promise가 실패하여 거부 상태가 되면 catch가 호출된다. catch는 오류를 받아 처리한다.
  • finally: Promise의 성공 여부와 관계없이 항상 실행된다.
promise
  .then(value => {
    console.log(value); // 'ellie'
  })
  .catch(error => {
    console.log(error);
  })
  .finally(() => {
    console.log('finally');
  });

Promise Chaining

여러 개의 Promise를 연결하여 순차적으로 처리할 수 있다.

const fetchNumber = new Promise((resolve, reject) => {
  setTimeout(() => resolve(1), 1000);
});

fetchNumber
  .then(num => num * 2)
  .then(num => num * 3)
  .then(num => {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(num - 1), 1000);
    });
  })
  .then(num => console.log(num)); // 5

Error Handling

.catch()를 사용하여 Promise 체인 중 발생한 오류를 캐치하고 처리할 수 있다.

const getHen = () => new Promise((resolve, reject) => {
  setTimeout(() => resolve('🐓'), 1000);
});
const getEgg = hen => new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error(`error! ${hen} => 🥚`)), 1000);
});
const cook = egg => new Promise((resolve, reject) => {
  setTimeout(() => resolve(`${egg} => 🍳`), 1000);
});

getHen()
  .then(getEgg)
  .catch(error => {
    return '🍞';
  })
  .then(cook)
  .then(console.log)
  .catch(console.log);

여기서 getEgg에서 오류가 발생하면 catch가 실행되어 '🍞'을 반환하고, cook은 이를 받아 계란 대신 빵을 요리한다.


Async & Await

asyncawait은 JavaScript에서 비동기 작업을 간결하고 명확하게 표현할 수 있는 문법이다. 이들은 Promise를 더 쉽게 사용할 수 있도록 도와준다.

Async

  • async 키워드를 함수 앞에 사용하면, 해당 함수는 항상 Promise를 반환한다.
  • 함수 내부에서 일반 값으로 반환(return)하면, 이 값은 Promise로 감싸진 값으로 변환된다.
async function fetchUser() {
  return 'ellie'; // Promise로 감싸진 'ellie' 반환
}

const user = fetchUser();
user.then(console.log); // ellie

Await

  • await 키워드는 async 함수 내부에서만 사용할 수 있다.
  • awaitPromise가 처리될 때까지 함수 실행을 일시 중지하고, Promise의 결과 값을 반환한다.
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function getApple() {
  await delay(2000); // 2초 기다림
  return '🍎';
}

async function getBanana() {
  await delay(1000); // 1초 기다림
  return '🍌';
}

async function pickFruits() {
  const apple = await getApple();
  const banana = await getBanana();
  return `${apple} + ${banana}`;
}

pickFruits().then(console.log); // 🍎 + 🍌

Error Handling

  • try...catch: async 함수 내에서 await 키워드를 사용할 때, try...catch 구분을 활용하여 동기 코드에서의 오류 처리와 유사하게 오류 처리를 할 수 있다.
  • fianlly: finally 블록은 try...catch와 함께 사용되어, 성공이든 실패든 상관없이 특정 코드를 실행할 수 있게 한다. 예를 들어, 로딩 상태를 관리할 때 유용할 수 있습니다.
async function fetchData() {
  try {
    const data = await fetchSomeData();
    console.log(data);
  } catch (error) {
    console.error('오류 발생:', error);
  } finally {
    console.log('데이터 요청 완료'); // 항상 실행
  }
}

주의 사항

  • await 키워드를 사용하는 모든 비동기 작업은 오류가 발생할 가능성이 있으므로, try...catch 구문으로 감싸는 것이 좋다.
  • 비동기 작업이 여러 개인 경우, 각각의 작업을 별도의 try...catch 블록으로 감쌀 수도 있고, 전체 작업을 하나의 try...catch 블록으로 감싸서 오류를 한 곳에서 처리할 수도 있다.
  • 오류를 적절히 처리하지 않으면, 프로그램이 예상치 못한 상태가 되거나 중단될 수 있으므로 항상 주의 깊게 오류를 관리해야 한다.

유용한 API

  • promise.all: 여러 Promise를 병렬로 처리하고, 모든 Promise가 완료되면 결과를 배열로 반환한다.
function pickAllFruits() {
  return Promise.all([getApple(), getBanana()]).then(fruits =>
    fruits.join(' + ')
  );
}
pickAllFruits().then(console.log); // 🍎 + 🍌
Promise
  • Promise.race: 여러 Promise 중 가장 먼저 완료되는 하나의 결과만 반환한다.
function pickOnlyOne() {
  return Promise.race([getApple(), getBanana()]);
}

pickOnlyOne().then(console.log); // 🍌 또는 🍎 (둘 중 빠른 것)

콜백 지옥 async와 await로 해결하기

위의 콜백 지옥의 예를 asyncawait으로 해결해보자.

class UserStorage {
  loginUser(id, password) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (
          (id === 'ellie' && password === 'dream') ||
          (id === 'coder' && password === 'academy')
        ) {
          resolve(id);
        } else {
          reject(new Error('not found'));
        }
      }, 2000);
    });
  }

  getRoles(user) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (user === 'ellie') {
          resolve({ name: 'ellie', role: 'admin' });
        } else {
          reject(new Error('no access'));
        }
      }, 1000);
    });
  }

  async getUserWithRole(user, password) {
    const id = await this.loginUser(user, password);
    const role = await this.getRoles(id);
    return role;
  }
}

const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your passrod');
userStorage
  .loginUser(id, password)
  .then(userStorage.getRoles)
  .then(user => alert(`Hello ${user.name}, you have a ${user.role} role`))
  .catch(console.log);

userStorage
  .getUserWithRole(id, password) //
  .catch(console.log)
  .then(console.log);

0개의 댓글