콜백함수

김하은·2023년 3월 6일
0

콜백함수란?

function aaa(qqq){
	함수로직
  }
 
  aaa(1234) ==> 얘는 인자. 해당함수의 매개변수에 들어가게된다.

  콜백 함수는  이 인자자리에 함수가 들어가는 형태이다. 

즉, 기존에 map((el)=>{}) 이런식으로 적어준 이 형태가 바로 콜백함수인것이다./

어차피 인지 부분이기에, 화살표 함수이거나 function으로쓴 함수이거나는 별로 중요하지 않은 부분이다.

정리하자면, 콜백함수란, 다른 함수의 인자로 들어가는 함수라고 할 수 있다.

setTimeout(()=>{},3000} => 앞에 부분이 첫번째 인자, 뒤에가 두번째 인자이다. 즉 3초뒤에 첫번째 로직 함수를 실행해줘 하면서 실행권한을 setTimeout이라는 함수에 맡긴 것이다.

이렇게 실행권한을 다른 함수에 맡기는용도로 사용되는것이 콜백함수이다.

오늘은 async / await 가 없던때 어떻게 사용했는지어떻게 변하해왔는지를 알아보았다.

초창기 await가 없던때는
XMLHttpRequest("주소",()=>{}) 대충 이런식으로 작성하여 첫번째 인자로 들어온 주소의 요청이 끝나면 두번째 인자인 콜백함수를 실행시켜줘! 라는 식으로 만든다.

함수로 표현해보면

function aaa(qqq){
	외부API에서 데이터 요청하는 로직.
    .
    .
    .
    요청중!
    const result = 받아온 데이터
    qqq(result)
  }
  aaa((result)=>{
  	console.log("요청끝")
    console.log("받아온 데이터는 "+result+"입니다")
    }) ==>aaa야 너 요청끝나면 이 함수 실행시켜줘.

aaa의 인자인 함수가 aaa라는 함수의 매개변수 qqq에 들어가고 일단 api요청을 aaa함수 에서 진행을 하고 그 결과를 qqq에 담아 콜백함수가 실행되게된다.

실습을 하면서 자세히 알아보았다.

1.콜백함수

시나리오

API 요청 ->
1차로 랜덤한숫자 가져오는 API -->
2차로 그 숫자로 koreanJson.com에서 게시물요청 --> 3차로 받아온 게시글의 유저id와 동일한게시물들 요청

html파일에서 수업중 실습을 진행했다.
html에서 작성한 부분의 코드들은 리엑트에서만 작동되는 것이 아니라 다른 뷰,앵귤러 등에서도 이용이 가능하다라는 의미로 tsx를 이용하지 않은것이다.

const myCallback = () => {
                const aa = new XMLHttpRequest();
                aa.open("get", `http://numbersapi.com/random?min=1&max=200`); // get방식의 메서드, 앤드포인트
                aa.send(); // aa요청
                aa.addEventListener("load", function (res) {
                    console.log(res); // res의 target의 response라는 부분에 랜덤으로 받은 숫자가 들어온것이 확인됨. 숫자부분만 필요하기에 split(" ") 띄어쓰기 부분으로 스필릿.배열로 들어가니 0번빼가 숫자.
                    const num = res.target.response.split(" ")[0]; // 랜덤하게 나오는 숫자
                    const bb = new XMLHttpRequest();
                    bb.open("get", `https://koreanjson.com/posts/${num}`);
                    bb.send();
                    bb.addEventListener("load", (res) => {
                        console.log(res); // res의 target의 response라는 부분에 랜덤숫자에 해당하는 게시물 받아옴. 문자열이기에 객체로 바꾸기
                        const userId = JSON.parse(res.target.response).UserId; //JSON.parse하여 객체로 만든 뒤 거기서 UserId를 뽑아오기
                        const cc = new XMLHttpRequest();
                        cc.open(
                            "get",
                            `https://koreanjson.com/posts?userId=${userId}`
                        );
                        cc.send();
                        cc.addEventListener("load", (res) => {
                            console.log(res); // 2단계에서 불러온 게시물의 유저의 전체게시물불러오기.res의 target의 response라는 부분에 배열에 객체들이 들어온형태
                        });
                    });
                }); 
            };

서버에 데이터를 요청하기 위한 XMLHttpRequest 객체 생성
:new XMLHttpRequest();

해당 부분을 변수에 담고, open() 그 객체 중 open이라는 함수를 이용해 두번째인자의 주소에 get으로 send를 사용해 요청을 보낸다.
그리고 addEventListner을 통해 기다리고 기다림이끝나면 콜백함수의 매개변수에 데이터가 들어오게되고 그런식으로

자세히 보게되면 콜백안에 콜백이 들어오는 식으로 작성된다.

이렇게되면 가독성도 떨어지고, 유지보수도 좋지않다.
이런것을 '콜백지옥'이라고 부른다.
콜백지옥을 구글링해보면 다음과 같은 이미지들이 나온다.

이러한 문제 때문에 Promise라는것이 등장하게되었다

2. Promise

Promise란? : 시간이 걸리는 작업을 처리해주는 도구이다.

new Promise() 라는 함수에 콜백함수가 들어간다.

try 로 성공시 받아오는 결과가 resolve라는 곳에 들어가게되고, catch로 실패시 받아오는 결과는 reject로 들어가게된다.그리고 .then()안의 콜백함수에서는 resolve에 들어온 값을 받아 로직을 실행, .catch()안의 콜백함수로는 reject의 값을 받아 로직을 실행하게된다.

해당부분을 작성해보면 이렇다

new Promise((resolve, reject) => {
     // 여기서 API요청을 한다면??
    // 성공시 resolve실행
	   try {
         const result = "철수"; // 성공시 받아온 결과
        resolve(result); //  resolve에 넣기
    } catch (error) {
         reject("실패했습니다"); // 실패시
     }
 })
    .then((res) => {
         // 성공시 실행할 함수 then안에서 실행시키는 콜백함수 res(매개변수임))에는 resolve에 넣은것이 들어옴
        console.log(res); // 철수
     })
    .catch((err) => {
         console.log(err); // 실패했습니다 reject에 넣은것이 들어옴
     });

그런데 이렇게 Promise를 직접 사용하지 않고 그것을 이용한 라이브러리를 사용한다면 훨씬 편하다.

그 라이브러리는 바로.... axios이다!!

axios.get('API 주소') ==> 이부분에 마우스를 올려보면 타입스크립트 리턴이 Promise인것을 알 수 있다.!!

:뒤의부분이 바로 리턴부분이다.

원래 리턴데이터가 있으면 원래자리에 리턴값이 들어오게된다.
axios.get('API 주소') 이 해당부분도 리턴인Promise() 가 들어와서 .then() , .catch() 등을 사용할 수 있게된다.

axios를 html 에서 사용하기.

npm을 통한 설치는 불가하다. 이럴경우에는 axios.cdn이라고 쳐서 맨 아래쪽 unpkg라는 부분의 코드를 스크립트태그이니 해드테그 안에 넣어준다.

cdn: 간단히 말해 다운로드 받을 수 있는 주소

unpkg :npm에 등록된 패키지를 CDN으로 바로 활용 가능한 서비스

const myPromise = () => {
                axios
                    .get(`http://numbersapi.com/random?min=1&max=200`)
                    .then((res) => {
                        const num = res.request.response.split(" ")[0];
                        axios
                            .get(`https://koreanjson.com/posts/${num}`)
                            .then(() => {
                                const userId = JSON.parse(
                                    res.request.response
                                ).UserId; //JSON.parse하여 객체로 만든 뒤 거기서 UserId를 뽑아오기

                                axios
                                    .get(
                                        `https://koreanjson.com/posts?userId=${userId}`
                                    )
                                    .then((res) => {
                                        // res최종결과
                                    });
                            });
                    });
            };

이렇게 작성하는데, 콜백함수랑 다를게 없는 것 같다. 그런데 이 axios 에서 중간에 return을 한다면 현재의 바깥쪽에서 then 사용이 가능하다.

즉,

 const myPromise = () => {
                // 프로미스 체인
                axios
                    .get(`http://numbersapi.com/random?min=1&max=200`)
                    .then((res) => {
                        console.log(res);
                        const num = res.request.response.split(" ")[0];
                        return axios.get(`https://koreanjson.com/posts/${num}`);
                    })
                    .then((res) => {
                        // res에는 return 값이 들어옴
                        const userId = JSON.parse(res.request.response).UserId;
                        return axios.get(
                            `https://koreanjson.com/posts?userId=${userId}`
                        );
                    })
                    .then((res) => {
                        // res최종결과
                        console.log(res);
                    });
            };

현재의 함수에서 return을 해주면 바깥에서 .then이 가능하고 그렇게되면 계단식 모양인 처음과, 콜백함수처럼이 아닌 바로 아래에 쌓이는 모양새가된다.

이렇게 콜백 지옥을 벗어났다.

그러나, 우리가 예상하는 순서대로 나오지 않는다.

다시말하면 순서를 예측하기가 힘들다.

그래서 등장하게된것이 async/await라는 개념이다.

await를 사용해 그것이 끝나야 다음것이 실행되게 만든다.
단, Promise인것 앞에만 await를 붙여줘야할 것!!

const myAsyncAwait = async () => {
                // 실행순서 100% 예측가능. 하나의 await가 끝나야 다음으로 넘어감
                const result = await axios.get(
                    `http://numbersapi.com/random?min=1&max=200`
                );
                console.log(result);
                const num = result.request.response.split(" ")[0];
                const result2 = await axios.get(
                    `https://koreanjson.com/posts/${num}`
                );
                const userId = JSON.parse(result2.request.response).UserId;
                const result3 = await axios.get(
                    `https://koreanjson.com/posts?userId=${userId}`
                );
                console.log(result3.request.response);
            };
//axios, fetch 등 프로미스를 지원하는 기능들의 경우 즉 Promise를 리턴하는 애들에만 await동작함.(Promise를 지원하는 기능)
            //axios, fetch를 기다리는 방법 2가지.
            // 1. .then()활용
            // 2. await 활용. 단, Promise를 리턴시에만 사용가능

axios 직접 만들어보기

일단 클릭하면 API가 요청되게 한다.
따라서 버튼을 만들고, 그에따른 클릭 함수를 만든다.

여기서 axios.get()으로 접근하는데 우리는 일단 axios를 만들것이니 우리가 만들 axios이름.get()이런식으로 작성하면되겠다. 그런데 .get으로 접근하는것을보니 객체의키가 .get이라는 함수인것같다.
따라서 객체를 하나 만들고

const myaxios = { 

}

이 안에 .get:()=>{}
.post:()=>{} 이런식으로 적어준다.

그리고 이 함수의 인자로 API주소가 들어오는데 이부분을 매개변수로 받기위해 소괄호 안에 url이라는 매개변수를 적어준다.

그런데 말했다시피 여기에는 await를 사용할 수 없다.

promise의 앞에 적어주는것인데 현재는 Promise가 사용되지 않기때문이다.
원래 axios도 Promise를 리턴하니 안에 return new Promise를 사용해 준다. 이렇게되면 함수 호출시에 .then()이나 await등이 사용가능하게된다.

그리고 앞에서 말했던 Promise방식과 동일하게 작성해준다.

 const myaxios = {
        // myaxios.get하면 리턴부분이 원래있던 주소 부분으로 들어가
        get: (url) => {
          return new Promise((resolve, reject) => {
            // return new Promise 했기에 then 사용할수 있게되고 순서 예측이 가능할 수 있게 Promise앞에 await를 붙여줌
            const qq = new XMLHttpRequest();
            qq.open("get", url); //url로 api요청
            qq.send();
            qq.addEventListener("load", (res) => {
              resolve(res.target.response); // 다끝나면 resolve됨
            });
          });
        },
        //   post: (url) => {}, // 얘도 같은 방식
      };

그리고 이것을 사용해 axios를 요청한다.

     const onClickAxios = async () => {
   //  axios 요청
        //axios.get()  // .get이라고 사용되니 아마 axios는 객체. 그리고 뒤에 get() 이니 함수
        //axios.post() // 동일

        // 1) .then()으로 받기
    myaxios.get("https://koreanjson.com/posts/1").then((res) => {
          console.log("여기는 .then(): ", res);
        }); // 여기 로 들어와 그럼 얘네가 propmise가 되어  뒤에 .then 으로 받을 수도 있고, 아니면 앞에 await로 받을 수 있음

        // 2) await 로 받기
        const result = await myaxios.get("https://koreanjson.com/posts/1");
        console.log("여기는  await: ", result);
      };

이벤트 루프때 Queue에 들어오는 부분은 콜백함수부분!!

다시말하면 setTimeout 등 시간이 걸리는 작업 부분이 Queue에 저장된다고했는데, 정확하게는 해당함수에 들어가는 인자인 콜백함수가 들어간다.

이 콜백함수가 바깥쪽 , 벡그라운드에서 정해놓은 시간(일정시간 ) 동안 기다리다가 시간이 지나면 큐에 들어와 스택이 비워지기를 기다린다. 따라서 이때의 queue를 콜백 큐 라고 한다.

==> 이렇게 뒤쪽으로빠져서 처리되는것을 비동기 작업 이라고 한다.

메크로큐(=테스크큐) , 마이크로큐

setTimeout(), setInterval() 등이 메크로큐에 , Promise를 가지는애들, API를 기다리는 애들이 마이크로큐에 들어온다.

둘의 차이?

마이크로큐의 우선순위가 더 높다.

따라서 마이크로큐의 부분이 다 끝나야 메크로큐(테스크큐)부분이 실행된다.

그렇다면 테스크큐를 실행하는 도중에 마이크로큐 부분에실행할 함수가 또 들어온다면??

그렇게되어도 무조건 마이크로큐 부분이 우선이기에 다시 마이크로큐부터 우선실행되고 난 뒤 남은 메크로큐를 실행한다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>이벤트 루프</title>
    <script>
      const onClickLoop = () => {
        console.log("시작!!!"); // 1번째

        // 비동기작업. (테스크큐에 들어감)
        setTimeout(() => {
          new Promise((resolve) => resolve("철수")).then(() => {
            console.log("저는 Promise (setTimeout 안에서 실행될거예요)");
          });
          console.log("저는 setTimeout , 마이크로큐 !!0초뒤에 실행될거예요"); // 제일 마지막에 나옴
        }, 0);

        // 비동기작업. (마이크로큐에 들어감)
        new Promise((resolve) => resolve("철수")).then(() => {
          console.log("저는 Promise 1 !! 마이크로큐 : 0초뒤에 실행될거예요");
        });
        // 비동기작업. (테스크큐에 들어감)
        setInterval(() => {
          console.log("저는 setInterval 메크로큐!! 0초마다 실행될거예요");
        }, 0);
        let sum = 0;
        for (let i = 0; i < 9000000000; i++) {
          // 이 반복문을 추가해도 0초뒤에 시실행하는 부분이 먼저 안나오고 젤 마지막에 0초뒤에 실행되는게 나옴
          sum += i;
        }
        // 비동기작업. (마이크로큐에 들어감)
        new Promise((resolve) => resolve("철수")).then(() => {
          console.log("저는 Promise 2 !! 마이크로큐 : 0초뒤에 실행될거예요");
        });
        console.log("끝!!"); // 두번째
      };
    </script>
  </head>
  <body>
    <button onclick="onClickLoop()">시작하기</button>
  </body>
</html>

일단 콜스텍 부터 실행된다. 버튼을 클릭하면 안에 "시작" 이라는 부분이 콘솔로 찍히고 나머지는 일정시간 기다리는 함수이기에 큐에 들어간다. 이중 Promise아이들은 마이크로큐에 들어가고 나머지는 테스크큐(메크로큐)에 들어간다.
그렇게 큐에 들어갈 부분이 들어가게되면 아래쪽에 for문에서 병목현상이 일어나 좀 시간이 걸리게되고 그 후에 맨아래 콘솔 "끝" 이 찍히게된다.

콜스택이 다 실행되니 이제는 큐를 돌 차례다. 이벤트 루프 쓰레드가 먼저 확인하는 큐는 마이크로큐이다.
마이크로 큐에 들어가는 Promise부분. Promise 1 부터 콘솔에 찍히고, 그다음 Promise 2가 찍힌다.

일단 마이크로 큐 부분은 다 비워졌으니 쓰레드가 이번에는 메크로큐를 확인한다. 그런데 처음부분의 메크로큐를 실행하다가 Promise를 만난다. 얘는 마이크로큐 부분이니 마이크로큐에 들어간다. 그리고 마이크로큐가 우선 실행되니 쓰레드는 다시 마이크로큐로 가서 이 Promise를 실행해
"저는 Promise (setTimeout 안에서 실행될거예요)"
가 콘솔에 찍히게 된다.

그다음 마이크로큐가 다 비워지면 메크로큐(테스크큐)에 가서 원래 실행되고있었던 부분에 돌아가 마저 실행되고 그다음에 들어와있는 애들을 실행시킨다.


await에 대하여

await와 async를 같이 쓰는 이유

await 를 만나면 얘를 감싸는 async function이 하던일을 중단하고 마이크로 큐 부분에 들어간다. 그럼 이 함수가 어디까지 실행했는지를 기록만 한뒤, 해당함수는 일단 종료된다.

async는 await를 감싸는 function이다! 라는것을 알려주는 것!!


이번엔 버튼이 클릭되었을시 동작이 실행되기도 전에 또 클릭되는것을 막기위한 로직을 작성해 보았다.

import axios from "axios";
import { useState } from "react";

export default function IsSubmitingPage() {
  //  버튼이 눌리고 로딩시간동안 버튼을 또 못누르게 막기.즉. 버튼한번클릭하면 또 클릭 못하게
  const [isSubmitting, setIsSubmitting] = useState(false);

  const [myData, setMyData] = useState<any>();

  const onClickSubmit = async () => {
    setIsSubmitting(true); // 버튼클릭시 disabled가 true가되고
    const result = await axios.get("https://koreanjson.com/posts/1");
    console.log(result);
    setMyData(result);

    setIsSubmitting(false); // 결과 다 받고 false로 되돌림
  };
  // disabled={isSubmitting}으로 제출중에는 disabled시킴. 즉, 클릭안됨
  return (
    <>
      <button onClick={onClickSubmit} disabled={isSubmitting}>
        등록하기 등의 API 요청 버튼
      </button>
    </>
  );
}

state를 사용해 값을 저장하고 기본을 false로 지정한다.

이렇게되면 버튼이 클릭시 일단 setSubmitting으로 true로 변경시에 버튼이 비활성화되고, API요청으로 결과를 받아온 후 setSubmitting을 다시 flase로 disabled를 해재해준다.

setState는 함수 내부에서 여러번 사용되어도 제일 나중에 바뀐걸로 최종 반영되는것이 맞다. 그런데 어떻게 이렇게 작성했는데도 원하는 결과가 나올까?

await에 대한 이해가 필요한 부분이다.

먼저 해당 문제를 풀어보았다

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>이벤트루프</title>
    <script>
      function onClickLoop() {
        console.log("=======시작!!!!=======");

        function aaa() {
          console.log("aaa-시작");
          bbb();
          console.log("aaa-끝");
        }

        async function bbb() {
          console.log("bbb-시작");
          const friend = await "철수";
          console.log(friend);
        }

        aaa();

        console.log("=======끝!!!!=======");
      }
    </script>
  </head>
  <body>
    <button onclick="onClickLoop()">시작하기</button>
  </body>
</html>

먼저 작동되어 찍힐것 같은 순서로 적어보았다.

일단 맨 먼저
=======시작!!!!======= 이부분이 찍히게될것은 확실하다

그다음 쭉 함수를 선언해주고 아래쪽에 aaa함수가 실행되니 aaa함수로 간다.
그럼 aaa-시작 이 찍힐것이고,
그 아래로 내려가는데 bbb의 함수가 실행된다. 그럼 곧바로 bbb의 함수로 가서 실행시키는데
일단 bbb-시작 부분이 찍히고,
await가 보이므로 이 함수 전체가 큐에 들어가게된다.
큐에 들어가게되면 일단 그 함수는 종료된 것으로 보기에 bbb함수는 일단끝이나고, 그다음 bbb함수를 실행시켰던 aaa함수로 다시 돌아와 aaa-끝 이 찍히고 아직 남아있는 콜스택이 있는지를 확인해본다.
최종적으로 콜스텍에 남아있는 부분인
=======끝!!!!======= 이 찍히고,
마지막에 큐로가서 await하여 담아놓았던
철수 가 마지막으로 찍히게된다.

답:
1. =======시작!!!!=======
2. aaa-시작
3. bbb-시작
4. aaa-끝
5. =======끝!!!!=======
6. 철수

다음문제

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>이벤트루프</title>
    <script>
      function onClickLoop() {
        console.log("=======시작!!!!=======");

        function aaa() {
          console.log("aaa-시작"); //1
          bbb(); //2
          console.log("aaa-끝"); // 7 aaa 끝
        }

       async function bbb() {
          console.log("bbb-시작"); //3
          await ccc(); // 여기가 포인트 await 는 ccc와는 관련없음. 따라서 여기의 await는 무시 // 4
          console.log("bbb-끝"); // 6 // await 를 만났으니 마이크로큐에들어감 // 10 두번째로 큐에 들어간애꺼내실행 bbb-끝
        }

        async function ccc() {
          console.log("ccc-시작"); // 5
          const friend = await "철수"; // 마이크로큐에 들어가고 ccc는 끝남.// 이게 먼저 큐에 들어가니 얘먼저 실행 9
          console.log(friend);
        }

        aaa();

        console.log("=======끝!!!!======="); // 8 끝. 콜스텍을 다 돌아 비워짐
      }
    </script>
  </head>
  <body>
    <button onclick="onClickLoop()">시작하기</button>
  </body>
</html>

마찬가지로 =======시작!!!!======= 이부분이 먼저 찍힌다.
그다음 함수를 선언하고 맨아래쪽 aaa함수 실행을 만나면 aaa함수를 실행해 aaa-시작 이 찍히고 그다음 bbb가 실행된다.

bbb로 가 먼저 bbb-시작 이 찍히는데 ccc함수실행부분에 await가 붙어있다.

이부분에서 헷갈렸는데,먼저 저때의 await는 별로 신경쓰지 말고 ccc로 넘어간다.
ccc함수가 실행되고 ccc-시작 찍히며,await가 붙었으니 해당함수가 큐에 들어간다. 그리고 해당함수는 일단 종료.

그러면 이 ccc함수를 실행했던 bbb함수로 들어와
bbb-끝 이 찍히며 해당함수가 종료, 되야하지만 async await 부분이니 통채로 큐에 들어가고
bbb 함수도 일단은 종료되며 해당 함수를 실행시킨 aaa가 종료되고 =======끝!!!!======= 이 찍힌다. 마지막으로 맨먼저 큐에 들어가 있던 ccc부분의 철수 가 먼저찍히고, 그다음에 bbb-끝 이 찍히며
끝난다.

답:
1.=======시작!!!!=======
2.aaa-시작
3.bbb-시작
7.ccc-시작
4. aaa-끝
5.=======끝!!!!=======
8.철수
6.bbb-끝

함수 실행부분에 await가 붙으면 일단 실행은되고 그 함수가 끝나게되면 다시 돌아와 실행함수부분을 담고있는 async function부분이 통째로 들어가는것 같다.

그렇다면 다시 버튼 예제로 돌아가보자.

setIsSubmitting(true); // 버튼클릭시 disabled가 true가되고
    const result = await axios.get("https://koreanjson.com/posts/1");
    console.log(result);
    setMyData(result); // state에  받아온 결과 저장

    setIsSubmitting(false); 

state를 위에서도 아래에서도 실행했는데 어떻게 이것이 가능한지는 await를 사용하여 알 수 있었다.
일단처음 실행되다가
await를 만남과 동시에 해당부분은 큐에들어가 해당함수는 일단 종료된다. 그리고 위에서부터 실행되는데
setIsSubmitting(false); 의 위의 부분은 큐에 들어가있으니 스택을 돌아도 받아올게 없다. 그러니 다시 큐에가서 해당부분들을 받아오고 그제서야 setIsSubmitting(false); 얘가 실행된다.

요약하자면, 위쪽의 setIsSubmitting(true); 가 먼저 실행된후 한번끊기기에 (한호흡 끊고가기에) 로직안에서 state를 두번사용했는데도 해당 부분이 제대로 작동되는것이다.

0개의 댓글