본문 바로가기
Study

21. JavaScript 비동기 프로그래밍: Promise | 웹 개발 기초

by 구구 구구 2024. 7. 21.
반응형

꽃 스타일, dall-e

 

JavaScript Using Promises: 비동기 작업을 더 쉽게 관리하는 방법

JavaScript에서 Promise를 사용하여 비동기 작업을 관리하는 방법을 알아봅니다. Promise 체이닝, 에러 처리, 중첩, 결합 방법 등을 다룹니다.

 

01. Promise란 무엇인가?

1) Promise의 기본 개념과 필요성

Promise는 비동기 작업의 성공 또는 실패를 나타내는 객체입니다. 비동기 작업이란 네트워크 요청, 파일 읽기, 타이머 설정 등 일정 시간이 지나야 완료되는 작업을 의미합니다. Promise는 이러한 비동기 작업을 더 간결하고 가독성 있게 관리할 수 있도록 도와줍니다.

Promise는 세 가지 상태를 가집니다:

  • 대기(Pending): 초기 상태, 비동기 작업이 아직 완료되지 않음.
  • 이행(Fulfilled): 비동기 작업이 성공적으로 완료됨.
  • 거부(Rejected): 비동기 작업이 실패함.

이 상태는 한 번 결정되면 변경되지 않습니다. Promise는 비동기 작업이 완료되었을 때 실행할 콜백 함수를 등록하여, 코드가 더 구조적이고 이해하기 쉽게 만듭니다.

2) 콜백 함수와 Promise 비교

비동기 작업을 처리하는 전통적인 방법은 콜백 함수를 사용하는 것입니다. 그러나 콜백 함수는 코드가 중첩되고 복잡해지는 "콜백 지옥"을 초래할 수 있습니다.

function getData(callback) {
  setTimeout(() => {
    callback('data received');
  }, 1000);
}

getData((data) => {
  console.log(data); // 'data received'
});
            

Promise를 사용하면 같은 작업을 더 간결하게 표현할 수 있습니다:

function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('data received');
    }, 1000);
  });
}

getData().then((data) => {
  console.log(data); // 'data received'
});
            

Promise는 콜백 함수보다 가독성이 높고 에러 처리가 용이합니다.

 

02. Promise 사용 방법

1) 기본적인 사용 예제

Promise는 new Promise 생성자로 생성할 수 있습니다. 생성자는 두 개의 콜백 함수, resolvereject를 인수로 받습니다. 비동기 작업이 성공하면 resolve를 호출하고, 실패하면 reject를 호출합니다.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true;
      if (success) {
        resolve('Data fetched successfully');
      } else {
        reject('Error fetching data');
      }
    }, 1000);
  });
}

fetchData().then((message) => {
  console.log(message); // 'Data fetched successfully'
}).catch((error) => {
  console.error(error);
});
            

2) 체이닝(Chaining) 기법

Promise는 체이닝을 통해 여러 비동기 작업을 순차적으로 처리할 수 있습니다. then 메서드는 새로운 Promise를 반환하므로, 다음 then 메서드를 체인으로 연결할 수 있습니다.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Step 1: Data fetched');
    }, 1000);
  });
}

function processData(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`${data}, Step 2: Data processed`);
    }, 1000);
  });
}

function saveData(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`${data}, Step 3: Data saved`);
    }, 1000);
  });
}

fetchData()
  .then((data) => processData(data))
  .then((data) => saveData(data))
  .then((finalData) => {
    console.log(finalData); // 'Step 1: Data fetched, Step 2: Data processed, Step 3: Data saved'
  })
  .catch((error) => {
    console.error(error);
  });
            

3) 에러 처리 방법

Promise의 에러 처리는 catch 메서드를 사용하여 체인의 마지막에 한 번만 작성해도 됩니다. 이는 콜백 함수의 에러 처리 방식보다 간단하고 직관적입니다.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = false;
      if (success) {
        resolve('Data fetched successfully');
      } else {
        reject('Error fetching data');
      }
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error(error); // 'Error fetching data'
  });
            

 

03. Promise 생성하기

1) Promise 생성자 사용법

Promise는 new Promise 생성자를 사용하여 생성할 수 있습니다. 이 생성자는 두 개의 콜백 함수, resolvereject를 인수로 받습니다. 비동기 작업이 성공하면 resolve를 호출하고, 실패하면 reject를 호출합니다.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true;
      if (success) {
        resolve('Data fetched successfully');
      } else {
        reject('Error fetching data');
      }
    }, 1000);
  });
}

fetchData().then((message) => {
  console.log(message); // 'Data fetched successfully'
}).catch((error) => {
  console.error(error);
});
            

2) 구형 콜백 API를 Promise로 래핑하기

구형 콜백 API를 Promise로 래핑하면 기존의 콜백 기반 코드를 Promise 기반 코드로 변환할 수 있습니다. 이는 코드를 더 읽기 쉽고 유지보수하기 쉽게 만듭니다.

const fs = require('fs');

function readFileAsync(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

readFileAsync('example.txt')
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.error(err);
  });
            

 

04. 고급 사용법

1) 중첩(Nesting)과 오류 범위

Promise 체인을 중첩하여 더 복잡한 비동기 작업을 처리할 수 있습니다. 중첩된 then 블록은 특정 범위 내에서 발생하는 오류를 처리할 수 있습니다.

function criticalTask() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('Critical task done'), 1000);
  });
}

function optionalTask() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject('Optional task failed'), 500);
  });
}

function additionalTask() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('Additional task done'), 700);
  });
}

criticalTask()
  .then((result) => {
    console.log(result);
    return optionalTask().then(additionalTask).catch((e) => {
      console.warn(e);
      return 'Optional task skipped';
    });
  })
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error('Critical task failed', error);
  });
            

2) Promise 결합: Promise.all, Promise.race, Promise.allSettled, Promise.any

여러 Promise를 결합하여 동시에 실행할 수 있습니다.

  • Promise.all: 모든 Promise가 이행될 때까지 기다립니다. 하나라도 거부되면 전체가 거부됩니다.
Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log('All promises resolved:', results);
  })
  .catch((error) => {
    console.error('One of the promises failed:', error);
  });
            
  • Promise.race: 가장 먼저 이행되거나 거부된 Promise의 결과를 반환합니다.
Promise.race([promise1, promise2, promise3])
  .then((result) => {
    console.log('First promise resolved:', result);
  })
  .catch((error) => {
    console.error('First promise rejected:', error);
  });
            
  • Promise.allSettled: 모든 Promise가 완료될 때까지 기다립니다(이행 또는 거부 포함).
Promise.allSettled([promise1, promise2, promise3])
  .then((results) => {
    results.forEach((result) => {
      if (result.status === 'fulfilled') {
        console.log('Promise fulfilled:', result.value);
      } else {
        console.log('Promise rejected:', result.reason);
      }
    });
  });
            
  • Promise.any: 하나라도 이행되면 해당 Promise의 결과를 반환합니다. 모든 Promise가 거부되면 에러를 반환합니다.
Promise.any([promise1, promise2, promise3])
  .then((result) => {
    console.log('First fulfilled promise:', result);
  })
  .catch((error) => {
    console.error('All promises were rejected:', error);
  });
            

3) 비동기 작업의 취소

Promise 자체는 취소 기능을 제공하지 않지만, AbortController를 사용하여 비동기 작업을 취소할 수 있습니다.

const controller = new AbortController();
const signal = controller.signal;

function fetchData(url) {
  return fetch(url, { signal });
}

fetchData('https://api.example.com/data')
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => {
    if (error.name === 'AbortError') {
      console.log('Fetch aborted');
    } else {
      console.error('Fetch error:', error);
    }
  });

// 1초 후에 요청 취소
setTimeout(() => {
  controller.abort();
}, 1000);
            

 

05. 타이밍과 보장

1) 태스크 큐와 마이크로태스크 큐

JavaScript의 이벤트 루프는 태스크 큐(Task Queue)와 마이크로태스크 큐(Microtask Queue)로 구성됩니다. 이 두 큐는 비동기 작업을 처리하는 방식이 다릅니다.

  • 태스크 큐(Task Queue): setTimeout, setInterval, I/O 작업 등의 콜백 함수가 여기에 추가됩니다.
  • 마이크로태스크 큐(Microtask Queue): Promise의 콜백 함수나 process.nextTick(Node.js) 등의 작업이 여기에 추가됩니다.

마이크로태스크는 태스크보다 우선적으로 처리됩니다. 이는 마이크로태스크가 현재 실행 중인 태스크가 완료되면 바로 실행됨을 의미합니다.

console.log('Start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise');
});

console.log('End');

// 출력 결과:
// Start
// End
// Promise
// setTimeout
            

위 예제에서 Promise 콜백은 마이크로태스크 큐에 추가되어 setTimeout 콜백보다 먼저 실행됩니다.

2) 비동기 작업과 이벤트 루프

이벤트 루프(Event Loop)는 JavaScript 런타임의 핵심 요소로, 비동기 작업을 처리하고 실행 컨텍스트를 관리합니다. 이벤트 루프는 콜 스택(Call Stack)이 비어 있을 때 태스크 큐나 마이크로태스크 큐에서 작업을 가져와 실행합니다.

function asyncTask() {
  console.log('Task started');
  
  setTimeout(() => {
    console.log('Task completed');
  }, 1000);
}

console.log('Before async task');
asyncTask();
console.log('After async task');

// 출력 결과:
// Before async task
// Task started
// After async task
// Task completed
            

위 예제에서 asyncTask 함수는 태스크 큐에 작업을 추가하고, 이벤트 루프는 1초 후에 해당 작업을 실행합니다.

 

06. Async/Await

1) Async/Await 기본 사용법

asyncawait는 Promise 기반의 비동기 코드를 더 간결하고 가독성 있게 작성할 수 있도록 도와줍니다. async 함수는 항상 Promise를 반환하며, await 키워드는 Promise가 이행될 때까지 기다립니다.

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

fetchData();
            

2) Promise와의 차이점 및 변환 방법

async/await는 Promise 체인의 대안으로, 코드를 더 직관적이고 읽기 쉽게 만들어줍니다. Promise 체인을 async/await로 변환하는 방법을 살펴보겠습니다.

Promise 체인 예제:

function fetchData() {
  return fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => {
      console.log(data);
    })
    .catch(error => {
      console.error('Error fetching data:', error);
    });
}

fetchData();
            

async/await로 변환된 예제:

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

fetchData();
            

Promise 체인은 thencatch 메서드를 사용하여 비동기 작업을 처리합니다. 반면 async/await는 동기 코드처럼 보이지만, 내부적으로는 여전히 비동기적으로 동작합니다. 이를 통해 비동기 작업의 흐름을 더 쉽게 이해할 수 있습니다.

async/await의 주요 차이점:

  • 더 간결하고 직관적인 코드 작성이 가능
  • try/catch 블록을 사용하여 에러 처리
  • 동기 코드와 유사한 구조로 가독성 향상

async/await는 Promise 기반의 비동기 작업을 더 쉽게 작성할 수 있도록 도와줍니다. 이를 통해 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다.


관련된 다른 글도 읽어보시길 추천합니다

 

2024.07.05 - [Study] - 09. JavaScript 변수와 데이터 타입 이해하기 | 웹 개발 기초

 

09. JavaScript 변수와 데이터 타입 이해하기 | 웹 개발 기초

JavaScript 변수 선언 및 데이터 타입 이해하기 01. 변수 선언: var, let, const1) var: 함수 스코프를 가지며 재선언과 재할당이 가능합니다.var greeting = "Hello";var greeting = "Hi"; // 재선언 가능greeting = "Hey"; /

guguuu.com

2024.07.05 - [Study] - 10. JavaScript 연산자와 조건문 이해하기 | 웹 개발 기초

 

10. JavaScript 연산자와 조건문 이해하기 | 웹 개발 기초

JavaScript 연산자와 조건문 이해하기 01. 서론1) JavaScript의 중요성JavaScript는 웹 개발에서 핵심적인 역할을 하는 프로그래밍 언어입니다. 클라이언트 측 스크립트로 작동하며, HTML과 CSS와 함께 웹

guguuu.com

2024.07.10 - [Study] - 11. JavaScript 반복문 완벽 가이드 | 웹 개발 기초

 

11. JavaScript 반복문 완벽 가이드 | 웹 개발 기초

JavaScript 반복문: for, while, do...while, for...in, for...of 문과 break, continue 사용법 1. 서론1.1 JavaScript에서 반복문의 중요성반복문은 JavaScript에서 필수적인 기능으로, 동일한 작업을 여러 번 수행할 때 사

guguuu.com


읽어주셔서 감사합니다

공감은 힘이 됩니다

 

:)

반응형

TOP

Designed by 티스토리