Call back Hell ?

이후띵·2022년 3월 30일
0

프론트_지식백과

목록 보기
6/8

notion에 작성한 것을 옮겨와서 중간에 깨진 부분들이 있을 수 있습니다.

https://librewiki.net/wiki/콜백_지옥

콜백 지옥(callback hell)
은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들정도로 깊어지는 현상을 얘기합니다. 주로 이벤트 처리나 서버 통신과 같은 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장하는데, 가독성이 떨어지면서 코드를 수정하기 어렵습니다.

  • 콜백지옥 예시
    setTimeout(
        (zzz) => {
            console.log(zzz);
    
            setTimeout(
                (name) => {
                    let corpList = name + ' ';
                    console.log(corpList);
    
                    setTimeout(
                        (name) => {
                            corpList += '|| ' + name + ' ';
                            console.log(corpList);
    
                            setTimeout(
                                (name) => {
                                    corpList += '|| ' + name + ' ';
                                    console.log(corpList);
    
                                    setTimeout(
                                        (name) => {
                                            corpList += '|| ' + name + ' ';
                                            console.log(corpList);
                                            setTimeout(
                                                (zz) => {
                                                    console.log(zz);
                                                },
                                                1000,
                                                'Done!'
                                            );
                                        },
                                        1000,
                                        'Crafton'
                                    );
                                },
                                1000,
                                'Channel Talk'
                            );
                        },
                        1000,
                        'Sparta Coding Club'
                    );
                },
                1000,
                'Naver'
            );
        },
        10,
        'Start!'
    );
    
    보시는 바와 같이, 들여쓰기 수준이 과도하게 깊어지고 값이 아래에서 위로 전달되어 가독성이 떨어집니다.
    1. 익명의 콜백 함수를 모두 기명함수로 전환

      let corpList = '';
      
      const start = (zzz) => {
      	console.log(zzz);
      	setTimeout(naver, 1000, 'Naver');
      };
      
      const naver = (name) => {
      	corpList += name + ' ';
      	console.log(corpList);
      	setTimeout(spartaCodingClub, 1000, 'Sparta Coding Club');
      };
      
      const spartaCodingClub = (name) => {
      	corpList += '|| ' + name + ' ';
      	console.log(corpList);
      	setTimeout(channelTalk, 1000, 'Channel Talk');
      };
      
      const channelTalk = (name) => {
      	corpList += '|| ' + name + ' ';
      	console.log(corpList);
      	setTimeout(crafton, 1000, 'Crafton');
      };
      
      const crafton = (name) => {
      	corpList += '|| ' + name + ' ';
      	console.log(corpList);
      	setTimeout(done, 1000, 'Done!');
      
      };
      
      const done = (zzz) => {
      	console.log(zzz);
      };
      
      setTimeout(start, 10, 'Start!');

      위에서 아래로 읽을 수 전달되고, 가독성 부분에서 개선되었습니다.

      한계: 일회성 함수를 전부 변수에 할당하는 것은 코드명을 일일이 따라다녀야 합니다.

    1. Promise를 이용한 방식

      ES6에서 업데이트된 Promise를 이용하는 방식입니다.

      Promise가 뭡네까

      https://sunset-asparagus-5c5.notion.site/async-await-ea1f7cfba83e47619a8bbd7c615b8651

      new Promise((resolve) => {
        setTimeout(() => {
          let name = 'Espresso';
          console.log(name);
          resolve(name);
        }, 500);
      })
        .then((prevName) => {
          return new Promise((resolve) => {
            setTimeout(() => {
              let name = prevName + '|| Naver ';
              console.log(name);
              resolve(name);
            }, 500);
          });
        })
        .then((prevName) => {
          return new Promise((resolve) => {
            setTimeout(() => {
              let name = prevName + '|| Channel Talk ';
              console.log(name);
              resolve(name);
            }, 500);
          });
        })
        .then((prevName) => {
          return new Promise((resolve) => {
            setTimeout(() => {
              let name = prevName + '|| Sparta Coding Club';
              console.log(name);
              resolve(name);
            }, 500);
          });
        })
      .then((prevName) => {
          return new Promise((resolve) => {
            setTimeout(() => {
              let name = prevName + '|| Crafton';
              console.log(name);
              resolve(name);
            }, 500);
          });
        });

      • new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then또는 catch로 넘어가지 않습니다. 따라서 비동기 작업이 완료될 때 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능해집니다.

    1. Generator를 이용한 방식

      const addCoffee = (prevName, name) => {
        setTimeout(() => {
          coffeeMaker.next(prevName ? `${prevName}, ${name}` : name);
        }, 500);
      };
      
      const coffeeGenerator = function* () {
        const espresso = yield addCoffee('', 'Espresso');
        console.log(espresso);
        const americano = yield addCoffee(espresso, 'Americano');
        console.log(americano);
        const mocha = yield addCoffee(americano, 'Mocha');
        console.log(mocha);
        const latte = yield addCoffee(mocha, 'Latte');
        console.log(latte);
      };
      
      const coffeeMaker = coffeeGenerator();
      coffeeMaker.next();
    • 위 코드는 ES6의 Generator를 이용했습니다. function* ()이런 형식으로 작성된 함수가Generator함수입니다. Generator함수를 실행하면 Iterator가 반환되는데 Iterator는 next메서드를 가지고 있습니다. 이 next메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그다음에 등장하는 yield에서 함수의 실행을 멈춥니다.
    • 따라서 비동기 작업이 완료되는 시점마다 next메서드를 호출하면 Generator함수 내부의 소스가 위에서부터 아래로 순차적으로 진행됩니다.
    1. Promise + async/await

      Promise + async/await

      const addCoffee = (name) => {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(name);
          }, 500);
        });
      };
      
      const coffeeMaker = async () => {
        let coffeeList = '';
        let _addCoffee = async (name) => {
          coffeeList += (coffeeList ? ', ' : '') + (await addCoffee(name));
        };
        await _addCoffee('Espresso');
        console.log(coffeeList);
        await _addCoffee('Americano');
        console.log(coffeeList);
        await _addCoffee('Mocha');
        console.log(coffeeList);
        await _addCoffee('Latte');
        console.log(coffeeList);
      };
      
      coffeeMaker();
    • 위 코드는 ES2017애서 추가 된 async/await를 이용한 코드입니다.
    • 비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실직적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고 해당 내용이 resolve된 이후에야 다음으로 진행됩니다.

정리

  • 비동기적인 작업을 수행하기 위해 콜백함수를 익명함수로 전달하는 과정에서 생기는 콜백 지옥을 PromiseGeneratorasync/await등을 사용해서 방지할 수 있다.

예상질문

  • Q. 콜백이 뭡니까? 다른 함수의 인자로 넣어주는 함수. 아니면 어떤 이벤트에 의해 호출되는 함수. 만약 한 함수가 호출되어지고 이어서 다른 함수를 호출되게 하고 싶을 때 사용한다.
  • Q. 콜백 함수는 늘 비동기 처리가 되는 건가요? 그렇지는 않다. 바로 실행할 수도 있고, 비동기로도 실행할 수도 있다. 이것은 콜백 함수가 비동기 함수인지 그 여부에 따라 달라진다.
  • Q. 비동기 처리를 왜 쓰나요?, 뭐가 좋나요? https://from2020.tistory.com/24
    • 대답 flow 자바 스크립트는 싱글쓰레드이며, 코드를 한 줄로 쭉 읽어 내려갑니다. 다시말해, 함수 등으로 인한 분기점이 일어나면 두 갈래로 나뉘어져 코드를 실행하는 것이 아닌, 한 줄기를 이어 나간다는 뜻입니다. 비동기 함수라면, 비동기적으로 실행됩니다. 비동기적 ?

한 코드를 실행하는것이 다음 코드의 실행을 멈추지 않는다는 점 ??????????
동기적이지 않은, 요청에 대한 결과를 기다리지 않는 방식
비동기 vs nonblocking
- 비동기 함수?
자바스크립트의 런타임은 크게 2가지로 구성됩니다.
- (런타임 더 알아보기)
런타임이란 해당 프로그래밍 언어로 작성된 코드가 구동되는 환경
을 말한다. 웹 브라우저와 Node.js가 대표적인 JavaScript 런타임이다. 웹 브라우저의 JavaScript 런타임은 크게 두 가지의 구성 요소로 이뤄져 있다. 바로 JavaScript 엔진과 웹 API이다. 이 중에서 JavaScript 엔진이 JavaScript 코드를 읽고 해석해서 실행하는 것을 담당 하는 것이며, 이는 단일 쓰레드로만 동작한다. 이것이 바로 JavaScript를 단일 쓰레드 기반의 프로그래밍 언어라 하는 이유이다.

💡 * JavaScript 엔진의 메모리 모델
은 크게 두 가지 영역을 정의한다. 하나는 힙(Heap) 영역
으로, 동적으로 생성되는 객체들을 할당하는 곳이며 구조화되지 않은 넓은 메모리 영역을 지칭한다. 나머지 하나는 스택(Stack) 영역
으로, 함수가 실행될 때마다 해당 스택 프레임이 푸시되고 함수가 종료될 때마다 해당 스택 프레임이 팝 되는 영역을 지칭한다. 멀티 프로세싱 환경의 경우 각 프로세스가 독자적인 스택 영역과 힙 영역을 가지지만, 멀티 쓰레딩 환경의 경우 각 쓰레드가 독자적인 스택 영역을 갖고 힙 영역은 함께 공유한다.

💡 * 스택 영역에 푸시되는 함수의 스택 프레임은 곧 그 함수의 실행 맥락(Execution Context)
을 의미한다. 실행 맥락이란 해당 함수를 실행하기 위해 필요한 각종 정보들(ex. 지역 변수)의 집합이라고 생각하면 된다. JavaScript 엔진은 JavaScript 코드의 실행을 시작함과 동시에 전역 실행 맥락(Global Execution Context)을 스택 영역에 푸시
하고, 이후 특정 함수의 호출 문을 만나는 순간 그 함수의 실행 맥락을 스택 영역에 푸시하고 그 함수가 종료될 때 그 실행 맥락을 팝 한다. 여기서 주목할 만한 사실은 JavaScript 코드의 실행이 전부 다 끝나기 전까지는 전역 실행 맥락이 스택 영역에서 팝 되지 않고 남아 있다는 사실이다. 이를 기억하고 있어야 뒤에서 설명할 "스택이 비는 순간"이라는 말의 의미를 조금 더 명확히 이해할 수 있다. 즉 스택이 비는 순간이라는 것은 결국 더 이상 실행할 JavaScript 코드가 없어서 전역 실행 맥락까지 스택 영역에서 팝이 된 경우를 말한다.

  1. 자바스크립트 엔진
    - 태스크 큐
    - 이벤트 루프
    2. 웹 API

웹 브라우저에서 제공하는 기능

Ajax, setTimeout(), axios, fetch, DOM으로 조작, XMLHttpRequest, Canvas webGL webAudio WebRTC Web Storage 이벤트핸들러 등록 등등..

(브라우저 api vs WebAPI?)

WebAPI의 처리는 JavaScript 엔진의 쓰레드와는 다른 쓰레드들에서 이루어진다.

  • JavaScript엔진의 스택에서 실행된 비동기함수가 요청하는 비동기 작업에 대한 정보와 콜백함수를 웹 API를 통해 브라우저에게 넘긴다.
  • 브라우저는 이러한 요청들을 별도의 쓰레드에 위임한다.
  • 그 쓰레드는 해당 요청이 완료되는 순간 전달받았던 콜백함수를 자바스크립트 엔진의 태스크 큐라는 곳에 집어 넣는다.

예) setTimeout() 함수가 실행되면 JavaScript 엔진은 웹 API를 통해 브라우저에게 setTimeout() 작업을 요청하면서 콜백 함수를 전달하고, 브라우저는 이러한 타이머 작업을 별도의 쓰레드에게 위임한다. 그러고 나면 JavaScript 엔진의 스택에서는 setTimeout() 함수의 스택 프레임이 즉시 팝 된다. 그리고 인자로 명시한 시간이 흐르고 나면 해당 타이머 작업을 처리하고 있던 쓰레드는 전달받았던 콜백 함수를 JavaScript 엔진의 태스크 큐에 집어넣게 된다.

https://it-eldorado.tistory.com/86

정리하자면,

비동기 함수를 사용하면 코드의 실행순서를 예측할 수 없기 때문에 필요시에 비동기 처리를 해야한다.

(비동기 함수가 이후 진행되는 코드에 영향을 미친다면, 비동기 처리를 해야한다.)

  • Q. 프로미스가 정확히 뭔가요? https://hees-dev.tistory.com/35?category=856226 이 자료 보면 좋을 거 같아용 자바스크립트 비동기 처리에 사용되는 객체. 비동기 처리 함수는 결과는 나중에 받게 되지만, 대신 프로미스 객체를 즉시 리턴받게 된다. 프로미스가 이행되거나 실패했을 때에 따라서 그 결과값을 비동기로 받게 된다.
  • Q. 프로미스를 쓰면 좋은 점이 무엇인가요?
    1. 비동기 처리를 할 수 있다.

    2. 콜백 지옥을 해결할 수 있다.

      콜백 지옥이란 비동기 처리를 연이어서 처리해야 할 때, 콜백 함수의 인자로 익명 콜백 함수가 들어가고, 또 그 콜백 함수의 인자로 콜백 함수가 또 들어가는 것의 반복으로 코드의 가독성이 떨어지고 유지보수도 어려워지는 현상을 의미한다.

추가 자료)

  • 익명함수
    • 우선 익명함수 자체는 다음과 같은 형태를 가지고 있다. 일반 함수와 비교하여보면, 함수의 이름이 존재하지 않는다.

      function() {
        console.log("hello!");
      }

      익명 함수는 재사용 하지 않는, 한번만 사용할 함수를 위한 개념으로, 따로 함수의 이름을 갖지 않는다. 리터럴(Literal) 방식으로 변수에 담겨 사용하는 함수이다. ("함수의 이름 != 변수" 임을 확실히 이해해야한다.)

      리터럴(Literal) 방식이란 글자 뜻 그대로 "문자 그대로 읽히는 방식"을 의미하며, 일반적으로 변수에 데이터를 넣을 때 사용하는 방식이 리터럴 방식이다.

      // 리터럴 방식let a = 10;
      const b = 20;

      리터럴 방식으로 사용되는 익명 함수는, 변수에 저장되게 된다.

      //익명 함수
      const sayHello = function() {
        console.log("hello!");
      }
      
      sayHello();// 출력: hello!

      다시 한번 말하지만, 함수가 이름을 갖는 것과 변수에 저장되는 것은 다르다. 이 차이점은 어디서 오냐면, 위에서 언급한 호이스팅의 개념에서 온다. 위의 일반함수의 경우, 함수 전체가 전부 맨 위로 올라가므로 함수를 호출하는 위치에 상관없이 사용될 수 있다고 했다. 반면, 리터럴 방식으로 사용되는 익명 함수의 경우, 호이스팅 시 함수를 담는 변수의 선언부만 위로 올라가고, 익명 함수 자체는 변수가 호출되었을 때 실행되기 때문에, 선언부가 호출 위치보다 위에 있어야 한다. 코드로 보면 다음과 같다.

      //익명 함수
      
      sayHello();// Uncaught ReferenceError: Cannot access 'sayHello' before initializationlet sayHello = function() {
        console.log("hello!");
      }
      
      sayHello();// 위에서 에러가 났으니 출력이 나오지 않음
profile
이후띵's 개발일지

0개의 댓글