비동기코드 문제점 그리고 Promise

jsonLee·2023년 7월 2일
0
post-thumbnail
  1. 비동기코드의 문제점
  2. 비동기 코드문제점 해결방법인 promise
  3. promise보다 더 간단한 표현 async/await
    결론: async/await 잘 쓸려면 promise 알고써야한다.

1. 비동기 코드의 문제점

JavaScript 비동기 코드의 문제점과 해결 방법: Promise와 async/await
비동기 코드는 JavaScript 개발에서 빠질 수 없는 중요한 요소입니다. 하지만 비동기 코드를 다루다 보면 여러 가지 문제가 발생할 수 있습니다. 이번 글에서는 비동기 코드의 문제점과 해결 방법에 대해 알아보겠습니다. 우선 비동기 코드의 주요 문제점은 다음과 같습니다.

1.1 콜백 지옥

비동기 작업을 연속적으로 처리해야 할 때, 콜백 함수를 중첩하여 사용하면 코드의 가독성이 떨어지고 유지보수가 어려워집니다. 이를 "콜백 지옥"이라고 부릅니다.

일단 콜백함수와 비동기 함수의 관계에 대해 잠깐 살펴보겠습니다.

let result = '비동기 전 데이터~~~~~~'

//-------------------잘못된 예) 

function 예상밖의_비동기코드() {
  setTimeout(() => {
    return '비동기 끝!!!!!!!!!!!!!!'
  }, 1000);
}

result = 예상밖의_비동기코드(); // 비동기 전 데이터~~~~~가 비동기 끝!!!!으로 바뀌여야 하는데?
console.log(result) //undefined...


//--------------------옳바른 예) 

const 콜백함수 = (string) => {
  console.log(string)
}

function 콜백함수_비동기코드(콜백함수) {
  setTimeout(() => {
    콜백함수('비동기 끝!!!!!!!!!!!!!!');
  }, 1000);
}


콜백함수_비동기코드(콜백함수); // 비동기 끝!!!!!!!!!!!!!!

비동기함수와 콜백함수와의 관계를 잠깐 살펴봤는데요. 비동기함수는 비동기 작업이 끝난후의 데이터를 조작하는 콜백함수로 인자로 반드시 넘겨줘야 합니다.
console.log(result) 가 undefined인 이유는 javascript의 특징인 싱글스레드와 이벤트 루프(Event Loop)라는 메커니즘에 영향을 받기 때문입니다. 이 특징은 여기서는 다루지 않겠습니다.

여기서 이런생각을 할수 있습니다. 콜백함수가 비동기 코드라면 어떨까요?
예를 들어 서버로 데이터를 불러오고(비동기코드) 그 결과를 가지고 다른자원을 서버로 요청하는 경우가 있을 수 있겠죠?

let data = '아직없음'


function 첫번째API(url) {
  data = '첫번째 데이터 가져오기 성공';
  (function 두번째API(data) {
    data = '두번쨰 데이터 가져오기 성공';
    (function 세번째API(data) {
      data = '세번째 데이터 가져오기 성공';
      마지막_출력_콜백함수(data)
    })();
  })();
}

function 마지막_출력_콜백함수(result) {
  console.log(result)
}

첫번째API('https:www.어쩌구저쩌구.com') //'세번째 데이터 가져오기 성공';

위는 콜백 헬이 발생하는 상황을 묘사한 것입니다. 비동기 처리를 위해 콜백 패턴을 사용하면 처리 순서를 보장하기 위해 여러 개의 콜백 함수가 네스팅(nesting, 중첩)되어 복잡도가 높아지는 콜백 헬(Callback Hell)이 발생하는 단점이 있습니다. 콜백 헬은 가독성을 나쁘게 하며 실수를 유발하는 원인이 됩니다.

1.2 에러 처리의 어려움

비동기 작업에서 에러가 발생하면 해당 에러를 적절히 처리해야 합니다. 하지만 콜백 함수를 사용하여 비동기 작업을 처리할 때 에러 처리가 복잡해지는 문제가 있습니다. 에러 처리의 어려움과 함께 발생할 수 있는 문제점에 대해 알아보겠습니다.

try {
  setTimeout(() => { throw new Error('Error!'); }, 1000);
} catch (e) {
  console.log('에러를 캐치하지 못한다..');
  console.log(e);
}

try 블록 내에서 setTimeout 함수가 실행되면 1초 후에 콜백 함수가 실행되고 이 콜백 함수는 예외를 발생시킵니다. 하지만 이 예외는 catch 블록에서 캐치되지 않습니다. 그 이유에 대해 알아볼까요? 코드의 흐름은 다음과 같습니다.

  1. setTimeout 함수가 호출되고, 일정 시간이 경과하면 타이머 이벤트가 발생합니다.
  2. 타이머 이벤트가 발생하면, setTimeout의 콜백 함수가 태스크 큐로 이동합니다.
  3. 호출 스택이 비어있을 때(즉, 현재 실행 중인 코드가 없을 때), 태스크 큐에서 가장 앞에 있는 작업이 호출 스택으로 이동하여 실행됩니다.
  4. setTimeout의 콜백 함수가 호출 스택으로 이동하여 실행됩니다.
  5. 콜백 함수 내에서 예외가 발생하면, 이 예외는 해당 콜백 함수의 실행 컨텍스트에서 발생합니다.
  6. try-catch 구문은 해당 콜백 함수의 실행 컨텍스트 내에 없기 때문에, 예외를 캐치할 수 없습니다.
  7. 예외는 호출 스택을 거슬러 올라가며, 호출 스택에 setTimeout 함수가 아직 남아있지 않기 때문에 try-catch 구문에서 캐치되지 않고, 예외는 처리되지 않은 예외로 남게 됩니다.
    요약하자면, setTimeout 함수의 콜백 함수에서 발생하는 예외는 해당 콜백 함수가 이미 실행된 후에 발생하게 됩니다. 따라서 try-catch 구문으로 예외를 캐치하려면, setTimeout의 콜백 함수 내에서 예외 처리를 직접 해주어야 합니다.
try {
  setTimeout(() => { 
    try {
      throw new Error('Error!');
    } catch (e) {
      console.log('에러를 캐치했습니다.');
      console.log(e);
    }
  }, 1000);
} catch (e) {
  console.log('에러를 캐치하지 못합니다.');
  console.log(e);
}

중복도 많고 가독성에 문제가 많죠? 이러한 문제를 극복하기 위해 Promise가 제안되었다고 합니다. Promise는 ES6에 정식 채택되어 IE를 제외한 대부분의 브라우저가 지원하고 있습니다.

2. Promise의 구조

Promise는 비동기 작업을 처리하기 위한 JavaScript의 내장 객체입니다. Promise를 사용하면 비동기 작업을 보다 구조화된 방식으로 처리할 수 있습니다. Promise의 구조와 사용법에 대해 자세히 알아보고, 앞서 언급한 문제점을 Promise를 활용하여 어떻게 해결할 수 있는지 살펴보겠습니다.

let result = '비동기 전 데이터~~~~~~';

function 예상밖의_비동기코드() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('비동기 끝!!!!!!!!!!!!!!');
    }, 1000);
  });
}

예상밖의_비동기코드()
  .then((data) => {
    result = data;
    console.log(result); // 비동기 끝!!!!!!!!!!!!!!
  })
  .catch((error) => {
    console.error(error);
  });

다음은 프로미스 사용의 기본 골격구조 입니다.

// Promise 객체의 생성
const promise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행한다.

  if (/* 비동기 작업 수행 성공 */) {
    resolve('result');
  }
  else { /* 비동기 작업 수행 실패 */
    reject('failure reason');
  }
});


// const result = promise(); 이렇게동작하지 않습니다.프로미스는 콜백함수를 then이라는 문법구문 안에서 작동합니다..

promise.then((data) => {
  console.log(data) //성공하면 result가 출력됨
}).catch((error) => {
  console.error(error) //에러가 있으면 'failure reason'가 출력됨
}) // 옳바른 사용의 예

프로미스는 Promise 생성자 함수를 통해 인스턴스화 합니다. Promise 생성자 함수는 비동기 작업을 수행할 콜백 함수를 인자로 전달받는데 이 콜백 함수는 resolve와 reject 함수를 인자로 전달받습니다.

어떤가요 좀 괜찮아졌나요? 음...잘 모르겠다구요? 그래도 확실한 것은 에러를 처리하는 부분은 너무 쉽게 표현되어서 좋아보이긴 합니다. 다음은 비동기함수에 콜백함수를 인자로 넘기면서 생기는 콜백 지옥의 문제를 해결하는 async/ await에 대해 알아봅시다.

3. async/await

Promise를 사용하여 비동기 코드를 다룰 수 있지만, 여전히 콜백 함수의 중첩이나 체이닝의 형태를 가질 수 있습니다. 이를 보다 간결하고 동기적인 코드로 작성할 수 있는 방법이 있습니다. 그 방법은 바로 async/await입니다. async/await를 사용하면 비동기 작업을 동기적인 코드처럼 작성할 수 있습니다. async/await의 동작 원리와 사용법에 대해 알아보고, Promise와 async/await를 함께 사용하는 방법을 예시 코드와 함께 살펴보겠습니다.

let result = '비동기 전 데이터~~~~~~';

function 예상밖의_비동기코드() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('비동기 끝!!!!!!!!!!!!!!');
    }, 1000);
  });
}

async function 비동기_작업_실행() {
  try {
    const data = await 예상밖의_비동기코드();
    result = data;
    console.log(result); // 비동기 끝!!!!!!!!!!!!!!
  } catch (error) {
    console.error(error);
  }
}

비동기_작업_실행();

위 예제에서는 비동기작업실행 함수를 async 함수로 선언합니다. await 키워드를 사용하여 비동기 작업의 완료를 기다릴 수 있습니다.

await 예상밖의_비동기코드()는 Promise 객체를 반환하는 함수이므로, await를 사용하여 비동기 작업이 완료될 때까지 기다립니다. 작업이 완료되면 결과 데이터를 data 변수에 할당하고, result 변수에 저장합니다.

비동기 작업이 성공적으로 완료되면 console.log(result)를 사용하여 결과를 출력합니다. 만약 비동기 작업이 실패한 경우에는 catch 블록에서 에러 처리를 합니다.

이렇게 async/await를 사용하면 프로미스를 더 직관적이고 동기적인 코드 스타일로 작성할 수 있습니다.

결론: async/await 잘 쓸려면 promise를 잘 알아야한다

async 키워드를 사용하여 선언해야 하는 함수는 비동기 작업을 수행하는 함수입니다. async 함수는 암묵적으로 Promise를 반환합니다. 내부에서 await 키워드를 사용하여 다른 Promise를 기다리고, 그 결과를 처리할 수 있습니다.

async 함수의 선언은 다음과 같은 형태를 가집니다:

async function 함수이름() {
  // 비동기 작업 수행
}
async function getData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
}

위의 코드에서 getData 함수는 비동기 작업을 수행하고, fetch를 사용하여 데이터를 가져옵니다. await 키워드를 사용하여 fetch 작업이 완료될 때까지 기다리고, 그 후에 response.json()을 호출하여 데이터를 가져옵니다. 그리고 마지막으로 데이터를 반환합니다.

이와같이 async 함수는 비동기 작업을 더 간편하고 동기적으로 다룰 수 있는 방법을 제공합니다. async 함수를 호출할 때에는 await 키워드를 사용하여 해당 함수의 완료를 기다리거나, then 메서드를 사용하여 반환된 Promise를 처리할 수 있습니다.

지금까지 보았듯이 async/await는 promise와 긴밀히 연관되어 있습니다. 또한 복잡하고 중첩된 비동기 함수 작성시 promise에 대한 이해가 없으면 어려움을 겪을 수 있다고 들었는데 정말 그런거 같네요.

모두들 비동기 함수랑 친해지고 즐거운 코딩 하시길 바라겠습니다.

그럼 즐코~

profile
풀스택 개발자

0개의 댓글