콜백과 약속 사이에 근본적인 차이점이 있습니까?


94

단일 스레드 비동기 프로그래밍을 수행 할 때 익숙한 두 가지 주요 기술이 있습니다. 가장 일반적인 것은 콜백을 사용하는 것입니다. 이는 콜백 함수를 매개 변수로 비동기식으로 작동하는 함수에 전달하는 것을 의미합니다. 비동기 작업이 완료되면 콜백이 호출됩니다.

jQuery이런 식으로 설계된 일부 일반적인 코드는 다음과 같습니다.

$.get('userDetails', {'name': 'joe'}, function(data) {
    $('#userAge').text(data.age);
});

그러나이 유형의 코드는 이전 코드가 끝나면 다른 비동기 호출을 차례로 수행하려고 할 때 복잡하고 중첩 될 수 있습니다.

두 번째 방법은 약속을 사용하는 것입니다. Promise는 아직 존재하지 않는 값을 나타내는 개체입니다. 콜백을 설정할 수 있으며, 값을 읽을 준비가되면 호출됩니다.

Promises와 기존 콜백 접근 방식의 차이점은 비동기 메소드가 이제 클라이언트가 콜백을 설정하는 Promise 객체를 동 기적으로 반환한다는 것입니다. 예를 들어 AngularJS에서 Promises를 사용하는 유사한 코드는 다음과 같습니다.

$http.get('userDetails', {'name': 'joe'})
    .then(function(response) {
        $('#userAge').text(response.age);
    });

내 질문은 : 실제로 실제 차이가 있습니까? 그 차이는 순전히 구문적인 것 같습니다.

한 기술을 다른 기술보다 사용해야하는 더 깊은 이유가 있습니까?


8
예 : 콜백은 일류 함수입니다. 약속은 값에 대한 작업을 연결하는 구성 가능한 메커니즘을 제공하고 편리한 인터페이스를 제공하기 위해 콜백과 함께 고차 함수를 사용하는 모나드입니다.
amon


5
@ gnat : 두 질문 / 답변의 상대적 품질을 고려할 때 IMHO와는 다른 방식으로 중복 투표가 이루어져야합니다.
Bart van Ingen Schenau

답변:


110

약속은 단지 구문 설탕이라고 말하는 것이 공정합니다. 콜백으로 할 수있는 약속으로 할 수있는 모든 것. 실제로, 대부분의 promise 구현은 원할 때마다 둘 사이를 변환하는 방법을 제공합니다.

약속이 더 나은 이유는 약속이 더 작기 때문입니다 . 이는 여러 약속을 결합하는 것이 "단순히 작동하지만"여러 콜백을 결합하는 것은 종종 그렇지 않음을 의미합니다. 예를 들어, 약속을 변수에 할당하고 나중에 추가 핸들러를 추가하거나 모든 약속이 해결 된 후에 만 ​​실행되는 대규모 약속 그룹에 핸들러를 첨부하는 것은 사소한 일입니다. 당신은 일종의 콜백으로 이러한 것들을 모방 할 수 있지만, 그것은 더 많은 코드를 걸립니다 아주 제대로 수행하기 어려운, 그리고 최종 결과는 일반적으로 훨씬 적은 유지 보수입니다.

조합 가능성을 보장하는 가장 크고 (미묘한) 방법 중 하나는 반환 값과 잡히지 않은 예외를 균일하게 처리하는 것입니다. 콜백을 사용하면 예외를 처리하는 방법은 중첩 된 많은 콜백 중 어느 것이 콜백을 수행했는지와 콜백을 수행하는 함수 중 구현에 try / catch가있는 기능에 전적으로 달려 있습니다. 약속, 당신은 알고 하나 개의 콜백 함수를 탈출 예외가 잡힌하고 함께 제공되는 오류 처리기에 전달됩니다 .error().catch().

단일 콜백 대 단일 약속에 대한 예에서 중요한 차이점은 없습니다. 그것은 당신이 약속 기반 코드가 훨씬 더 좋아 보이는 경향을 가진 대폭 선 약속 대 단거리 콜백이있을 때입니다.


다음은 약속으로 작성된 가상 코드에 대한 시도와 콜백을 사용하여 내가 말하는 것에 대한 아이디어를 줄 정도로 복잡해야합니다.

약속과 함께 :

createViewFilePage(fileDescriptor) {
    getCurrentUser().then(function(user) {
        return isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id);
    }).then(function(isAuthorized) {
        if(!isAuthorized) {
            throw new Error('User not authorized to view this resource.'); // gets handled by the catch() at the end
        }
        return Promise.all([
            loadUserFile(fileDescriptor.id),
            getFileDownloadCount(fileDescriptor.id),
            getCommentsOnFile(fileDescriptor.id),
        ]);
    }).then(function(fileData) {
        var fileContents = fileData[0];
        var fileDownloads = fileData[1];
        var fileComments = fileData[2];
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }).catch(showAndLogErrorMessage);
}

콜백 사용 :

createViewFilePage(fileDescriptor) {
    setupWidgets(fileContents, fileDownloads, fileComments) {
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }

    getCurrentUser(function(error, user) {
        if(error) { showAndLogErrorMessage(error); return; }
        isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id, function(error, isAuthorized) {
            if(error) { showAndLogErrorMessage(error); return; }
            if(!isAuthorized) {
                throw new Error('User not authorized to view this resource.'); // gets silently ignored, maybe?
            }

            var fileContents, fileDownloads, fileComments;
            loadUserFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileContents = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getFileDownloadCount(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileDownloads = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getCommentsOnFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileComments = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
        });
    });
}

약속없이 콜백 버전에서 코드 중복을 줄이는 영리한 방법이있을 수 있지만, 내가 생각할 수있는 모든 것은 약속 같은 것을 구현하는 것으로 요약 할 수 있습니다.


1
약속의 또 다른 주요 장점은 async / await 또는 약속 된 약속에 대한 약속 된 값을 다시 전달하는 코 루틴을 사용하여 "당화"할 수 있다는 것 yield입니다. 여기서 장점은 기본 제어 흐름 구조를 혼합 할 수 있다는 것입니다.이 흐름 구조는 수행하는 비동기 작업 수에 따라 다를 수 있습니다. 이것을 보여주는 버전을 추가하겠습니다.
acjay

9
콜백과 약속의 근본적인 차이점은 제어의 역전입니다. 콜백을 사용하면 API가 콜백을 수락 해야 하지만 약속을 통해 API는 약속을 제공 해야 합니다 . 이것이 가장 큰 차이점이며 API 디자인에 광범위한 영향을 미칩니다.
cwharris 2012

@ChristopherHarris 잘 모르겠습니다. 가진 then(callback)콜백 (대신이 콜백을 수용 API에 대한 방법을) 받아들이는 약속에 대한 방법을 IOC의와 함께 아무것도 할 필요가 없습니다. Promise는 컴포지션, 체인 및 오류 처리 (실제로 철도 지향 프로그래밍)에 유용한 한 수준의 간접 지정을 도입하지만 콜백은 여전히 ​​클라이언트에 의해 실행되지 않으므로 실제로 IoC가 없습니다.
dragan.stepanovic

1
@ dragan.stepanovic 당신 말이 맞습니다. 저는 잘못된 용어를 사용했습니다. 차이점은 간접적 인 것입니다. 콜백을 사용하면 결과로 수행해야 할 작업을 이미 알고 있어야합니다. 약속하면 나중에 결정할 수 있습니다.
cwharris 2016
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.