Frontend/JavaScript

콜백함수와 비동기처리

findmypiece 2021. 3. 4. 00:45
728x90

콜백함수는 비동기 작업에서 많이 사용되는데

결과에 따라 추가 작업을 해야 하는데 시간이 오래 걸리는 A라는 함수를 호출해야 할 경우

작업이 끝나면 호출할 B함수를 인자에 할당해서 비동기 호출을 해놓고

결과를 기다리지 않고 다른 일을 한다.

그럼 A함수는 작업이 끝나면 인자로 받은 B함수를 실행해서 작업이 끝났음을 알린다.

여기에서 B함수가 바로 콜백함수이다.

call back 은 "전화를 해왔던 사람에게 다시 전화하다" 라는 뜻인데

위 상황만 보면 콜백함수의 "콜백"도 이대로 해석해도 무리는 없다.

하지만 실제로 콜백함수는 위와 같이 단순히 작업이 끝났음을 알리기 보다

작업결과를 이용해서 추가작업을 수행하기 때문에 콜에프터(call after) 함수라고도 불린다.

어쨌든 너무 복잡하게 생각하지 말고 간단하게 생각하려면

함수를 호출하기 전에 생성해서 인자로 전달하는 함수는

그냥 콜백함수라고 봐도 무방하다.

 

자바스크립트에서 가장 기본적인 비동기 작업처리 방법은 setTimeout 함수를 사용하는 것인데

첫번째 인자의 함수를 두번째 인자의 ms 초 뒤에 백그라운드에서 실행되도록 할 수 있다.

참고로 두번째 인자를 0으로 줘도 자바스크립트 특성상 실제로는 4ms초 뒤에 실행된다고 한다.

아무튼 비동기 작업의 경우 종료시점을 알 수 없기 때문에 종료시점에 추가작업이 필요할 경우

아래와 같이 반드시 콜백함수를 활용해야 한다.

다르게 말하면 비동기 작업이 아니라면 굳이 콜백함수를 사용할 이유는 없다.

 

const job = (callback) => {
  setTimeout(() => {
    let i;
    for (i = 0; i < 1000000000; i++) {}
    callback(i);
  }, 0);
};

const addJob_1 = (startValue, callback) => {
  console.log(startValue + '까지 완료');
  setTimeout(() => {
    let i;
    for (i = startValue; i < startValue+1000000000; i++) {}
    callback(i);
  }, 0);
};

console.log('작업 시작!');

job((startValue) => {
  addJob_1(startValue, (startValue) => {
    console.log(startValue + '까지 완료 / 모든 작업 끝!');
  });
});

console.log('작업중...');

/****콘솔출력 결과***
"작업 시작!"
"작업중..."
"1000000000까지 완료"
"2000000000까지 완료 / 모든 작업 끝!"
*/

 

그런데 이 방식의 문제는 비동기 작업 이후 수행되어야 할 함수가 많을 경우

반복적으로 콜백함수를 지정해줘야 하기 때문에 코드의 열 너비가 계속해서 늘어나서

가독성이 떨어진다는 것이데 흔히 콜백지옥이라고 불린다.

아래는 비동기 호출의 후처리로 5개의 함수를 순차적으로 실행하고

각 함수의 결과를 다음 함수에서 넘겨받아서 이어서 처리하도록 한 모습이다.

addJob_1~addJob_5 함수는 동일한 내용의 중복인데

그냥 테스트를 위해서 이렇게 구현한 것이고 실제로는 이전 결과를 받아서

서로 다른 로직을 수행한다고 생각하자.

const job = (callback) => {
  setTimeout(() => {
    let i;
    for (i = 0; i < 1000000000; i++) {}
    callback(i);
  }, 0);
};

const addJob_1 = (startValue, callback) => {
  console.log(startValue + '까지 완료');
  setTimeout(() => {
    let i;
    for (i = startValue; i < startValue+1000000000; i++) {}
    callback(i);
  }, 0);
};

const addJob_2 = (startValue, callback) => {
  console.log(startValue + '까지 완료');
  setTimeout(() => {
    let i;
    for (i = startValue; i < startValue+1000000000; i++) {}
    callback(i);
  }, 0);
};

const addJob_3 = (startValue, callback) => {
  console.log(startValue + '까지 완료');
  setTimeout(() => {
    let i;
    for (i = startValue; i < startValue+1000000000; i++) {}
    callback(i);
  }, 0);
};

const addJob_4 = (startValue, callback) => {
  console.log(startValue + '까지 완료');
  setTimeout(() => {
    let i;
    for (i = startValue; i < startValue+1000000000; i++) {}
    callback(i);
  }, 0);
};

const addJob_5 = (startValue, callback) => {
  console.log(startValue + '까지 완료');
  setTimeout(() => {
    let i;
    for (i = startValue; i < startValue+1000000000; i++) {}
    callback(i);
  }, 0);
};

console.log('작업 시작!');

job((startValue) => {
  addJob_1(startValue, (startValue) => {
    addJob_2(startValue, (startValue) => {
      addJob_3(startValue, (startValue) => {
        addJob_4(startValue, (startValue) => {
          addJob_5(startValue, (startValue) => {
            console.log(startValue + '까지 완료 / 모든 작업 끝!');
          });
        });
      });
    });
  });
});

console.log('작업중...');

/****콘솔출력 결과***
"작업 시작!"
"작업중..."
"1000000000까지 완료"
"2000000000까지 완료"
"3000000000까지 완료"
"4000000000까지 완료"
"5000000000까지 완료"
"6000000000까지 완료 / 모든 작업 끝!"
*/

이러한 콜백지옥의 가독성을 개선하기 위해 ES6에서 Promise 라는 기능이 도입되었다.

콜백함수를 사용할 경우 결국 인자에 함수를 할당해야 하기 때문에

이어지는 콜백함수가 많을 경우 열의 너비가 늘어나는 것을 피할 수 없는데

Promise 에서는 콜백함수를 정의하지 않고 콜백함수 기능을 구현한다고 생각하면 된다.

사용방법은 아래와 같이 비동기 처리로직을 return new Promise((resolve, reject) => { } 로 감싸고

정상적인 처리 결과는 resolve() 로 감싸서 리턴하고 에러는 reject() 로 감싸서 리턴하면 된다.

const job = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try{
        let i;
        for (i = 0; i < 1000000000; i++) {}
        return resolve(i);
      }catch(e){
        return reject(e);
      }
    }, 0);
  });
};

후처리는 아래와 같이 지정하면 된다.

job()
  .then(result => {
    console.log(result + '까지 완료');
  })
  .catch(e => {
  	console.log(e);
  });

이를 이용해서 비동기 처리를 다시 구현하면 아래와 같다.

const job = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try{
        let i;
        for (i = 0; i < 1000000000; i++) {}
        return resolve(i);
      }catch(e){
        return reject(e);
      }
    }, 0);
  });
};

const addJob_1 = (startValue) => {
  return new Promise((resolve, reject) => {
    console.log(startValue + '까지 완료');
    setTimeout(() => {
      try{
        let i;
        for (i = startValue; i < startValue+1000000000; i++) {}
        return resolve(i);
      }catch(e){
        return reject(e);
      }
    }, 0);
  });
};

const addJob_2 = (startValue) => {
  return new Promise((resolve, reject) => {
    console.log(startValue + '까지 완료');
    setTimeout(() => {
      try{
        let i;
        for (i = startValue; i < startValue+1000000000; i++) {}
        return resolve(i);
      }catch(e){
        return reject(e);
      }
    }, 0);
  });
};

const addJob_3 = (startValue) => {
  return new Promise((resolve, reject) => {
    console.log(startValue + '까지 완료');
    setTimeout(() => {
      try{
        let i;
        for (i = startValue; i < startValue+1000000000; i++) {}
        return resolve(i);
      }catch(e){
        return reject(e);
      }
    }, 0);
  });
};

const addJob_4 = (startValue) => {
  return new Promise((resolve, reject) => {
    console.log(startValue + '까지 완료');
    setTimeout(() => {
      try{
        let i;
        for (i = startValue; i < startValue+1000000000; i++) {}
        return resolve(i);
      }catch(e){
        return reject(e);
      }
    }, 0);
  });
};

const addJob_5 = (startValue) => {
  return new Promise((resolve, reject) => {
    console.log(startValue + '까지 완료');
    setTimeout(() => {
      try{
        let i;
        for (i = startValue; i < startValue+1000000000; i++) {}
        return resolve(i);
      }catch(e){
        return reject(e);
      }
    }, 0);
  });
};

console.log('작업 시작!');

job()
  .then(startValue => {
  	return addJob_1(startValue);
  })
  .then(startValue => {
  	return addJob_2(startValue);
  })
  .then(startValue => {
  	return addJob_3(startValue);
  })
  .then(startValue => {
  	return addJob_4(startValue);
  })
  .then(startValue => {
  	return addJob_5(startValue);
  })
  .then(startValue => {
  	console.log(startValue + '까지 완료 / 모든 작업 끝!');
  })
  .catch(e => {
  	console.log(e);
  });
  
  console.log('작업중...');

/****콘솔출력 결과***
"작업 시작!"
"작업중..."
"1000000000까지 완료"
"2000000000까지 완료"
"3000000000까지 완료"
"4000000000까지 완료"
"5000000000까지 완료"
"6000000000까지 완료 / 모든 작업 끝!"
*/

이제 후처리로 수행되어야 할 함수가 아무리 많아져도

열 너비가 늘어날 일은 없다.

 

ES8에서는 async/await 문법이 도입되어 Promise 를 더욱 쉽게 사용할 수 있다.

초기 비동기 처리 함수는 구현은 동일하고 이에 대한 후처리를 아래처럼 할 수 있다.

async 키워드를 포함해서 비동기 작업을 수행할 함수를 별도로 구현하고

내부에서 각 함수를 호출할 때 await 키워드를 추가해서 처리결과를 대기했다가 결과값을 얻어올 수 있다.

const runJob = async () => {
	console.log('작업 시작!');
	try{
		let startValue = await job();
		
		startValue = await addJob_1(startValue);
		startValue = await addJob_2(startValue);
		startValue = await addJob_3(startValue);
		startValue = await addJob_4(startValue);
		startValue = await addJob_5(startValue);
		console.log(startValue + '까지 완료 / 모든 작업 끝!');
	}catch(e){
		console.log(e);
	}
}

runJob();
  
console.log('작업중...');

/****콘솔출력 결과***
"작업 시작!"
"작업중..."
"1000000000까지 완료"
"2000000000까지 완료"
"3000000000까지 완료"
"4000000000까지 완료"
"5000000000까지 완료"
"6000000000까지 완료 / 모든 작업 끝!"
*/

 

참고로 현재 가장 많이 사용되는 자바스크립트 http 클라이언트인 axios 라이브러리가

http 요청을 Promise 기반으로 처리한다.

728x90

'Frontend > JavaScript' 카테고리의 다른 글

클로저  (0) 2021.03.09
script 태그 async, defer 옵션  (0) 2021.03.04
ES6 import, export  (0) 2021.03.03
ES6 화살표 함수  (0) 2021.03.03
ES6 전개연산자  (0) 2021.03.03