[NodeJS] JavaScript Promise & async/await

Onam Kwon·2023년 5월 20일
0

Node JS

목록 보기
23/25

Promise & async/await

  • 비동기 코드 핸들링:
    • JavaScriptPromiseasync/await 은 비동기 코드를 동기적으로 사용하기 위해 사용됩니다. 이를 사용하므로 인해 nested callbacks (콜백지옥)을 방지해 주며 가독성 있는 코드를 만들도록 도와줍니다.
  • 에러 핸들링: 두가지 방법 모두 에러 핸들링의 사용에도 도움을 줍니다.
    • Promise: .catch() 메소드를 이용해 에러를 핸들할 수 있습니다.
    • async/await: try-catch 블럭을 이용해 에러를 핸들할 수 있습니다.

callback

  • JavaScript에는 비동기 함수가 포함되어 있으며 비동기 함수는 작업을 완수하는데 시간이 걸리기 때문에 해당 작업이 수행 된 후 따로 처리를 해 주어야 합니다.
  • 이를 위한 해결 방법으로 callback 함수라는 개념이 도입되었습니다.
    • callback 함수란 특정 함수가 실행 된 후 바로 이어서 실행되는 함수를 말합니다. 이를 이용해 비동기 함수의 수행이 완료 된 후 콜백 함수로 등록해 놓은 함수가 실행 되게 처리할 수 있습니다.
    • 하지만 이 callback 함수에도 한가지 문제점이 존재하는데 여러개의 콜백 함수가 겹치면 가독성이 아주 나빠진다는 단점이 존재했습니다. 이를 nested callback, 콜백 지옥 이라고 표현합니다.
// Example: Reading a file using nested callbacks

// Read file asynchronously
fs.readFile('file.txt', 'utf8', function(err, data) {
  if (err) {
    console.error('Error reading file:', err);
  } else {
    // Perform some operation with the file data
    processData(data, function(err, result) {
      if (err) {
        console.error('Error processing data:', err);
      } else {
        // Write the result to another file
        fs.writeFile('output.txt', result, function(err) {
          if (err) {
            console.error('Error writing file:', err);
          } else {
            console.log('File write successful!');
          }
        });
      }
    });
  }
});
  • 위는 흔히 말하는 콜백 지옥, nested callbacks를 표현한 코드입니다.
  • 위 코드의 특징으로는 여러개의 콜백이 중첩되어 있어 읽기에도 어렵고 코드를 수정하는데도 어려움을 줍니다.
  • 이런 콜백지옥을 해결하기 위한 개념으로 Promise 객체가 도입되었습니다.

Promise

  • JavaScriptPromise 객체는 비동기 작업을 다루는데 사용합니다.
  • Promise 객체는 비동기 작업의 성공 또는 실패의 결과값을 나중에 받을 수 있는 대리자 역할을 합니다.
  • Promise 객체는 다음과 같은 세가지 상태를 가질 수 있습니다.
    • pending(대기): 객체의 초기 상태로 비동기 작업이 아직 완료 되지 않은 상태입니다(작업중). 이후 아래 두가지 상태중 하나로 변합니다.
    • fulfilled(이행): 비동기 작업이 성공적으로 완료된 상태입니다.
    • rejected(거부): 비동기 작업이 실패한 상태입니다. 오류가 있음을 나타냅니다.
  • Promises는 비동기 코드로 작업할 수 있는 깨끗하고 일관된 방법을 제공하여 비동기 작업에 대해 더 쉽게 쓰고, 읽고, 추론할 수 있도록 도와줍니다. 콜백 지옥과 복잡한 중첩 코드로 이어질 수 있는 콜백 함수에 의존하는 대신, Promises를 사용하면 비동기 작업을 함께 연결하여 성공 또는 실패를 보다 선형적이고 순차적으로 처리할 수 있습니다.
const myPromise = new Promise((resolve, reject) => {
  // Doing some asynchronous task here..
  const data = fetch('URL_WILL_BE_HERE');
  
  // Error handling..
  if(data) {
    // When the data properly exists.
    resolve(data);
  } else {
    // When the data does not exist.
    reject('Error');
  }
});
  • 위의 코드는 기본적인 Promise 객체의 사용법 입니다.
  • 비동기 작업을 실행 후 해당 결과를 data 변수에 저장합니다.
    • 만약 data 변수의 값이 존재한다면 resolve() 성공 메소드를 호출해 data값을 반환합니다.
    • 만약 data 변수의 값이 존재하지 않는다면 reject() 실패 메소드를 호출해 에러를 반환합니다.
myPromise
  .then((data) => { 
    // resolve() method from the above passes the argument as a parameter in here.
    console.log('Data:', data); 
  })
  .catch((err) => { 
    // reject() method from the above passes the argument as a paramter in here.
    console.error('Error:', err); // Error: Error
  })
  .finally(() => {
    // Once then() method or catch() method are done, this method is automatically performed.
    console.log('The end.');  // The end.
  });
  • Promise 객체의 .then(), catch() 그리고 finally() 메소드를 이용해 에러 핸들링을 진행할 수 있습니다.
  • 에러가 없을 땐 resolve() 함수를 호출해 .then() 메소드로 인자를 전달할 수 있으며
  • 에러가 있을 땐 reject() 함수를 호출해 .catch() 메소드로 이동할 수 있습니다.
  • 위의 상황이 모두 종료된 후 마지막으로 .finally() 메소드를 호출할 수 있습니다.
    • 해당 메소드는 에러의 유무와 상관없이 마지막에 호출 가능한 함수입니다.
function myPromise() {
  return new Promise((resolve, reject) => {
    const data = fetch('URL');
    if(data) {
      resolve(data);
    } else {
      reject('Error');
    }
  });
}

myPromise()
  .then((data) => {
    // first then method.
  })
  .then((data) => {
    // second then method.
  })
  .catch((err) => {
    // first catch method.
  })
  .finally(() => {
      // final method.
  });
  • 일반적으로는 위와 같은 방법으로 함수로 감싸서 사용합니다.
// Read file asynchronously
const readFile = (path) => {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
};

// Process data
const processData = (data) => {
  return new Promise((resolve, reject) => {
    // Perform some operation with the data
    // ...

    // Simulating asynchronous operation
    setTimeout(() => {
      resolve('Processed data');
    }, 1000);
  });
};

// Write file asynchronously
const writeFile = (path, data) => {
  return new Promise((resolve, reject) => {
    fs.writeFile(path, data, (err) => {
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
  });
};

// Usage
readFile('file.txt')
  .then((data) => processData(data))
  .then((result) => writeFile('output.txt', result))
  .then(() => {
    console.log('File write successful!');
  })
  .catch((err) => {
    console.error('Error:', err);
  });
  • 위의 코드는 Promise 객체를 적용하여 처음 코드를 개선한 코드입니다.
  • 맨 아래의 readFile 함수를 보면 이전 상태의 nested callback이 사라지고 Promise 객체의 .then() 메소드를 사용해 좀 더 직관적으로 함수의 흐름을 읽을 수 있게 되었고, .catch() 메소드를 사용해 에러 핸들링을 진행 하였습니다.
  • 하지만 이를 적용해도 처음 보다는 개선 되었지만 여러개의 ,then 메소드의 사용은 가독성을 해칩니다.
    • 이를 해결하기 위해 나온 async/await 라는 문법도 존재합니다.

async/await

  • Promise 객체는 callback 함수의 콜백지옥을 해결하기 위해 나타났지만 .then() 메소드의 많은 사용은 프로미스 지옥을 만들어냅니다.
  • 이러한 문제를 해결하기 위해 나타난 async/await 이라는 문법도 존재합니다.
    • ECMAScript 2017 (ES8)부터 사용 가능한 기능으로 Promise 객체의 잦은 .then() 메소드로 인한 코드 가독성 저하를 해결하기 위해 사용합니다.
    • 하지만 async/awaitPromise를 대체하지 않으며 이를 기반으로 작동합니다.
// asynchronous function.
function getTimeT() {
  const t = 1000;
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(t);
    }, t);
  });
}

async function asyncFunction() {
  try {
    var t = await getTimeT();
    console.log('t value is:', t); // t value is: 1000
  } catch(err) {
    throw new Error(err);
  }
}

asyncFunction();
  • 위 코드는 기본적인 async/await 사용법 입니다.
    • getTimeT() 함수는 내부에 Promise 객체를 가지고 있으며 setTimeout() 메소드를 포함합니다. 이는 1초 후에 1000을 리턴합니다. setTimeout() 메소드는 비동기 함수 이므로 getTimeT() 함수는 비동기 함수입니다.
    • 이 함수를 호출할 때 await 키워드를 붙혀 주고 해당 범위의 함수에 async 키워드를 붙혀 주는 방법을 통해 비동기 함수를 기다릴 수 있습니다.
  • 아래는 위에서 배운 async/await을 원래의 예시에 적용한 코드 입니다.
// Read file asynchronously
const readFileAsync = async (path) => {
	try {
		const data = await readFile(path, 'utf8');
		return data;
	} catch(err) {
		throw new Error(err);
	}
}

// Process data
const processDataAsync = async (data) => {
	// Perform some operations with the data..

	// Simulating asynchronous operations..
	await delay(1000);
	return 'processed data';
};

// Write file asynchronously
const writeFileAsync = async (path, data) => {
	try {
    	await writeFile(path, data);
	    console.log('File write successful!');
	} catch (err) {
		console.error('Error:', err);
	}
};

// Usage with async/await
const main = async () => {
  try {
    const data = await readFileAsync('file.txt');
    const processedData = await processDataAsync(data);
    await writeFileAsync('output.txt', processedData);
  } catch (err) {
    console.error('Error:', err);
  }
};

main();
  • .then() 메소드만 사용했을 때 보다 훨씬 더 깔끔하고 선형적인 상태의 코드를 확인할 수 있습니다.
profile
권오남 / Onam Kwon

0개의 댓글