JavaScript

[JS] 비동기 처리 문제점과 해결 방법

_doit 2024. 7. 22. 19:17
728x90
반응형

자바스크립트 비동기 처리 원리

자바스크립트는 '싱글 스레드' 언어이다, 따라서 한 번에 하나의 작업만 처리할 수 있다

그런데 스레드가 하나인데, 자바스크립트는 어떻게 여러 작업(task)들을 동시에 처리할 수 있을까? 

 

자바스크립트를 실행하는 콜 스택(Call Stack)은 싱글 스레드지만, 자바스크립트가 구동되는 환경(브라우저, Node.js 등)에서는 여러 개의 스레드가 사용될 수 있다.

서버에게 리소스를 요청하거나 파일 입출력 혹은 타이머 대기 작업을 실행하는 Web APIs 들은 멀티 스레드이기 때문에 동시 작업 처리가 가능하다.

 

이러한 작업들은 비동기적으로 실행되며, 완료되면 콜백 함수를 콜백 큐(callback queue)에 넣는다.

단일 콜 스택을 사용하는 자바스크립트 엔진과 연동하기 위해 사용하는 장치가 이벤트 루프(event loop)이다.

이벤트 루프는 콜 스택이 비어 있는 상태를 감시하고, 콜백 큐에서 대기 중인 콜백 함수를 콜 스택으로 옮겨 실행한다.

이로 인해 자바스크립트는 싱글 스레드로 동작하면서도 비동기 작업을 처리할 수 있다.

이와 같은 원리 덕분에 자바스크립트는 효율적으로 비동기 작업을 관리하고, 여러 작업을 동시에 처리하는 것처럼 보이게 된다.

 

 

비동기 처리 과정이 더 궁금하다면 아래 유튜브 참고

https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=255s

 

비동기 처리의 문제점

비동기처리는 요청한 작업의 완료 여부를 기다리지 않고 자신의 그다음 작업을 계속 수행해 나간다.

그런데 만일 그다음 실행할 작업이 이전에 요청한 작업의 결과가 반드시 필요할 경우 문제가 생긴다.

function fetchData() {
    setTimeout(() => {
      return "Hello, world!";
    }, 1000);
  }
 
  const data = fetchData();
  console.log(data); // undefined가 출력됨

 

위 코드에서 fetchData 함수는 비동기로 데이터를 가져오지만, console.log(data)가 실행될 때 data는 아직 정의되지 않았기 때문에 undefined가 출력된다.

이럴 떄 문제를 해결하는 몇가지 방법이 있다. 가장 대표적인 것이 콜백 함수 기법이다.

 

비동기를 알맞게 처리하기 위한 방법

비동기와 콜백 함수

비동기 방식은 요청과 응답의 순서를 보장하지 않는다. 따라서 응답의 처리 결과에 의존하는 경우에는 콜백 함수를 이용하여 작업 순서를 간접적으로 끼워 맞출 수 있다. 콜백은 자바스크립트에서가장 기본적인 비동기 패턴이다

콜백 함수 방식으로 위의 문제 코드를 수정하면 다음과 같게 된다.

// fetchData 함수는 callback이라는 매개변수를 받음
 
function fetchData(callback) {
  setTimeout(() => {
    const data = "Hello, world!";
    callback(data);    // 콜백 함수 호출
  }, 1000);
}
 
function handleData(data) {
  console.log(data);     // "Hello, world!"가 출력됨
}

fetchData(handleData)    // fetchData 함수를 호출하면서 handleData 함수를 callback 매개변수로 전달

위 코드는 콜백 함수 내에서 data 변수의 값을 받아 출력하므로, 비동기 작업이 완료된 후에 출력되게 된다. 즉, 콜백 함수는 비동기 함수에서 작업 결과를 전달받아 처리하는데 사용되어 작업 순서를 맞출수 있게 되는 것이다. 따라서 비동기 함수와 콜백 함수는 서로 밀접한 관계를 가지고 있다고 말하는 것이다

 

다만 너무 복잡하게 얽힌 비동기 처리 때문에 콜백 함수 방식은 코드 복잡도를 증가시켜, 개발자가 어플리케이션의 흐름을 읽기 어려워지는 등의 문제가 있을 수 있어 잘못하면 콜백 지옥(callback hell) 에 빠질수 있다는 단점이 있다

fetchData((data1) => {
  fetchData((data2) => {
    fetchData((data3) => {
      console.log(data1, data2, data3);
    });
  });
});

 

비동기와 프로미스 객체

콜백 함수는 엄연히 말하자면 비동기를 순차적으로 처리하기 위한 일종의 '편법' 같은 것이지 정식으로 지원하는 비동기 전용 함수가 아니다. 따라서 자바스크립트의 Promise 객체는 이러한 한계점을 극복하기 위해 비동기 처리를 위한 전용 객체로서 탄생하였다. Promise는 비동기 작업의 성공 또는 실패와 그 결과값을 나타내는 객체이다. 그래서 Promise를 사용하면 비동기 작업을 쉽고 깔끔하게 연결할 수 있게 된다.

function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      const data = "Hello, world!";
      resolve(data);
    }, 1000);
  });
}

function handleData(data) {
  console.log(data); // "Hello, world!"가 출력됨
}

fetchData()
  .then(handleData)
  .catch((error) => {
    console.error(error);
});

 

비동기와 async / await

하지만 프로미스도 완벽한 해결책은 아니다. 왜냐하면 Callback Hell이 있듯이 지나친 then 핸들러 함수의 남용으로 인한 Promise Hell이 존재하기 때문이다. 즉, 프로미스가 여러 개 연결되면 코드가 길어지고 복잡해질 수 있다는 것이다. 그래서 자바스크립트에는 async/await라는 문법이 또한 추가되었다. async/await는 프로미스를 기반으로 하지만, 마치 동기 코드처럼 작성할 수 있게 해준다. 비동기 작업을 쉽게 읽고 이해할 수 있게 해주기 때문에 비동기 작업을 처리할 일이 있다면 대게 async/await 방식을 쓰는 것이 보통이다.

function fetchData() {
    return new Promise((resolve) => {
      setTimeout(() => {
        const data = "Hello, world!";
        resolve(data);
      }, 1000);
    });
  }
 
  async function fetchAsyncData() {
    try {
      const data = await fetchData();
      console.log(data); // "Hello, world!"가 출력됨
    } catch (error) {
      console.error(error);
    }
  }
 
  fetchAsyncData();

자바스크립트에서 Promise나 async/await와 같은 문법을 사용하는 이유도 이런 비동기 처리의 흐름을 좀 더 명확하게 인지하고자 하는 노력인 것이다.

 

 

비동기 예외 처리 

아래 getUser 함수를 실행하니, HTTP 요청이 실패했는데도 success를 표시하는 알림창만 나타나고 error를 표시하는 알림창이 나타나지 않았는다고 했을 때, 

function getInfo() {
    try {
      // HTTP 요청을 통해 정보를 가져온다
      http.get('/info');
      alert('success');
    } catch {
      alert('error');
    }
}

이제는 보고 아 이게 HTTP 요청이 완료되기 전에 다음 줄 alert('success'); 가 실행돼서 그렇구나 알 수 있다.

 

function getInfo() {
    try {
      // HTTP 요청을 통해 정보를 가져온다
      http.get('/info', () => {
        alert('success');
      });
    } catch (error) {
      alert('error');
    }
  }
 
  getInfo();

그러면 이제 콜백 사용해서 요청이 올 때 까지 기다리는데,  HTTP 요청이 실패한 경우 아무런 알림창이 나타나지 않는 이유는 뭘까?

http.get 함수가 비동기적으로 실행되고, 비동기 요청이 완료되기 전에 try...catch 블록이 종료되기 때문에 아무 알림창도 뜨지 않는다.

결과적으로 try...catch 블록은 비동기 콜백 함수 내부에서 발생한 예외를 잡지 못하므로, 요청이 실패한 경우에도 catch 블록이 실행되지 않는다.

 

따라서 콜백 함수 내부에서 에러 처리를 하든지 

async/await와 try...catch를 사용하거나, Promise와 then...catch를 사용하여 비동기 예외처리를 해주면 된다

  function getInfo() {
    http.get('/info', (error, response) => {
      if (error) {
        alert('error');
      } else {
        alert('success');
      }
    });
  }
  function getInfo() {
    http.get('/info')
      .then(response => {
        alert('success');
      })
      .catch(error => {
        alert('error');
      });
  }
 
  getInfo();
  async function getInfo() {
    try {
      // HTTP 요청을 통해 정보를 가져온다
      const response = await http.get('/info');
      alert('success');
    } catch (error) {
      alert('error');
    }
  }
  getInfo();
728x90
반응형