[javascript] “콜백 지옥”이란 무엇이며 RX가이를 해결하는 방법과 이유는 무엇입니까?

누군가 JavaScript와 node.js를 모르는 사람을 위해 “콜백 지옥”이 무엇인지 설명하는 간단한 예제와 함께 명확한 정의를 제공 할 수 있습니까?

언제 (어떤 종류의 설정에서) “콜백 지옥 문제”가 발생합니까?

왜 발생합니까?

“콜백 지옥”은 항상 비동기 계산과 관련이 있습니까?

아니면 단일 스레드 애플리케이션에서도 “콜백 지옥”이 발생할 수 있습니까?

나는 Coursera에서 Reactive Course를 수강했고 Erik Meijer는 그의 강의 중 하나에서 RX가 “콜백 지옥”문제를 해결한다고 말했습니다. Coursera 포럼에서 “콜백 지옥”이 무엇인지 물었지만 명확한 답을 얻지 못했습니다.

간단한 예에서 “콜백 지옥”을 설명한 후, RX가이 간단한 예에서 “콜백 지옥 문제”를 어떻게 해결하는지 보여줄 수 있습니까?



답변

1) javascript와 node.js를 모르는 사람을위한 “콜백 지옥”이란 무엇입니까?

이 다른 질문에는 Javascript 콜백 지옥의 몇 가지 예가 있습니다. Node.js에서 비동기 함수의 긴 중첩을 피하는 방법

자바 스크립트의 문제는 계산을 “고정”하고 “나머지”가 후자 (비동기 적으로) 실행되도록하는 유일한 방법은 “나머지”를 콜백 안에 넣는 것입니다.

예를 들어 다음과 같은 코드를 실행하고 싶다고 가정합니다.

x = getData();
y = getMoreData(x);
z = getMoreData(y);
...

이제 getData 함수를 비동기로 만들려면 어떻게됩니까? 즉, 값이 반환되기를 기다리는 동안 다른 코드를 실행할 수있는 기회가 생깁니다. Javascript에서 유일한 방법은 연속 전달 스타일을 사용하여 비동기 계산에 영향을주는 모든 것을 다시 작성하는 것 입니다 .

getData(function(x){
    getMoreData(x, function(y){
        getMoreData(y, function(z){
            ...
        });
    });
});

나는이 버전이 이전 버전보다 더 추악하다는 것을 누구에게도 설득 할 필요가 없다고 생각합니다. 🙂

2) “콜백 지옥 문제”는 언제 (어떤 종류의 설정에서) 발생합니까?

코드에 콜백 함수가 많을 때! 코드에 더 많이 포함할수록 작업하기가 더 어려워지고 루프, try-catch 블록 등을 수행해야 할 때 특히 나빠집니다.

예를 들어, 내가 아는 한, JavaScript에서 이전 반환 후 실행되는 일련의 비동기 함수를 실행하는 유일한 방법은 재귀 함수를 사용하는 것입니다. for 루프를 사용할 수 없습니다.

// we would like to write the following
for(var i=0; i<10; i++){
    doSomething(i);
}
blah();

대신 다음과 같이 작성해야 할 수도 있습니다.

function loop(i, onDone){
    if(i >= 10){
        onDone()
    }else{
        doSomething(i, function(){
            loop(i+1, onDone);
        });
     }
}
loop(0, function(){
    blah();
});

//ugh!

이런 종류의 일을 수행하는 방법을 묻는 StackOverflow에 대한 질문의 수는 그것이 얼마나 혼란 스러웠는지에 대한 증거입니다 🙂

3) 왜 발생합니까?

JavaScript에서 비동기 호출이 반환 된 후 실행되도록 계산을 지연하는 유일한 방법은 지연된 코드를 콜백 함수 안에 넣는 것입니다. 기존의 동기 스타일로 작성된 코드를 지연시킬 수 없으므로 모든 곳에서 중첩 된 콜백이 발생합니다.

4) 아니면 “콜백 지옥”이 단일 스레드 응용 프로그램에서도 발생할 수 있습니까?

비동기 프로그래밍은 동시성과 관련이있는 반면 단일 스레드는 병렬성과 관련이 있습니다. 두 개념은 실제로 같은 것이 아닙니다.

단일 스레드 컨텍스트에서 동시 코드를 계속 가질 수 있습니다. 사실 콜백 지옥의 여왕 인 JavaScript는 단일 스레드입니다.

동시성과 병렬성의 차이점은 무엇입니까?

5) 간단한 예제에서 RX가 “콜백 지옥 문제”를 어떻게 해결하는지 보여 주시겠습니까?

특히 RX에 대해 잘 모르지만 일반적으로이 문제는 프로그래밍 언어에서 비동기 계산에 대한 기본 지원을 추가하여 해결됩니다. 구현은 다양 할 수 있으며 비동기, 생성기, 코 루틴 및 callcc를 포함합니다.

파이썬에서 우리는 이전 루프 예제를 다음과 같이 구현할 수 있습니다.

def myLoop():
    for i in range(10):
        doSomething(i)
        yield

myGen = myLoop()

이것은 전체 코드는 아니지만 누군가 myGen.next ()를 호출 할 때까지 “yield”가 for 루프를 일시 중지한다는 아이디어입니다. 중요한 것은 우리가 재귀 loop함수 에서했던 것처럼 로직을 “내부”로 만들 필요없이 for 루프를 사용하여 코드를 작성할 수 있다는 것입니다 .


답변

질문에 답하십시오. RX가이 간단한 예제에서 “콜백 지옥 문제”를 어떻게 해결하는지 보여 주시겠습니까?

마법은 flatMap입니다. @hugomg의 예를 위해 Rx에 다음 코드를 작성할 수 있습니다.

def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
         .flatMap(y -> Observable[Z])
         .map(z -> ...)...

동기식 FP 코드를 작성하는 것과 같지만 실제로 Scheduler.


답변

Rx가 콜백 지옥을 해결하는 방법에 대한 질문을 해결하려면 다음을 수행하십시오.

먼저 콜백 지옥에 대해 다시 설명하겠습니다.

사람, 행성 및 은하의 세 가지 자원을 얻기 위해 http를 수행해야하는 경우를 상상해보십시오. 우리의 목표는 그 사람이 살고있는 은하계를 찾는 것입니다. 먼저 우리는 그 사람, 그 다음 행성, 그리고 은하계를 얻어야합니다. 3 개의 비동기 작업에 대한 3 개의 콜백입니다.

getPerson(person => {
   getPlanet(person, (planet) => {
       getGalaxy(planet, (galaxy) => {
           console.log(galaxy);
       });
   });
});

각 콜백은 중첩됩니다. 각 내부 콜백은 상위에 종속됩니다. 이것은 콜백 지옥의 “파멸의 피라미드”스타일로 이어집니다. . 코드는> 기호처럼 보입니다.

RxJ에서 이것을 해결하려면 다음과 같이 할 수 있습니다.

getPerson()
  .map(person => getPlanet(person))
  .map(planet => getGalaxy(planet))
  .mergeAll()
  .subscribe(galaxy => console.log(galaxy));

으로 mergeMap일명 flatMap연산자 당신은 더 간결 만들 수 있습니다 :

getPerson()
  .mergeMap(person => getPlanet(person))
  .mergeMap(planet => getGalaxy(planet))
  .subscribe(galaxy => console.log(galaxy));

보시다시피 코드는 평면화되고 단일 메서드 호출 체인을 포함합니다. 우리에게는 “파멸의 피라미드”가 없습니다.

따라서 콜백 지옥은 피합니다.

궁금한 점이 있다면 promise 는 콜백 지옥을 피할 수있는 또 다른 방법입니다.하지만 promise는 observable처럼 게으르지 않고 열성적 이며 (일반적으로 말해서) 쉽게 취소 할 수 없습니다.


답변

콜백 지옥은 비동기 코드에서 함수 콜백 사용이 모호하거나 따르기 어려운 코드입니다. 일반적으로 간접 수준이 두 개 이상인 경우 콜백을 사용하는 코드는 따라 가기 어렵고 리팩토링하기가 더 어려워지고 테스트하기가 더 어려워 질 수 있습니다. 코드 냄새는 여러 계층의 함수 리터럴을 전달하기 때문에 여러 수준의 들여 쓰기입니다.

이것은 종종 행동에 의존성이있을 때 발생합니다. 즉, B가 C보다 먼저 발생하기 전에 A가 발생해야 할 때 발생합니다. 그러면 다음과 같은 코드가 생성됩니다.

a({
    parameter : someParameter,
    callback : function() {
        b({
             parameter : someOtherParameter,
             callback : function({
                 c(yetAnotherParameter)
        })
    }
});

이와 같은 코드에 동작 의존성이 많으면 문제가 빠르게 발생할 수 있습니다. 특히 분기하면 …

a({
    parameter : someParameter,
    callback : function(status) {
        if (status == states.SUCCESS) {
          b(function(status) {
              if (status == states.SUCCESS) {
                 c(function(status){
                     if (status == states.SUCCESS) {
                         // Not an exaggeration. I have seen
                         // code that looks like this regularly.
                     }
                 });
              }
          });
        } elseif (status == states.PENDING {
          ...
        }
    }
});

이건 안돼. 이러한 콜백을 모두 전달할 필요없이 어떻게 비동기 코드를 정해진 순서대로 실행할 수 있습니까?

RX는 ‘반응 적 확장’의 약자입니다. 나는 그것을 사용하지 않았지만 인터넷 검색은 그것이 이벤트 기반 프레임 워크라고 제안합니다.이벤트는 깨지기 쉬운 결합을 생성하지 않고 코드를 순서대로 실행하는 일반적인 패턴 입니다. C가 ‘aFinished’를 듣고 B가 호출 된 후에 만 ​​발생하는 이벤트 ‘bFinished’를 수신하도록 할 수 있습니다. 그런 다음 쉽게 추가 단계를 추가하거나 이러한 종류의 동작을 확장 할 있으며 테스트 케이스에서 이벤트를 브로드 캐스팅하여 코드가 순서대로 실행되는지 쉽게 테스트 할 수 있습니다 .


답변

Call back hell은 다른 콜백 내부의 콜백 내부에 있으며 필요가 가득 차지 않을 때까지 n 번째 호출로 이동 함을 의미합니다.

set timeout API를 사용하여 가짜 ajax 호출의 예를 통해 이해하고 레시피 API가 있다고 가정하고 모든 레시피를 다운로드해야합니다.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
            }, 1500);
        }
        getRecipe();
    </script>
</body>

위의 예에서 타이머가 만료되면 1.5 초 후 콜백 코드 내부에서 실행됩니다. 즉, 가짜 아약스 호출을 통해 모든 레시피가 서버에서 다운로드됩니다. 이제 특정 레시피 데이터를 다운로드해야합니다.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

특정 레시피 데이터를 다운로드하기 위해 첫 번째 콜백 내부에 코드를 작성하고 레시피 ID를 전달했습니다.

이제 ID가 7638 인 레시피의 동일한 게시자의 모든 레시피를 다운로드해야한다고 가정 해 보겠습니다.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                    setTimeout(publisher=>{
                        const recipe2 = {title:'Fresh Apple Pie', publisher:'Suru'};
                        console.log(recipe2);
                    }, 1500, recipe.publisher);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

게시자 이름 suru의 모든 레시피를 다운로드해야하는 우리의 요구를 충족시키기 위해 두 번째 콜백 내부에 코드를 작성했습니다. 콜백 지옥이라는 콜백 체인을 작성 했음이 분명합니다.

콜백 지옥을 피하고 싶다면 js es6 기능인 Promise를 사용할 수 있습니다. 각 promise는 promise가 가득 차면 호출되는 콜백을받습니다. 프라 미스 콜백에는 해결되거나 거부되는 두 가지 옵션이 있습니다. 당신의 API 호출이이 결의를 호출하고를 통해 데이터를 전달할 수 있습니다 성공적으로 가정 해결 , 당신은 사용하여이 데이터를 얻을 수 있습니다 다음 () . 그러나 API가 실패하면 거부를 사용하고 catch 를 사용 하여 오류를 포착 할 수 있습니다 . 항상 사용 약속을 기억 한 후 해결을 위해 캐치 거부에 대한

promise를 사용하여 이전 콜백 지옥 문제를 해결해 보겠습니다.

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        getIds.then(IDs=>{
            console.log(IDs);
        }).catch(error=>{
            console.log(error);
        });
    </script>
</body>

이제 특정 레시피를 다운로드하십시오.

<body>
    <script>
        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }
        getIds.then(IDs=>{
            console.log(IDs);
            return getRecipe(IDs[2]);
        }).
        then(recipe =>{
            console.log(recipe);
        })
        .catch(error=>{
            console.log(error);
        });
    </script>
</body>

이제 우리는 약속을 반환하는 getRecipe 와 같은 또 다른 메서드 호출 allRecipeOfAPublisher 를 작성할 수 있으며, 또 다른 then ()을 작성하여 allRecipeOfAPublisher에 대한 해결 약속을받을 수 있습니다.이 시점에서 직접 할 수 있기를 바랍니다.

그래서 우리는 프라 미스를 구성하고 소비하는 방법을 배웠습니다. 이제 es8에 도입 된 async / await를 사용하여 더 쉽게 프라 미스를 소비하도록하겠습니다.

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }

        async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

        getRecipesAw();
    </script>
</body>

위의 예에서는 백그라운드에서 실행되기 때문에 async 함수를 사용했습니다. async 함수 내 에서 약속이 이행 될 때까지 해당 위치를 기다리기 때문에 약속 인 각 메서드 앞에 await 키워드를 사용 했습니다. getIds가 해결 될 때까지 코드를 벨로우즈하거나 프로그램을 거부하면 ID가 반환 될 때 해당 줄 아래에서 코드 실행이 중지되고 다시 ID로 getRecipe () 함수를 호출하고 데이터가 반환 될 때까지 await 키워드를 사용하여 기다립니다. 그래서 이것이 우리가 콜백 지옥에서 마침내 회복 한 방법입니다.

  async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

await를 사용하려면 비동기 함수가 필요합니다. promise를 반환 할 수 있으므로 resolve promise에는 then을 사용하고 거부 promise에는 cath를 사용합니다.

위의 예에서 :

 async function getRecipesAw(){
            const IDs = await getIds;
            const recipe = await getRecipe(IDs[2]);
            return recipe;
        }

        getRecipesAw().then(result=>{
            console.log(result);
        }).catch(error=>{
            console.log(error);
        });


답변

콜백 지옥을 피할 수있는 한 가지 방법은 RX의 “향상된 버전”인 FRP를 사용하는 것입니다.

최근에 FRP를 사용하기 시작했습니다 . Sodium( http://sodium.nz/ ) 라는 좋은 구현을 찾았 기 때문 입니다.

일반적인 코드는 다음과 같습니다 (Scala.js).

def render: Unit => VdomElement = { _ =>
  <.div(
    <.hr,
    <.h2("Note Selector"),
    <.hr,
    <.br,
    noteSelectorTable.comp(),
    NoteCreatorWidget().createNewNoteButton.comp(),
    NoteEditorWidget(selectedNote.updates()).comp(),
    <.hr,
    <.br
  )
}

selectedNote.updates()Stream발광하는 경우 selectedNode(a 인 Cell변화)는이 NodeEditorWidget후 대응 업데이트한다.

따라서의 내용에 따라 selectedNode Cell현재 편집 된 내용 Note이 변경됩니다.

이 코드는 Callback-s를 완전히 피합니다. 거의 Cacllback-s가 앱의 “외부 계층”/ “표면”으로 푸시됩니다. 여기서 상태 처리 논리는 외부 세계와 인터페이스합니다. 내부 상태 처리 로직 (상태 머신을 구현 함) 내에서 데이터를 전파하는 데 필요한 콜백이 없습니다.

전체 소스 코드는 여기에 있습니다.

위의 코드 조각은 다음과 같은 간단한 만들기 / 표시 / 업데이트 예제에 해당합니다.

여기에 이미지 설명 입력

이 코드는 또한 서버에 업데이트를 보내므로 업데이트 된 엔터티에 대한 변경 사항이 서버에 자동으로 저장됩니다.

모든 이벤트 처리는 Streams 및 Cells 를 사용하여 처리됩니다 . 이것이 FRP 개념입니다. 콜백은 FRP 로직이 사용자 입력, 텍스트 편집, 버튼 누름, AJAX 호출 반환과 같은 외부 세계와 인터페이스하는 경우에만 필요합니다.

데이터 흐름은 FRP (Sodium 라이브러리에 의해 구현 됨)를 사용하여 선언적 방식으로 명시 적으로 설명되므로 데이터 흐름을 설명하는 데 이벤트 처리 / 콜백 로직이 필요하지 않습니다.

FRP (RX의보다 “엄격한”버전)는 상태를 포함하는 노드를 포함 할 수있는 데이터 흐름 그래프를 설명하는 방법입니다. 이벤트는 노드 ( Cells 라고 함 )를 포함하는 상태에서 상태 변경을 트리거 합니다.

Sodium은 고차 FRP 라이브러리입니다. 즉, flatMap/ switch원시 라이브러리를 사용하면 런타임에 데이터 흐름 그래프를 재정렬 할 수 있습니다.

나는 Sodium 책을 살펴 보는 것이 좋습니다 FRP가 외부 자극에 대한 응답으로 애플리케이션 상태를 업데이트하는 것과 관련이있는 데이터 흐름 논리를 설명하는 데 필수적이지 않은 모든 콜백을 제거하는 방법을 자세히 설명합니다.

FRP를 사용하면 외부 세계와의 상호 작용을 설명하는 콜백 만 유지하면됩니다. 즉, 데이터 흐름은 FRP 프레임 워크 (예 : Sodium)를 사용하거나 “FRP 유사”프레임 워크 (예 : RX)를 사용할 때 기능적 / 선언적 방식으로 설명됩니다.

Sodium은 Javascript / Typescript에도 사용할 수 있습니다.


답변

콜백 및 지옥 콜백에 대한 지식이없는 경우 문제가 없습니다. 이제 문제는 콜백 및 지옥 콜백입니다. 예 : 지옥 콜백은 클래스 내부에 클래스를 저장할 수있는 것과 같습니다. C, C ++ 언어에 중첩 된 것에 대해 중첩 된 것은 다른 클래스 내부의 클래스를 의미합니다.