프로그래밍을 하다 보면 '비동기 처리' 혹은 '비동기적 프로그래밍' 이란 단어를 자주 접할 수 있다.
정해진 흐름에서 벗어난 동작을 하도록 만드는 방법인데, 왜 이렇게 처리해야 하는지에 대해 적어보려고 한다.
그리고 callback 함수는 '비동기적 프로그래밍' 에서는 빼놓을 수 없을 정도로 중요하다. 왜 중요한지는 아래에서 천천히 알아보도록 하자!😉
비동식 코드의 예를 보기 전에, 먼저 동기식으로 실행되는 코드부터 보도록 하자.
const fs = require('fs');
console.log(fs.readFileSync('./text_1.txt').toString());
console.log(fs.readFileSync('./text_2.txt').toString());
console.log(fs.readFileSync('./text_3.txt').toString());
실행 결과
1. text_1.txt content!
2. text_2.txt content!
3. text_3.txt content!
node.js
의 fs
모듈을 이용해서 동기적으로 파일을 읽고, console.log()
함수를 이용해 파일의 내용을 출력해주는 코드이다.
동기적인 코드는 보시다시피 위에서 아래로 흐르는, 우리가 일반적으로 알고 있는 실행 순서를 따른다.
그렇다면 파일의 정보를 읽어오는 작업을 비동기식 코드로 작성하면 어떻게 되는지 보도록 하자!
const fs = require('fs');
fs.readFile('./text_1.txt', (err, data) => console.log(data.toString()));
fs.readFile('./text_3.txt', (err, data) => console.log(data.toString()));
console.log(fs.readFileSync('./text_2.txt').toString());
실행 결과
1. text2_txt.content!
2. text1_txt.content!
3. text3_txt.content!
분명히 코드의 흐름으로 보면 1 -> 3 -> 2의 순서로 파일을 읽고 내용을 출력할 것으로 예상했지만, 결과를 보면 완전히 다르다.
동기식으로 작성한 2번 파일의 내용을 먼저 출력하고 그 뒤에 1번 파일과 3번 파일의 내용이 출력되는 것을 볼 수 있는데, 이렇게 동기식으로 작성한 코드가 먼저 실행된 이유는 브라우저의 이벤트 루프가 동기식으로 작성된 코드를 우선적으로 처리하기 때문이다.
(이 포스팅에서는 이벤트 루프에 대해서는 다루지 않을 예정이다..😅 자세한 내용은 MDN
이나 다른 분들이 보기 좋게 정리한 자료를 참고하도록 하자!)
이처럼 비동기식 코드는 코드의 흐름을 파악하는데 어려움을 준다.
그런데도 굳이 비동기식 코드를 사용하는 이유는, 동기식으로 작성된 코드를 실행하는 데 걸리는 시간이 매우 오래 걸리는 경우에, 해당 코드의 실행을 마치기 전까지 메인 스레드는 다음 코드를 실행하지 못하고 멈춰있기 때문이다.
위의 예제에서 파일의 용량이 매우 커서 하나를 불러오는 데 1분이 걸린다고 한다면, 메인 스레드는 파일을 불러오는 동안에는 다음 코드를 실행시킬 수 없어서 굉장히 비효율적이다.
특히 메인 스레드가 싱글 스레드인 JavaScript
의 특성상, 런타임 환경이 웹이라면 사용자 입장에서는 파일을 불러오는 시간 동안 아무런 작업도 하지 못하는 치명적인 상황이 생길 수 있다.
그리고 비동기식 코드는 흐름을 파악하는 것 외에도 다음 예제처럼 다른 문제를 일으키기도 한다.
비동기식 코드는 흐름을 파악하기 어렵다는 것 외에도, 동기식 코드와 섞어서 사용하는 경우에도 문제가 발생한다. 아래 예제를 보도록 하자!
const fs = require('fs');
console.log(getText()); // 'original'
function getText() {
let str = 'original';
fs.readFile('./text_1.txt', (err, data) => {
if(err) {
return console.error(err);
}
str = data.toString();
});
return str; // str === 'original'
}
getText()
함수를 호출하면 비동기식으로 text_1.txt
파일의 내용을 변수 str
에 저장해서 반환하는 동작을 기대했다.
하지만 파일을 읽어와서 변수 str
에 대입하기 전에 이미 getText()
함수는 반환을 끝마쳤고, 결국 getText()
가 반환한 값은 원래 있던 문자열인 original
을 반환하게 된다.
이렇게 동기식 코드와 비동기식 코드를 같이 사용하면 실행 순서를 보장할 수 없게 되고, 결국 원했던 결과와 다른 동작을 하게 된다....😥
하지만 callback 함수를 이용하면 비동기 처리가 끝난 후에 해야 할 작업에 대해 순서를 보장해줄 수 있다! 위의 코드에서 getText()
함수의 매개변수에 callback 함수를 추가해보자.
const fs = require('fs');
getText((err, data) => {
if(err) {
return console.error(err);
}
console.log(data); // text_1.txt content!
});
function getText(callback) {
fs.readFile('./text_1.txt', (err, data) => {
if(err) {
return console.error(err);
}
if(data.toString() === '') {
callback(new Error('file data is empty'));
}else {
callback(null, data.toString());
}
});
}
getText()
의 callback 함수는 자신이 무슨 작업을 할지 정해두고, 비동기 처리가 끝난 후 데이터가 넘어오면 callback 함수에게 데이터를 넘겨 이전 작업이 완료됐음을 알리고, 그때 작업을 시작하는 방식으로 순서를 보장한다.
이렇게 error-first callback 패턴을 이용해도 비동기 처리에서 작업 순서를 보장할 수 있지만, 비동기 처리 이후 순서대로 처리해야 할 작업이 많아질수록 callback hell로 불리는 상황이 일어나서 가독성이 굉장히 떨어지게 된다.
코드의 복잡성 또한 증가해서 디버깅이 어려워지게 되고, try ... catch
는 같은 함수 안에서만 동작하므로 익명 함수를 자주 사용하는 callback의 특성상 예외 처리도 굉장히 힘들어진다.
이렇게 callback 패턴의 여러 문제점을 해결하기 위해서 ES 2015
의 promise 와 ES 2017
의 async/await가 등장했다. 두 기능은 다음에 다루도록 하겠다.😉
훨씬 편리하고 직관적으로 비동기 처리를 도와주는 여러 기능이 등장했지만, 기존 방식은 어떻게 구현하고 무엇이 불편해서 새로운 기능이 탄생했는지 공부하기 위해 포스팅을 작성했습니다.😶
혹시라도 틀린 부분이 있거나 부족한 내용이 있다면 알려주시면 감사하겠습니다!!
참고 자료
자바스크립트 비동기 처리와 콜백 함수
https://joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/
[JavaScript] 비동기 처리, 콜백 함수
https://velog.io/@pyo-sh/JavaScript-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EC%BD%9C%EB%B0%B1-%ED%95%A8%EC%88%98
1. 동기와 비동기, 콜백함수
https://pro-self-studier.tistory.com/89