[JS] 콜백함수 비동기실행 이해

colki·2021년 4월 23일
0

동기, 비동기에 대해서 공부하고 있는데
개념으로는 알겠지만 막상 코드를 읽고 또 응용해서 사용하려니 이해가 잘 되지 않았다.
특히 callback함수가 홍길동마냥 여기 번쩍 저기도 번쩍 하는데,
이들을 어떻게 어디서부터 읽어 내려가야 하는 것인지 몰라서 고통의 시간을 보냈었다.

그래서 조금이나마 알게 된 비동기함수의 동시 실행(병렬)과 하나가 실행되면 그 다음이 실행되는(직렬) 방법에 대해 정리해보려고 한다.

비동기함수 병렬 실행


function foo(callback) {
  setTimeout(() => {
    console.log('일');
    callback();
  }, 150)
}
// '일'

위와 같이 생긴 함수가 있다.
보아하니 callback이라는 매개변수를 받고
150ms가 지나면 콘솔에 '일'을 출력하고
매개변수로 받은 callback을 실행하는 것 같다.
callback은 함수라는 거구나! 오케이

그렇다면, 이렇게 생긴 함수가 여러개 들어있는 배열[f, f, f]을 받아서 순서대로 동작시키려는 executeCallback함수가 있다.
for...of 로 배열의 요소들을 순회하고 호출이 됐다면
콜백호출 몇 번째 라고 콘솔에 출력시키는 일을 한다.

var functions = [
  function foo(callback) {
    setTimeout(() => {
      console.log('일');
      callback();
    }, 150)
  },
  function bar(callback) {
    setTimeout(() => {
      console.log('이');
      callback();
    }, 50)
  },
  function bar(callback) {
    setTimeout(() => {
      console.log('삼');
      callback();
    }, 10)
  },
];

function executeCallback() {
  var count = 0;

  for(const func of functions) {
    func(function callback() {
      console.log(`콜백 호출 ${++count}번째`)
    });
  }   
}

executeCallback(functions);

콘솔에 어떻게 찍힐 지 잠시 생각해보자. 🤔

.
.
.
.

"일"
"콜백 호출 1번째"

"이"
"콜백 호출 2번째"

"삼"
"콜백 호출 3번째"

이라고 생각했다면 정답!

이 아니라 틀렸다.
콘솔에는 다음과 같이 찍힐 것이다.

"삼"
"콜백 호출 1번째"

"이"
"콜백 호출 2번째"

"일"
"콜백 호출 3번째"

콜백 호출은 순서대로 잘 나오고 있는데
일 이 삼 이 아니라 삼 이 일 거꾸로 나온다.
이건 바로 setTimeout이 비동기함수이기 때문이다.

functions[0] >> functions[1] >> functions[2]

이렇게 순서대로 시작되는게아니라
for...of 가 먼저 실행컨텍스트에서 실행되고, webAPI가 자기 자기 자식인 setTimeout을 따로
데리고 있다가 setTimeout의 시간(10, 50, 150)이 지나고, 동기적으로 실행하는 함수들의 일이 끝나고 나면,
그때서야 큐에 담는다. 그리고 setTimeout은 순서대로 하나씩 실행컨텍스트로 담겨져 실행된다.

그래서 이때의 setTimeout은 출발선은 똑같다.
다만 도착시간이 다를 뿐이다.

그림으로 나타내면 다음과 같고 이렇게 실행되는 걸 parlell, 병렬로 실행된다고 한다.

그리고 이해가 또 안됐던 부분이 있는데

executeCallback(functions)

executeCallback 함수에 functions라는 함수배열이 담기는데
executeCallback함수 안에 callback 함수가 선언이 되어있는데 거기에 인자로
callback함수실행문을 가지고 있는 함수들이 들어간다..? 근데 또 개네 매개변수도 callback이라니?
여기서 콜백탈트가 왔다 😑

그런데 함수는 reference이기 때문에 이렇게 생각하면 안되는 것이었다.
함수에 배열을 넣는다 가 아니라, 주소를 넘긴다 라고 말하는 것이 정확하다.
executeCallback의 주소와 functions 각각의 주소는 다 다르다.
함께 같은 공간에 있다고 생각하면 틀린 것이다.

function callback() {} 도 executeCallback에 담겨있는게 아닌 것이다.

functions의 함수들은 일꾼들이다. executeCallback에 담겨서 매개변수로 callback을 받은 게 아니고
원래 callback을 매개변수로 가지고 있다고 생각하면 된다. 물론 그렇게 주는건 바로 너랑 나이다.
주는 건 우리지만 실행은 functions의 함수들이 하는 것이다.

야 너네 callback이라고 전화번호 담긴 핸드폰 하나씩 줄 테니까 너네 호출당해서 할일 끝내면 callback으로 전화해~

말잘듣는 일꾼들은 이름이 불리면 곧 전화를 하게 되는데, 얘네는 자기들이 가지고 있는
핸드폰의 기종이 뭐고 공시할인인지 선택약정인지 등등 정보에 대한 관심은 전혀 없다.
일을 가장 빨리 끝낸 functions[2], functions[1], functions[0] 순서대로 전화를 할 것이다.

var functions = [
  function foo(callback) {
    setTimeout(() => {
      console.log('일');
      callback();
    }, 150)
  },
  function bar(callback) {
    setTimeout(() => {
      console.log('이');
      callback();
    }, 50)
  },
  function bar(callback) {
    setTimeout(() => {
      console.log('삼');
      callback();
    }, 10)
  },
];

function executeCallback() {
  var count = 0;

  for(const func of functions) {
    func(function callback() {
      console.log(`콜백 호출 ${++count}번째`)
    });
  }   
}

executeCallback(functions);

그리고 이 아이들에게 일하라고 부르는 게 바로 executeCallback함수이다.
얘는 for...of로 함수들을 부르고 사실 종료되지만
하지만 그 안에는 callback이라는 함수는 죽지 않고 살아 있다. 자신에게 전화할 사람이 있으니
자신의 집에서 기다리고 있는 것이다.

일꾼이 callback() 으로 전화번호를 눌러서 전화를 하면
function callback() {} 함수는 다음과 같이 동작할 것이다.


아 functions[2] 너가 먼저 일 끝냇구나.
그럼 나도 카운트 해야지 아 카운트 근데 뭐더라? ( 여기서 클로저 등판)
아 처음에 나 생길 때 내 렉시컬 스코프에서 본 것같은데 아 저기있네 지금 0이네 내가 1 더해줘야겠다~

"삼"
"콜백 호출 1번째"

"이"
"콜백 호출 2번째"

"일"
"콜백 호출 3번째"

그래서 위와 같은 순서로 찍히는 것이다.

오케이 setTimeout 비동기라서 저렇게실행되는 건 알았다.

비동기함수 직렬 실행하기

그럼 처음 하고싶었던 대로 functions [0] >> functions[1] >> functions[2] 이 순서대로 실행하고 싶어서 많은 방법을 사용해보았다.

위 코드에서 잘 바꾸면 되지 않을까? 해서
검색하면 for문 IIFE로 실행하는 예제가 많이 나오는데 이 문제에 적용할 수 없는 예제이다.

아니면 promise를 쓰면 된다고 하지만 나는 promise의 p도 모르기 때문에
다른 방법을 간구해야 했다. 배열을 순회하는 방법도 다 써보고 reduce는 안되나 했는데
모두 다 아니다. 왜냐 이것들은 동기적으로 실행하는 애들이기 때문에 비동기함수를 컨트롤 할 수가 없다.
어찌됐던간에 동기로 먼저 실행하고 마지막에 functions들이 일하게 되는 구조라 저런 방법들을
사용할 수가 없다.

그럼 메서드가 아니라면 순서를 어떻게 지정하면 좋을까.
아 그럼 내가 직접 순서를 세면 되지 않을까?
그리고 functions [0] >> functions[1] >’> functions[2] 이렇게 실행된다는 것은
functions[0]만 실행시키면 될 것 같다. 그럼 다음과 같이 코드를 작성할 수 있겠다.

function executeCallback() {
  let count = 0;

  function callback() {
    if (count === functions.length - 1) {
      console.log(`콜백 호출 끝!`);
      return;
    }
    count++
    console.log(`콜백 호출 ${count}번째`);
    functions[count](callback);
  }
  
 functions[0](callback);
}

executeCallback(functions);

fucntions[0]부터 먼저 실행시키고, setTimeout 시간이 끝나서
callback() 호출을 하면 바로 다음 functions[1]을 부를 수 있게
function callback(){} 에 조건을 넣어주면 된다.

"일"
"콜백 호출 1번째"

"이"
"콜백 호출 2번째"

"삼"
"콜백 호출 끝!"

이렇게 하나씩 순서대로 실행되는 것을 waterfall, 직렬구조로 실행된다고 한다.
실무에서도 많이 사용하는 말인 것 같다. 아마도..?

profile
매일 성장하는 프론트엔드 개발자

0개의 댓글