callback 패턴의 단점과 Promise

JaeungE·2021년 8월 18일
0

JavaScript

목록 보기
14/16
post-thumbnail

ES6 이전까지는 비동기 처리에서 작업 순서를 보장하기 위해 error-first callback 패턴을 사용했다.

node.js에서 fs 모듈을 사용하여 파일을 읽을 때 사용하는 readFile() 함수가 error-first callback 패턴을 사용하는 좋은 예다.

하지만 지속해서 사용하다 보니 여러 단점이 발견되었고, 이런 단점을 해결하기 위해 Promise가 탄생했다.

과연 어떤 문제가 있는지, 또 error-first callback 패턴을 어떻게 Promise 패턴으로 변경하는지 알아보도록 하자!🙂



callback hell

callback hell 은 비동기 처리가 필요한 작업의 순서를 보장하려고 할 때 생긴다.

예를 들어 text_1.txt -> text_2.txt -> text_3.txt 순서대로 파일의 내용을 출력한다고 해보자.


const fs = require('fs');

fs.readFile('./text_1.txt', (err, data) => {
    if(err) { 
        console.error(err); 
    }
    console.log(data.toString());
});

fs.readFile('./text_2.txt', (err, data) => {
    if(err) { 
        console.error(err); 
    }
    console.log(data.toString());
});

fs.readFile('./text_3.txt', (err, data) => {
    if(err) { 
        console.error(err); 
    }
    console.log(data.toString());
});

실행 결과
1. text_1.txt content!
2. text_3.txt content!
3. text_2.txt content!


동기식 흐름을 따르지 않는 비동기 처리의 특성상, 비동기 작업의 실행 순서가 보장되지 않는다..😥
만약 파일의 용량이 각자 다르다면 더욱 예상하지 못한 결과를 얻을 수도 있다.

이번엔 순서대로 실행될 수 있도록 코드를 고쳐보자!


const fs = require('fs');

fs.readFile('./text_1.txt', (err, data) => {
    if(err) { 
        console.error(err); 
    }
    console.log(data.toString());

    fs.readFile('./text_2.txt', (err, data) => {
        if(err) { 
            console.error(err); 
        }
        console.log(data.toString());

        fs.readFile('./text_3.txt', (err, data) => {
            if(err) { 
                console.error(err); 
            }
            console.log(data.toString());
        });
    });
});

실행 결과
1. text_1.txt content!
2. text_2.txt content!
3. text_3.txt content!


위처럼 코드를 고치면 비동기 작업의 순서를 보장할 수 있다. 하지만 순서대로 실행하려는 파일의 개수가 10개가 넘어간다면?🤔

이처럼 error-first callback 패턴을 사용하면 코드의 indent가 깊어지고, 지금은 데이터를 출력하는 간단한 작업이지만 가져온 데이터를 분기별로 다르게 조작하려 한다면 더욱 가독성이 떨어질 수밖에 없다.

이 외에도 예외처리를 위해 try...catch 문법을 callback 함수마다 달아줘야 하는 등, 여러 가지 단점이 존재하고 이를 해결하기 위해 Promise가 등장하게 된다.





Promise

Promiseerror-first callback 패턴의 여러 단점을 보완하기 위해서 생긴 기능이다.

그렇다고 callback을 아예 사용하지 않는 것은 아니고, 비동기 처리에 사용되는 callback 에서 일어날 수 있는 여러 가지 단점들을 보완해주는 기능이다.

Promise의 특징은 비동기 처리를 실행하긴 하지만, 결과를 바로 callback 함수로 건네주는 것이 아니라 나중에 처리할 수 있다는 것이다.

또한 Promisenew Promise() 생성자 함수를 통해 Promise 객체를 만들어서 처리하기 때문에 다른 함수에게 Promise 객체를 넘겨서 처리하는 것도 가능하다.

그러면 이제 Promise에 대해 자세히 알아보도록 하자!😆



Promise의 상태

Promise는 아래처럼 세 가지 상태를 가진다.

  • pending

    Promise가 생성되고 나서부터 fulfilledrejected 상태가 되기 전 까지의 상태

  • fulfilled

    Promise에서 resolve() 함수가 호출되면 fulfilled 상태가 된다.

  • rejected

    Promise에서 reject() 함수가 호출되면 rejected 상태가 된다.

그리고 resolve() 혹은 reject() 함수가 호출되면 해당 Promisesettled Promise라고 하고, settled Promiseresolve()reject()를 다시 호출한다고 해도 상태가 변하지 않는다.

한 마디로 Promise는 최종적으로 fulfilled 혹은 rejected 중 하나의 상태만을 가진다.



Promise 객체 활용

Promise가 등장한 이후로 error-first callback 패턴을 이용하던 함수들은 Promise를 지원하도록 바뀌었다.

하지만 Promise 객체를 반환하지 않는 함수들도 new Promise() 생성자 함수를 이용해서 Promise 객체를 사용할 수 있으니 걱정하지 않아도 된다.

fs.readFile() 함수를 Promise 객체로 변환하는 예제를 통해 Promise의 사용법을 알아보자!🙂


const fs = require('fs');

getText('./text_1.txt')
.then(data => console.log(data)) // 파일 내용이 있을 경우 실행
.catch(err => console.error(err)); // 파일 내용이 없거나 파일을 불러오지 못 한 경우 실행

function getText(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, (err, data) => {
            if(err || data.toString() === '') 
              	reject(new Error('no contents'));
            resolve(data.toString());
        })
    });
}

(원래 fs 모듈은 Promise를 지원하지만, error-first callback 패턴 함수를 Promise 객체로 만드는 예제를 보여주기 위해 일부러 이렇게 작성하였다.)


이제 getText() 함수를 호출하면 Promise 객체를 반환하게 되는데, Promise 객체의 매개변수를 보면 resolvereject를 가지는 callback 함수로 비동기 처리할 작업을 감싸고 있는 것을 볼 수 있다.

resolvereject 또한 callback 함수이며, 함수가 정상적으로 동작한다면 resolve()를 호출, 실패했다면 reject()를 조건에 따라 호출하면 된다.

그리고 반환된 Promise 객체의 then() 메서드로 resolve()에 대한 처리, catch() 메서드로 reject()에 대한 처리를 진행할 수 있다.

then() 메서드의 두 번째 매개변수로도 reject() 처리가 가능하지만, 그렇게 되면 then()callback 에서 발생한 예외는 잡아내지 못하므로, catch() 메서드를 사용하도록 하자!😣

이렇게만 해도 callback이 두 번 호출되는 경우를 방지할 수 있는 장점이 있지만, Promise의 진가는 체이닝에 있다.



Promise chaining

만약 Promise chaining이 없다고 가정해보고 Promise를 활용해서 callback hell이 일어날 수 있는 코드를 작성해보자.

비교를 위해 callback hell 에서 사용했던 예제를 똑같이 사용하겠다!


const fs = require('fs');

getText('./text_1.txt').then(data => {
    console.log(data);
    getText('./text_2.txt').then(data => {
        console.log(data);
        getText('./text_3.txt').then(data => {
            console.log(data);
        }, (err) => console.error(err))
    }, (err) => console.error(err))
}, (err) => console.error(err));

function getText(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, (err, data) => {
            if(data.toString() === '') reject(new Error('err'));
            resolve(data.toString());
        })
    });
}

Promise를 사용했는데도 불구하고 indenterror-first callback 패턴을 사용한 것과 큰 차이가 없어 보인다...😥

하지만 Promise chaining 을 활용하면 아주 깔끔한 코드를 작성할 수 있다.


const fs = require('fs');

getText('./text_1.txt')
.then(data => { console.log(data); return getText('./text_2.txt');})
.then(data => { console.log(data); return getText('./text_3.txt');})
.then(data => console.log(data))
.catch(err => console.error(err));

function getText(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, (err, data) => {
            if(data.toString() === '') reject(new Error('err'));
            resolve(data.toString());
        })
    });
}

Promise 또한 객체이기 때문에 then() 함수의 반환 값으로 또 다른 Promise 객체를 반환해서 코드를 간결하게 만들었다.

그리고 여러 개의 비동기 작업에 대한 reject() 처리도 Promise chaining을 이용하면 하나의 catch() 만으로도 처리가 가능하다!

이처럼 Promise를 활용하면 error-first callback 패턴의 여러 단점을 해결할 수 있다.



JavaScript의 특성상 비동기 처리는 거의 필수적이고, error-first callback 기반으로 만들어진 많은 API들이 Promise 기반으로 재구성되었기 때문에, Promise 객체에 대한 정확한 이해가 필요하다고 생각해서 정리해 보았습니다.

혹시 부족한 내용이나 틀린 부분이 있다면 지적해주시면 감사하겠습니다!!🤗





참고 자료
Using promises - JavaScript | MDN
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Using_promises


Promise() 생성자 - JavaScript | MDN
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise


Promise - JavaScript | MDN
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise

0개의 댓글