콜백 함수를 이용한 비동기 처리의 단점

박민우·2023년 9월 30일
1

JavaScript

목록 보기
8/14
post-thumbnail

JS는 비동기 처리를 위한 하나의 패턴으로 콜백함수를 사용한다. 하지만 이러한 전통적인 콜백 패턴은

  1. 콜백 지옥으로 인한 나쁜 가독성

  2. 비동기 처리 중 발생한 에러 처리의 곤란

  3. 여러 개의 비동기 처리의 한계

등의 단점을 가지고 있다. 이에 대해서 자세히 알아보자.

여기서 말하는 비동기 처리

=> 비동기로 동작하는 작업의 결과를 이용해 이를 다음 작업에 활용하는 것. 이를 위해서는 비동기 작업이 끝나고 그 다음 작업이 이뤄져야하므로 즉 비동기 작업을 동기적으로(순서대로) 처리하는 것을 말한다.


📌 1. 콜백 지옥

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


setTimeout 예시

let g = 0;

setTimeout(()=>{ g=100;}, 0);
console.log(g); // 0

setTimeout 함수를 예를 들어 보자. setTimeout 함수가 비동기 함수인 이유는 콜백 함수의 호출이 비동기로 동작하기 때문이다. setTimeout 함수를 호출하면 콜백 함수를 호출 스케줄링한 다음, 타이머 id를 반환하고 종료된다.

즉, 콜백함수는 비동기 함수인 setTimeout 함수가 종료된 이후 호출 스택 내 다른 함수들도 모두 종료된 뒤 호출 스택이 비어있을 때 호출된다. 그렇다면 호출 스택이 비어있다면, 콜백 함수에서 접근한 변수도 생명주기를 다해 소멸되었을 수도 있을 것이다.

따라서, setTimeout 함수 내부의 콜백 함수에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않는다.


비동기 처리를 위한 Callback 함수의 전달

이처럼 비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고, 상위 스코프의 변수에 할당할 수도 없다.

=> 따라서 비동기 함수의 처리 결과(서버 응답 등)에 대한 후속 처리는 비동기 함수 내부에서 수행해야 한다.

=> 이 때 비동기 함수를 범용적으로 사용하기 위해 비동기 함수에 비동기 처리 결과에 대한 후속 처리를 수행하는 콜백 함수를 전달하는 것이 일반적이다.

EX) GET 요청 코드

const get = (url, successCallback, failureCallback) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.send();
    
    xhr.onload = () => {
        if(xhr.status === 200){
            successCallback(JSON.parse(xhr.response));
        }
        else{
            failureCallback(xhr.status);
        }
    };
};

get('/posts/1', console.log, console.error);

즉, " get 함수 너는 비동기 작업을 하는 함수라서 너가 처리한 비동기 작업의 결과를 외부로 전달할 수 없으니, 그 결과를 가지고 너가 할 일을 내가 함수로 전달해줄테니 비동기 작업한 결과를 그 함수의 인자로 넣어서 호출해!" 라고 말하는 상황인 것이다.

하지만 만약 비동기 함수가 비동기 처리 결과를 가지고 또 다시 비동기 함수를 호출해야 한다면 콜백 함수 호출이 중첩되어 복잡도가 높아지는 콜백 헬이 발생할 수 있다.

get('/step1', a => {
    get('/step2/${a}', b => {
        get('/step3/${b}', c=> {
            get('/step4/${c}', d=>{
                console.log(d);
            })
        })
    })
})

📌 2. 에러 처리의 한계

try {
    setTimeout(()=>{ throw new Error('Error!'); }, 1000);
} catch (e) {
    console.error('캐치한 에러', e);
}

위의 코드에서 try 코드 블록 내에서 setTimeout 함수의 콜백함수에서 전달된 에러는 아래 catch 코드 블록에서 캐치되지 않는다.

그 이유는?

  • 에러는 호출자(caller) 방향으로 전파된다. 즉, 호출 스택의 아래 방향(실행 중인 실행 컨텍스트가 푸시되기 직전에 스택의 맨 위에 있었던 실행 컨텍스트 방향)으로 전파된다.
  • 하지만 콜백함수가 실행될 때, setTimeout 함수는 이미 콜 스택에서 제거된 상태이다. 이는 콜백함수를 호출한 호출자가 setTimeout 함수가 아니라는 것을 의미한다.
  • 따라서 setTimeout 함수의 콜백함수가 발생시킨 에러는 catch 블록에서 캐치되지 않는다.

📌 콜백을 보완하기 위한 프로미스의 사용

콜백 함수를 통한 비동기 처리는 위와 같은 단점을 가지고 있고, 이를 해결하기 위해 또 다른 비동기 처리 패턴인 프로미스를 사용할 수 있다. 다음 글에서 프로미스에 대해서 더 알아보자.


🙇🏻‍♂️ 참고

모던 자바스크립트 Deep Dive

profile
꾸준히, 깊게

0개의 댓글