Promise & async/await
- 비동기 코드 핸들링:
JavaScript
의 Promise
와 async/await
은 비동기 코드를 동기적으로 사용하기 위해 사용됩니다. 이를 사용하므로 인해 nested callbacks
(콜백지옥)을 방지해 주며 가독성 있는 코드를 만들도록 도와줍니다.
- 에러 핸들링: 두가지 방법 모두 에러 핸들링의 사용에도 도움을 줍니다.
- Promise:
.catch()
메소드를 이용해 에러를 핸들할 수 있습니다.
- async/await:
try-catch
블럭을 이용해 에러를 핸들할 수 있습니다.
callback
JavaScript
에는 비동기 함수가 포함되어 있으며 비동기 함수는 작업을 완수하는데 시간이 걸리기 때문에 해당 작업이 수행 된 후 따로 처리를 해 주어야 합니다.
- 이를 위한 해결 방법으로
callback
함수라는 개념이 도입되었습니다.
callback
함수란 특정 함수가 실행 된 후 바로 이어서 실행되는 함수를 말합니다. 이를 이용해 비동기 함수의 수행이 완료 된 후 콜백 함수로 등록해 놓은 함수가 실행 되게 처리할 수 있습니다.
- 하지만 이
callback
함수에도 한가지 문제점이 존재하는데 여러개의 콜백 함수가 겹치면 가독성이 아주 나빠진다는 단점이 존재했습니다. 이를 nested callback
, 콜백 지옥 이라고 표현합니다.
fs.readFile('file.txt', 'utf8', function(err, data) {
if (err) {
console.error('Error reading file:', err);
} else {
processData(data, function(err, result) {
if (err) {
console.error('Error processing data:', err);
} else {
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
JavaScript
의 Promise
객체는 비동기 작업을 다루는데 사용합니다.
Promise
객체는 비동기 작업의 성공 또는 실패의 결과값을 나중에 받을 수 있는 대리자 역할을 합니다.
Promise
객체는 다음과 같은 세가지 상태를 가질 수 있습니다.
pending(대기)
: 객체의 초기 상태로 비동기 작업이 아직 완료 되지 않은 상태입니다(작업중). 이후 아래 두가지 상태중 하나로 변합니다.
fulfilled(이행)
: 비동기 작업이 성공적으로 완료된 상태입니다.
rejected(거부)
: 비동기 작업이 실패한 상태입니다. 오류가 있음을 나타냅니다.
Promises
는 비동기 코드로 작업할 수 있는 깨끗하고 일관된 방법을 제공하여 비동기 작업에 대해 더 쉽게 쓰고, 읽고, 추론할 수 있도록 도와줍니다. 콜백 지옥과 복잡한 중첩 코드로 이어질 수 있는 콜백 함수에 의존하는 대신, Promises
를 사용하면 비동기 작업을 함께 연결하여 성공 또는 실패를 보다 선형적이고 순차적으로 처리할 수 있습니다.
const myPromise = new Promise((resolve, reject) => {
const data = fetch('URL_WILL_BE_HERE');
if(data) {
resolve(data);
} else {
reject('Error');
}
});
- 위의 코드는 기본적인
Promise
객체의 사용법 입니다.
- 비동기 작업을 실행 후 해당 결과를
data
변수에 저장합니다.
- 만약
data
변수의 값이 존재한다면 resolve()
성공 메소드를 호출해 data
값을 반환합니다.
- 만약
data
변수의 값이 존재하지 않는다면 reject()
실패 메소드를 호출해 에러를 반환합니다.
myPromise
.then((data) => {
console.log('Data:', data);
})
.catch((err) => {
console.error('Error:', err);
})
.finally(() => {
console.log('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) => {
})
.then((data) => {
})
.catch((err) => {
})
.finally(() => {
});
- 일반적으로는 위와 같은 방법으로 함수로 감싸서 사용합니다.
const readFile = (path) => {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
};
const processData = (data) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Processed data');
}, 1000);
});
};
const writeFile = (path, data) => {
return new Promise((resolve, reject) => {
fs.writeFile(path, data, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
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/await
은 Promise
를 대체하지 않으며 이를 기반으로 작동합니다.
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);
} catch(err) {
throw new Error(err);
}
}
asyncFunction();
- 위 코드는 기본적인
async/await
사용법 입니다.
getTimeT()
함수는 내부에 Promise
객체를 가지고 있으며 setTimeout()
메소드를 포함합니다. 이는 1초 후에 1000
을 리턴합니다. setTimeout()
메소드는 비동기 함수 이므로 getTimeT()
함수는 비동기 함수입니다.
- 이 함수를 호출할 때
await
키워드를 붙혀 주고 해당 범위의 함수에 async
키워드를 붙혀 주는 방법을 통해 비동기 함수를 기다릴 수 있습니다.
- 아래는 위에서 배운
async/await
을 원래의 예시에 적용한 코드 입니다.
const readFileAsync = async (path) => {
try {
const data = await readFile(path, 'utf8');
return data;
} catch(err) {
throw new Error(err);
}
}
const processDataAsync = async (data) => {
await delay(1000);
return 'processed data';
};
const writeFileAsync = async (path, data) => {
try {
await writeFile(path, data);
console.log('File write successful!');
} catch (err) {
console.error('Error:', err);
}
};
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()
메소드만 사용했을 때 보다 훨씬 더 깔끔하고 선형적인 상태의 코드를 확인할 수 있습니다.