본문 바로가기

Coding Language/JavaScript

[JavaScript] Promise - 비동기처리를 동기방식으로 처리

[해당 포스트는 개인적으로 공부를 하고 차후에 참고용으로 하고자 작성한 것입니다.
따라서 잘못된 부분이나 부족한 부분이 있을 수 있기에 참고하시기 바랍니다.]

 

JavaScript는 대부분 비동기 방식으로, 처리 요청을 받으면 알려만 주고 바로 다음으로 넘어간다. 주로 통신 쪽 처리할 때 많이 겪는 문제가 있는데, 서버 쪽으로 요청을 보내고 결과 값을 받아온 후 화면에 출력을 해야 하는 작업이 있다. 하지만, 비동기 처리방식 때문에 처리 결과가 오기도 전에 화면에 출력을 해버리기 때문에 빈 화면을 보게 되는 경우가 발생한다.

 

때문에 Callback 방식으로 어떤 작업을 요청하면 함수에 등록하고, 작업이 수행된 결과를 나중에 Callback 함수를 통해 알려주는 구조가 흔하다. 문제는 Callback 함수가 늘어나면 코드가 중첩되는 문제가 발생하는데 이른바 'Callback 지옥'을 접하게 될 것이다.

 

async(1, function() {
    async(2, function() {
        async(3, function() {
            ...
        });
    });
});

 

이러한 문제를 해결하기 위해 Promise를 사용한다.

다음의 예제를 보면서 Promise를 어떻게 사용하는지 알아보도록 하자.

 

전형적인 비동기작업인 setTimeout을 이용하여 처리해보자.

 

function f1() {
    setTimeout(() => {
        console.log(1);
    }, 2000);
}

function f2() {
    setTimeout(() => {
        console.log(2);
    }, 1000);
}

(function f3() {
    f1();
    f2();
})()

 

f1()은 2초 뒤에 1이 출력이 되고, f2()는 1초 뒤에 2가 출력된다.

일반적으로 절차지향적으로 생각하면 f1()이 먼저 호출 됐기 때문에 1 2라는 결과를 예상하겠지만, JavaScript는 비동기 방식으로 처리되기 때문에 2 1이 출력될 것이다.

 

이번엔 이걸 순차적으로 출력할 수 있게 처리를 해보자.

 

setTimeout(() => {
    console.log(1);
    setTimeout(() => {
        console.log(2);
        setTimeout(() => {
            console.log(3);       
        }, 1000)      
    }, 1000)
}, 1000)

 

어디서 많이본 형태다.

위에서 봤던 Callback지옥인 형태로 코드가 점점 길어지는 것을 볼 수 있다.

 

이것을 Promise를 통해 해결해보도록 하자.

호출은 다음과 같이 할 수 있다.

 

new Promise( ( reslove, reject ) => { ... } )

 

Promise 내부에 익명 함수를 호출할 수 있으며, 인자는 reslove와 reject를 받는다.

Promise는 체이닝 기법을 통해 thencatch를 호출할 수 있다.

 

new Promise((resolve, reject) => {

})
.then(() => {

})
.catch(() => {
    
})

 

우선 Promise의 익명함수는 무조건 실행되는 부분이다.

만약 내부 작업이 성공적으로 처리를 하면 resolve를 호출하여 then으로 분기시킬 수 있다.

처리가 비정상적으로 되었을 땐 reject를 호출하여 catch로 분기하여 에러를 출력하는 곳으로 빠질 수 있다.

 

다음의 코드를 호출하면

Hello Promise

Then!

의 결과를 볼 수 있다.

 

new Promise((resolve, reject) => {
    console.log('Hello Promise');
    resolve();
})
.then(() => {
    console.log('Then!');

}).catch(() => {
    console.log('Error!'); 
})

 

이번에는 then 쪽에 다시 new Promise를 하여 처리하는 문장을 만들어 보자.

편의상 resolve 인자는 r1, reject 인자는 r2로 하겠다.

 

new Promise((r1, r2) => {
    console.log('Hello Promise1!');
    r1();
})
.then(() => {
    console.log('Hello Promise2!');

    new Promise((r1, r2) => {
        console.log('Hello Promise3!');
        r1();
    })
    .then(() => {
        console.log('Hello Primise4!');
    })
})

 

다음과 같이 호출하면

Hello Promise1!

Hello Promise2!

Hello Promise3!

Hello Primise4!

가 호출된다.

 

문제는 then 안에 new Promise를 계속 생성하면, 위에서 봤었던 Callback지옥에 빠지게 될 것이다.

이러한 문제를 해결하도록 내부에서 실행되는 Promise는 return을 시켜주자. 그러면 내부에 있던 Promise가 Scope에서 탈출하기 때문에 내부에 있던 Promise의 then을 내부에서 아닌 외부에서 호출을 할 수 있게 된다.

코드는 다음과 같다.

 

new Promise((r1, r2) => {
    console.log('Hello Promise1!');
    r1();
})
.then(() => {
    console.log('Hello Promise2!');

    //new Promise가 return됨.
    return new Promise((r1, r2) => {
        console.log('Hello Promise3!');
        r1();
    })
})
.then(() => {
    console.log('Hello Promise4!');

    return new Promise((r1, r2) => {
        console.log('Hello Promise5!');
        r1();
    })
})
.then(() => {
    console.log('Hello Promise6!');
})

 

여기서 공통적인 부분이 보일 것이다.

return new Promise() 생성과 r1함수 호출이 반복적으로 호출이 된다.

이것을 하나의 함수로 생성하여 좀 더 단순하게 만들어 보자.

 

function f1() {
    return new Promise((r1, r2) => {
        r1();
    })
}

f1()
.then(() => {
    console.log('Hello Promise2!');
    f1()
})
.then(() => {
    console.log('Hello Promise4!');
    f1()
})
.then(() => {
    console.log('Hello Promise6!');
})

 

람다식의 특성을 이용하여 한 줄로 처리할 땐 Scope를 제거하여 작성할 수 있다.

최종적으로 단순한 형태는 다음과 같아진다.

 

function f1() {
    return new Promise((r1, r2) => {
        r1();
    })
}
// 계속 한 줄로 처리해도 문제가 없다!
f1()
.then(() => f1() )
.then(() => f1() )
.then(() => f1() )
.then(() => f1() )
.then(() => f1() )
.then(() => f1() )

 

이번엔 위에서 사용했던 비동기 함수인 setTimeout을 동기적으로 처리할 수 있도록 만들어보자.

소스코드를 다음과 같이 작성해보자.

 

function f1() {
    return new Promise((r1, r2) => {
        setTimeout(() => {
            console.log('멍멍이');

            r1();
        }, 1000);
    })
}

f1()
.then(() => f1())
.then(() => f1())
.then(() => f1())

 

이러한 형태로 호출하면 setTimeout의 1초가 지나면 r1()이 호출이 된다.

따라서 동시에 4개의 멍멍이가 출력되는 것이 아니라, 1초 간격으로 멍멍이가 출력이 된다.

 

또한 인자로 값을 넘길 수 있다.

f1함수를 생성할 때, 인자를 받도록 만들자, 이후 f1함수를 호출할 때 원하는 값을 넣으면 된다.

 

function f1(n) {
    return new Promise((r1, r2) => {
        setTimeout(() => {
            console.log('멍멍이' + n);
        }, 1000 * n);
        r1();
    })
}

f1(1)
.then(() => f1(2))
.then(() => f1(3))
.then(() => f1(4))

 

다음과 같이 작성하면 처음엔 1초 다음엔 2초, 3초, 4초 뒤에 멍멍이들이 출력이 된다.