선결론 : 제대로 사용할 줄만 안다면 큰 도움이 된다.
서론
자바스크립트 언어는 위키백과의 말을 빌리자면 다음과 같습니다.
JavaScript is high-level, often just-in-time compiled, and multi-paradigm
파이썬과 같은 사람들이 사용하는 일상 용어와 비슷하면 high-level 이라고도 합니다. 즉, 기계언어, machine language 와 비슷해질 수록 low-level 로 불리죠. 여기에 속하는 언어들은 C 가 있죠. 더 들어가면 Machine Language, 거의 이진수를 사용하는 수준과 사람들이 그나마 이해할 수 있는 단어들이 존재하는 Assembly Language 가 있습니다.
사실 위 정의를 굳이 언급하지 않아도 자바스크립트를 사용하시는 분들은 혹은 어느 정도 경험이 있으신 분들은 이미 알고 계실 겁니다. Just-in-time compiled 는 무슨 뜻일까요? 간단히 설명을 하자면 ahead-of-time compilation 과 interpretation 의 합성입니다. 즉, 프로그램이 실행될 때에 기계가 이해할 수 있도록 변경한다는 뜻이죠.
Multi-paradigm 이란 event-driven, functional 그리고 imperative 프로그래밍을 가능하게 해주죠.
여기에서 이번 글에서 살펴볼 것은 event-driven 이라는 개념입니다. 자바스크립트의 이벤트 룹에 대해서 얼마나 아시나요? 아래 영상들에서는 이벤트 룹에 대해서 꽤 자세히 그리고 친절하게 설명을 해주고 있습니다.
쉽게 풀어 말하자면 자바스크립트에서는 콜백으로 비동기를 처리하게 됩니다. 즉, 콜 스택과 메모리 힙만 생성해서 돌아가는 언어에서 기대할 있는 부분이 아니죠. 그렇다면 브라우저에서는 어떻게 비동기를 실행할까요? 개발자 콘솔 창을 열어서 fetch 와 같은 코드를 사용하게 되면 비동기로 작동을 하는데 어떻게 가능할까요?
그건 자바스크립트가 아니고 브라우저에서 제공하는 기능이기 때문입니다. 다시 말해서 브라우저에서는 비동기를 처리할 수 있도록 API 를 제공하고 있습니다. Node JS 또한 이러한 런타임 환경을 제공하죠.
예전에는 콜백으로 함수 내에서 다른 함수를 불러 비동기를 실행했습니다. 이렇게 되면 동기적으로 실행했을 때에 발생할 수 있는 성능 저하와 이벤트 룹의 방해 요소, 불안정한 프레임 배치 등을 방지할 수 있기 때문이죠. 하지만 문제는 콜백을 실행하면서 시작했습니다.
Callback Hell
'콜백 헬'이라고 들어봤나요?
헬이 지옥이라는 사실은 어느 정도 사람들이 인지하고 있습니다. 그렇다면 콜백 헬은 무슨 지옥일까요? 바로 들여쓰기 지옥입니다. 즉, 하나의 함수를 호출했는데 해당 함수에서 호출하는 다른 함수가 또다른 함수를 호출하는 지독하게 엮여있는 상태입니다. 콜백의 예시는 구글에 치기만 해도 넘쳐납니다. 예시로 보여드리자면 다음과 같죠
fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})
// Source : https://www.toppr.com/guides/computer-aptitude-and-knowledge/basics-of-computers/computer-languages/
각 단계에서는 에러가 발생할 경우를 대비해 처리를 해야 하지만, 계속해서 안쪽으로 파고드는 경향이 있어 보이는 것을 보실 수 있습니다. 위 코드가 무슨 일을 할까요? 그저 파일을 읽어 각 파일의 사이즈를 읽고 리사이징을 거치는 겁니다. 뭔가 실제로 하는 일은 없어보이는데 읽기 조차 싫습니다.
그래서 나온 대안이 체이닝입니다. Promise 이죠. 간단히 말해서는 저런 방식으로 들여쓰기 하는 것보다 일자로 쭈욱 내려가는 방법이라고 볼 수 있습니다. Promise 에는 resolve 와 reject, 쉽게 말해 원하는 방향으로 흘러갔을 때와 에러가 났을 때 처리해주는 두 개의 콜백으로 나중에 .then, .catch 를 통해 어떤 결과를 받는지 볼 수 있습니다.
Promise Hell
처음에는 해결방법이 먹히는 듯 했습니다. 이전과 달리 이제는 들여쓰기를 안해도 되니 코드 읽기가 더 간결해진 거죠. 허나 쉽게 물러갈 콜백헬이 아니었고 Promise 또한 근본적으로 연이어서 해결해야 하는 부분을 제대로 처리하지 못했습니다. 그렇게 프로미스 헬이 등장하게 됩니다. 다른 의미로 헬이었죠. 이제는 들여쓰기는 고쳐지나 싶었지만 다른 부분이 날뛰기 시작했습니다. 같이 보실까요?
return getCurrentUser()
.then((user) => {
const promises = [];
if (attachFavouriteFood) {
promises.push(getFood(user.favouriteFoodId)
.then((food) => {
user.food = food;
}));
}
if (attachSchool) {
promises.push(getSchool(user.schoolId)
.then((school) => {
user.school = school;
if (attachFaculty) {
return getUsers(school.facultyIds)
.then((faculty) => {
user.school.faculty = faculty;
});
}
}));
}
return Promise.all(promises)
.then(() => {
return user;
});
});
// Source : https://medium.com/@pyrolistical/how-to-get-out-of-promise-hell-8c20e0ab0513
원글에서 나오듯이 이러한 문제점의 대부분은 사실 코드를 작성할 때에 promise 의 장점을 제대로 활용하지 못했기 때문입니다. 여기에서도 줄일 수 있는 부분들이 꽤나 있습니다. Promise.all 등이 있어 동시에 여러 프로미스 객체들을 실행할 수가 있죠.
하지만 그럼에도 불구하고 근본적으로 체이닝의 문제를 해결할 수는 없다고 생각합니다. 코드를 작성하는 것은 마치 글을 작성하는 것과도 같다고 생각합니다. 사람마다 저마다의 방식이 있겠지만 글과 비슷하게 타인이 읽을 수 있어야 합니다. 하나의 긴 문장을 작성하게 되면 쓰는 입장과 달리 읽는 사람의 입장은 매우 헷갈릴 수도 있습니다.
그렇게 해서 나온 것이 Async/Await 입니다.
Async/Await
진정한 비동기 처리 방식이 나왔다고 보는 시점입니다. 물론 Promise 또한 어느 정도 해결책을 제공했지만 해당 Promise 를 더욱 활용하는 것이 Async/Await 입니다. 그 이유는 동기함수처럼 취급하도록 해주기 때문이죠.
이 둘은 하나입니다. 하나가 뇌이면 다른 하나는 심장이죠. Await 은 홀로 존재할 수가 없습니다 Async 가 이미 적용된 상태에서 사용 가능하죠. 마찬가지로 Async 자체로서 무엇인가를 할 수 없습니다. Async 는 그저 특정 함수를 동기적으로 실행할 수 있는 능력을 부여할 뿐 실제로 이행하는 것은 Await 입니다.
이 부분도 코드로 한번 살펴보겠습니다.
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // wait until the promise resolves (*)
alert(result); // "done!"
}
f();
// Source : https://javascript.info/async-await
여기에서는 간단한 프로미스 객체를 하나 만듭니다. 물론 프로미스 자체로는 딱히 동기적으로 실행하기에는 역부족이죠. 허나 Async/Await 의 도움으로 다 끝날 때까지, 여기에서는 1000ms 이 지날 때까지 아래 alert(result) 이 기다려줍니다.
보시면 result 라는 변수는 promise 의 값을 담고 있습니다. 그 이유는 await 이 앞에 있기 떄문이죠.
결론
콜백 헬... 사실 이미 해결이 된 부분입니다. 역사죠 이제는. 하지만 그럼에도 불구하고 계속 등장하게 되는 이유는 그만큼 중요하기 때문입니다. Class 문법이 새로 자바스크립트에 도입이 되기는 했지만 결국에는 sugar coating, sugar syntax 이기 때문에 근본적으로 prototyping 이 바뀌지는 않았습니다. 그저 이전에 사용했던 Object.create(), Object.prototype 등 프로토타입을 이용해 만들던 것을 더욱 쉽게 설계해주게 하는 것 뿐이죠.
마찬가지입니다. 진정으로 비동기 처리법에 대한 이해는 그 근본부터 살펴봐야 합니다. 무엇이 변화를 요구했는지, 왜 promise, async/await 등이 등장했는지 등.
세상은 변합니다. 그대로 있지 않죠. 눈치채든 안채든 상관없습니다. 자바스크립트 또한 95년도 공식 사용부터 지금가지 20 몇년간 많은 변화가 있었습니다.
허나 근본은 쉽게 변경하지 않습니다. 그렇기 때문에 표면보다는 가끔씩 속내를 주시해야 한다고 생각합니다.
감사합니다.
'Programming Languages > Javascript' 카테고리의 다른 글
ES6 Classes and Super...? (0) | 2022.05.31 |
---|---|
this의 세계 (0) | 2022.05.30 |
비동기적 호출 - [setTimeout, setInterval] (0) | 2022.05.30 |
super()를 이용한 class 상속 방법 (ES6) (0) | 2022.05.30 |
Javascript의 Class 활용법 (0) | 2022.05.30 |