비동기 처리 - 1. 콜백 패턴

이윤표·2024년 7월 29일
0

JS 표지

동기 vs 비동기
동기는 현재 실행 중인 작업이 끝날 때까지 다음 작업은 대기하고 작업이 끝나면 다음 작업이 실행되는 것을 의미한다. 반대로 비동기는 현재 실행 중인 작업을 기다리지 않고 다음 작업을 수행하는 것을 의미한다.
동기는 작업 실행의 순서를 보장한다는 장점이 있지만 블로킹이 발생한다는 단점이 있다.
비동기는 작업의 블로킹이 발생하지 않는다는 장점이 있지만 작업 실행의 순서를 보장하지 못한다는 단점이 있다.

비동기 처리의 필요성

function plus() {
  let a = 1;
  setTimeout(()=>console.log(++a), 1000);
  return a;
}

const result = plus();
console.log('result :', result);
result: 1
// 1초 뒤
2

위 코드에서 plus 함수는 비동기 함수다. 비동기 함수란 함수 내부에 비동기로 동작하는 코드를 포함한 함수를 말한다.

비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 해도 기다리지 않고 즉시 종료된다. 즉, 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 왼료된다. 따라서 비동기 함수 내부의 비동기로 동작하는 코드에서 처리결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대대로 동작하지 않는다.

plus 함수가 비동기 함수인 이유는 plus 함수 내부의 setTimeout 함수가 비동기로 동작하기 때문이다. plus 함수를 호출하면 a 를 반환하고 즉시 종료된다. 즉, plus 함수 내부의 setTimeout 의 콜백 함수 () => console.log(++a)plus 함수가 종료된 이후에 실행된다.

plus 함수의 기대하는 기능(1초 뒤 1 더해진 값 출력) 을 수행하기 위해 상위 스코프에 변수를 할당하면 어떨까?

let a = 1;
function plus() {
  setTimeout(()=>console.log(++a), 1000);
	return a;
}

const result = plus();
console.log('result :', result); 1️⃣
console.log("a:", a); // 2️⃣ 
result : 1
a: 1
// 1초 뒤
2

여전히 plus 함수의 반환값으로 여전히 1 이 출력되는 문제가 발생한다. setTimeout 함수의 콜백함수는 언제나 2️⃣ 의 console.log 가 종료된 이후에 호출된다. 따라서 2️⃣ 의 시점에는 아직 전역변수 a 에 +1 한 결과가 할당되기 이전이다.

이런 현상이 발생하는 이유는 함수 실행 과정에 의해 설명된다.

  1. plus 함수가 호출되면 plus 실행 컨텍스트가 콜스택에 푸시된다.

  2. 이후 함수 코드 실행과정에서 setTimeout 함수의 실행 컨텍스트가 콜 스택에 push 되어 실행되고 즉시 pop 된다.

  3. 이후 브라우저는 타이머 설정 및 만료를 기다리고 타이머가 만료되면 setTimout 함수의 콜백 함수를 태스크 큐에 푸시한다.

  4. plus 함수가 종료하면 plus 실행 컨텍스트가 콜 스택에서 팝 되고, 곧바로 1️⃣과 2️⃣의 console.log 가 호출된다.

  5. 이후 1️⃣ 과 2️⃣ 가 종료되어 콜스택에서 제거되면 이벤트 루프에 의해 태스크 큐에 있던 setTimeout 의 콜백함수를 콜스택에 푸시되어 실행된다.

이처럼 비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고, 상위 스코프 변수에 할당할 수도 없다. 따라서 비동기 함수의 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 수행해야 한다. 이 때, 비동기 함수를 범용적으로 사용하기 위해 비동기 함수에 비동기 처리 결과에 대한 후속처리를 수행하는 콜백함수를 전달하는 것이 일반적이다.

비동기 처리 방법 1: 콜백 패턴

콜백 패턴 사용

function plus(callback) {
  let a = 1;
  setTimeout(() => callback(++a), 1000);
}

plus((result) => {
  console.log('Async result:', result);
});
// 1초 뒤
Async result: 2

콜백 패턴을 사용하여 비동기 함수의 문제를 해결하려면, 함수가 비동기 작업을 완료한 후 콜백 함수를 호출하도록 수정해야 합니다. 위 코드는 plus 함수가 콜백을 인수로 받아서 비동기 작업이 완료된 후 콜백을 호출하는 방식으로 수정했다.

콜백 패턴의 단점

콜백 지옥 발생

이처럼 비동기 함수가 콜백 함수를 통해 후속 처리를 수행하고, 그 결과로 또 다른 비동기 함수를 호출해야 할 때 콜백 호출이 중첩되어 복잡도가 높아지는 현상을 콜백 지옥이라고 한다.

function plus(num, callback) {
  setTimeout(() => callback(++num), 1000);
}

// 콜백 지옥 예시
plus(1, (result1) => {
  plus(result1, (result2) => {
    plus(result2, (result3) => {
      plus(result3, (result4) => {
          console.log('result 4', result4);
      });
    });
  });
});

// 4초 후
// result 4: 5 출력

plus 함수 호출은 이전 호출의 결과를 받아서 다시 plus 함수를 호출한다.

이와 같이 콜백 패턴을 이용하면 코드의 가독성과 유지보수성이 떨어진다. 이를 해결하기 위해 Promiseasync/await를 사용한다.

function plus(num) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(++num), 1000);
  });
}

// 프로미스 패턴을 사용한 비동기 처리
plus(1)
  .then((result1) => {
    return plus(result1);
  })
  .then((result2) => {
    return plus(result2);
  })
  .then((result3) => {
    return plus(result3);
  })
  .then((result4) => {
    console.log('result 4', result4);
  })
  .catch((error) => {
    console.error('Error:', error);
  });

에러 처리 어려움

profile
프론트엔드 개발자 지망생

0개의 댓글