자바스크립트의 비동기 코드의 처리에 대해 공부한 내용을 정리했다.
블로킹과 논블로킹, 동기와 비동기
비동기 처리 및 그와 관련된 개념을 정리하기 전에 블로킹과 논블로킹, 동기와 비동기에 대한 개념부터 다시 잡고 가야할 것 같아서 내용을 찾아보았다.
- 동기, 비동기 : 코드의 순서와 실행 순서가 일치하는가
- 동기(Synchronous) : 코드가 순서대로 실행된다
- 비동기(Asynchronous) : 코드가 순서대로 실행되지 않는다.
- 블로킹, 논블로킹 : 코드의 실행이 다른 코드의 실행을 막는가
- 블로킹(Blocking) : 코드의 실행이 다른 코드의 실행을 막는다.
- 논블로킹(Non-blocking) : 코드의 실행이 다른 코드의 실행을 막지 않는다.
노드에서는 비동기면 논블로킹인 경우가 많다고 한다. fs같은 것들은 파일시스템 동작을 백그라운드로 넘겨버려 다음 코드들이 실행되고, 백그라운드에서 파일시스템 동작이 완료되면 정의해둔 콜백이나 프로미스가 실행된다. 즉, 코드도 순서대로 실행되지 않는다.
비동기 코드
// data.txt - 'This works - data from the text file!'라는 내용의 텍스트파일
const fs = require('fs');
function readFile() {
// let fileData;
fs.readFile('data.txt', (err, fileData) => {
console.log('File parsing done!');
console.log(fileData.toString());
});
console.log('Hi there!');
}
readFile();
module.exports = { readFile };
// --- console.log
// Hi there!
// File parsing done!
// This works - data from the text file!
- console.log('Hi there!');가 함수의 가장 마지막 줄에 있음에도 로그에는 가장 먼저 출력되는데, 그 이유는 fs.readFile은 비동기적으로 실행되기 때문이다.
- fs.readFile로 파일 읽기가 시작된 후 그 작업이 끝날 때까지 기다려주지 않고, 바로 그 다음 코드인 console.log('Hi there!');로 넘어가버린다.
- console.log('Hi there!'); 가 실행되어버리고 나서야 fs.readFile에서 정의해둔 콜백이나 프로미스가 실행되어 'File parsing done!'이나 fileData.toString()는 그 이후에 출력되는 것이다.
콜백 함수
콜백 함수 : 다른 함수의 인자로 전달되어 나중에 실행되는 함수
콜백 함수는 비동기 코드를 처리하기 위해 사용한다. 콜백 함수를 사용하면 비동기적 작업이 완료된 후에 특정 동작이 실행되도록 할 수 있다.
위의 비동기 코드 예제의 readFile 함수를 다시 보자.
function readFile() {
// let fileData;
fs.readFile('data.txt', (err, fileData) => { // callback
console.log('File parsing done!');
console.log(fileData.toString());
// start another async task that sends the data to a database
});
console.log('Hi there!');
}
data.txt가 지금처럼 아주 작고 귀여운 텍스트 파일이 아니라 DB에서 꺼내오는 엄청난 양의 데이터 파일이라고 가정해보자.
이 데이터 파일에서 readFile로 데이터를 읽고, 또 읽어온 데이터를 바탕으로 새로운 무언가를 다시 보낸다고 한다면 콜백 안의 또 콜백, 그 안의 또 콜백.. 들여쓰기때문에 아래와 같은 콜백 지옥이 생기게 될 것이다.
따라서 서로 의존해야하는 비동기 작업이 많을 수록 콜백함수 내부에 더 많은 콜백함수가 있게 된다. 왼쪽 이미지는 몇 줄 안되는 콜백지옥인데도 가독성이 매우 떨어진다.
이러한 비동기 코드를 가독성 좋게 처리하는 몇가지 방법이 있다.
프로미스
const fs = require('fs/promises');
node.js에는 비동기 방식의 fs 메서드를 동기처리 할 수 있도록 하는 내장 패키지 fs/promises가 있다. 단순하게 fs 메서드의 프로미스 버전을 제공해주는 것이다.
const fs = require('fs/promises');
function readFile() {
fs.readFile('data.txt')
.then((fileData) => {
console.log('File parsing done!');
console.log(fileData.toString());
// return anotherAsyncOperation;
})
.then(() => {
// 다른 콜백함수
});
console.log('Hi there!');
}
promise를 반환하고 체이닝할 수 있기 때문에, 콜백 지옥에서의 들여쓰기가 없어져서 좀 더 구조화된 느낌으로 가독성이 개선되었다.
콜백 함수와 프로미스에서의 에러 처리
콜백 함수
try ~ catch문은 콜백 함수에서 사용할 수 없다. 콜백 함수 방식의 경우 보통 오류에 대한 정보를 알려주는 오류 매개변수를 갖고, 이를 if 검사를 사용해서 오류가 설정되었는지 체크하여 에러를 처리할 수 있다.
const fs = require('fs');
function readFile() {
fs.readFile('data.txt', (error, fileData) => { // error와 data 매개변수
if (error) {
...
}
console.log('File parsing done!');
console.log(fileData.toString());
// start another async task that sends the data to a database
});
console.log('Hi there!');
};
안그래도 들여쓰기 때문에 가독성 헬인데 거기다 에러처리를 위해서 if문까지 다닥다닥 들어가 있다고..? ...상상도 하고싶지 않다.
프로미스
프로미스의 경우에는 .then 메서드는 작업이 성공한 경우에만 데이터를 얻는다. 이 때 에러가 발생했을 때 에러에 대한 정보를 얻으려면 .catch 메서드를 추가해서 사용한다.
const fs = require('fs/promises');
function readFile() {
fs.readFile('data.txt')
.then((fileData) => {
console.log('File parsing done!');
console.log(fileData.toString());
// return anotherAsyncOperation;
})
.then(() => {
// 다른 콜백함수
})
.catch((error) => { // catch the error!
console.log(error);
});
console.log('Hi there!');
}
async/await - 프로미스의 가독성 개선 버전
promise와 .then 체이닝을 더욱 더 가독성 좋게 처리할 수 있는 방법이 바로 async/await이다. ES6에서 처음 등장했다.
async/await를 사용하면 자바스크립트는 내부적으로 promise와 then을 추가하고 이를 비동기적으로 처리한다.
const fs = require('fs/promises');
async function readFile() {
const fileData = await fs.readFile('data.text');
console.log('File parsing done!');
console.log(fileData.toString());
console.log('Hi there!');
return fileData;
}
async/await를 사용하여 작성한 코드는 동기적으로 보이지만, 내부적으로는 여전히 promise와 then 버전으로 변환된다는 것을 기억하자!
async/await에서의 에러처리
또한 async/await에서는 try ~ catch문을 사용하여 에러를 처리한다.
async function readFile() {
let fileData;
try {
fileData = await fs.readFile('data.text');
} catch (error) {
console.log(error);
}
console.log('File parsing done!');
console.log(fileData.toString());
// return anotherAsyncOperation;
console.log('Hi there!');
}
이제서야 마음이 편안하다.
콜백 함수로부터 프로미스를 거쳐 여기까지 직접 와보니 async/await 구문이 얼마나 가독성이 좋은 것인지 피부로 느낄 수 있었다.
자바스크립트의 역사에서 비동기 작업을 처리함에 있어서 여러 단계를 거쳐 동기식 코드처럼 보이는 스타일로 돌아오게 된 것이다.
'✍️ What I Learned > TIL' 카테고리의 다른 글
[TIL] Recoil Selector (0) | 2023.10.29 |
---|---|
[TIL] React-hook-form 복습 - register, validate, errors, handleSubmit (0) | 2023.10.19 |
[TIL] 비동기 함수로부터 받아오는 data에 제네릭 사용하기 (0) | 2023.09.14 |
[TIL] HTTP 상태 코드 (0) | 2023.09.12 |
[TIL] JavaScript 동기, 비동기, 비동기 프로그래밍의 필요성 (0) | 2023.09.09 |