JavaScript

비동기? Promise, async/await, fetch API, 골고루 알아보자

남희정 2023. 8. 31. 18:52

테스트 코드, API를 사용하게 되면서 코드로 접한 비동기는 공부할 때의 비동기와는 또 달랐다. 익숙해지기 위해선 계속 공부하고 접해야 된다. 꼼꼼히 공부해보자.


 

비동기 Asynchronous

 

여기서 자바스크립트의 비동기 처리란 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 특성을 의미한다. 얼마전에 공부한 Web API의 setTimeout()도 비동기 처리 사례라고 볼 수 있다.

 

// #1
console.log('Hello');
// #2
setTimeout(function() {
    console.log('Bye');
}, 3000);
// #3
console.log('Hello, Again');

위의 결과 값으로 Hello => Hello Again => (3초후) Bye 가 출력된다.

대처법으로 콜백 함수를 사용한다.

 

콜백함수 Callback

콜백함수를 사용하면 비동기 작업의 완료, 결과를 처리할 수 있다.

콜백 함수는 함수를 다른 함수의 매개변수로 전달하고, 어떤 작업이 완료되면 해당 함수를 호출하는 방식이다.

function readFileAsync(filename, callback) {
  // 파일을 비동기적으로 읽는 작업을 수행
  // 작업이 완료되면 콜백 함수를 호출하고 데이터를 전달
  // 오류가 발생하면 오류 정보를 전달
}

// 콜백 함수를 전달하여 파일 읽기 작업을 시작
readFileAsync('example.txt', function(err, data) {
  if (err) {
    console.error('파일을 읽는 중 오류 발생:', err);
  } else {
    console.log('파일 내용:', data);
  }
});

다만 이런 콜백함수를 중첩하여 사용할 때 코드 가독성과 유지보수성이 떨어지는 현상이 나타나는데, 이를 콜백 지옥(Callback hell)이라고 한다.

readFile('file1.txt', function(err, data1) {
  if (err) {
    console.error('file1을 읽는 중 오류 발생:', err);
    return;
  }

  readFile('file2.txt', function(err, data2) {
    if (err) {
      console.error('file2를 읽는 중 오류 발생:', err);
      return;
    }

    const combinedData = data1 + data2;

    writeFile('output.txt', combinedData, function(err) {
      if (err) {
        console.error('output.txt를 쓰는 중 오류 발생:', err);
        return;
      }

      console.log('파일 합치기 완료');
    });
  });
});

작업량이 적을 땐 괜찮지만, 불러올 데이터도 많아지고 작업량이 많아지면 모든 과정을 비동기로 처리하게 되면 콜백 안에 콜백을 무는 식으로 코딩을 하게 된다. 이런 구조를 바꾸기 위해 PromiseAsync를 사용한다.

Promise

프로미스는 생성된 시점에는 알려지지 않았을 수도 있는 값을 위한 대리자로, 비동기 연산이 종료된 이후에 결과 값과 실패 사유를 처리하기 위한 처리를 연결할 수 있다. 비동기 메서드에서 동기 메서드처럼 값을 반환할 수 있다. 최종 결과를 반환하기보다 미래의 어떤 시점에 결과를 제공하겠다는 약속을 반환한다.

 

 

Promise의 상태

  • 대기(pending): 이행하지도, 거부하지도 않은 초기 상태.
  • 이행(fulfilled): 연산이 성공적으로 완료됨.
  • 거부(rejected): 연산이 실패함.

Promise Process

위 다이어그램을 보면 쉽게 이해할 수 있다.

 

Promise()

const myFirstPromise = new Promise((resolve, reject) => {
  // do something asynchronous which eventually calls either:
  //
  //   resolve(someValue)        // fulfilled
  // or
  //   reject("failure reason")  // rejected
});

연습한 코드들을 예시로 보겠다.

function runInDelay(seconds) {
  return new Promise((resolve, reject) => {
    if (!seconds || seconds < 0) {
      reject(new Error("0 이상으로 설정해야한다."));
    }
    setTimeout(resolve, seconds * 1000);
  });
}

runInDelay(1)
  .then(() => console.log("타이머 완료"))
  .catch(console.error)
  .finally(() => console.log("끝났다"));

 

Promise객체는 new 키워드와 생성자를 사용해 만든다. 생성자는 매개변수로 실행 함수를 받는다. 이 함수는 매개 변수로 두 가지 함수를 받아야 하는데, 첫 번째 함수 resolve*는 비동기 작업을 성공적으로 완료결과를 값으로 반환할 때 호출해야 하고, 두 번째 함수 reject작업이 실패하여 오류의 원인을 반환할 때 호출하면 된다. 두 번째 함수는 주로 오류 객체를 받는다.

 

✔️ 콜백은 Event Loop가 현재 실행중인 콜 스택을 완료하기 이전에는 절대 호출되지 않는다.
고전적인 콜백 함수 전달 방식은 오류를 놓칠 수 있는 위험이 있다.

✔️ then()을 여러 번 사용하여 여러 개의 콜백을 추가할 수 있다.

✶ 가장 뛰어난 장점은 Chaining이다.

 

Chaining

보통 비동기 작업을 두개 이상으로 순차적으로 실행해야하는 상황을 흔하게 볼 수 있다. 처음 비동기 작업이 성공하고 나서 그 결과값을 이용하여 다음 비동기 작업을 실행해야되는 경우.

function fetchEgg(chicken) {
  return Promise.resolve(`${chicken} => 🥚`);
}

function fryEgg(egg) {
  return Promise.resolve(`${egg} => 🍳`);
}

function getChicken() {
  return Promise.reject(new Error("Where is the chicken?"));
  //   return Promise.resolve(`🐣 => 🐔`);
}

getChicken()
  .catch(() => "🐔")
  .then(fetchEgg)
  .then(fryEgg)
  .then(console.log);

 

1️⃣ getChicken() 함수는 Promise.reject를 사용하여 에러를 발생시키는 프로미스를 반환한다.

2️⃣.catch(() => "🐔")는 앞서 발생한 에러를 처리하고 "🐔" 값을 반환한다. 이 부분은 에러 핸들링을 하고 기본 값으로 "🐔"을 반환하므로 에러가 발생하더라도 체이닝이 중단되지 않는다. (생략하면 오류 메세지를 뱉는다.)
3️⃣.then(fetchEgg)는 이전 단계에서 반환된 값을 fetchEgg 함수에 전달하고, 그 결과를 반환한다.

✔️ 비동기 작업을 순차적으로 연결하는 핵심.
4️⃣.then(fryEgg)는 fetchEgg 함수에서 반환된 값을 fryEgg 함수에 전달하고, 그 결과를 반환한다.
5️⃣.then(console.log)은 최종 결과를 출력한다. 이 단계에서는 "🍳"가 출력된다.

결과 : 🐔 => 🥚 => 🍳

 

async/await

function getGhost() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("👻");
    }, 1000);
  });
}

function getAilen() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("👽");
    }, 3000);
  });
}

function getDevil() {
  return Promise.reject(new Error("No devil.. 👿"));
}

//유령이랑 외계인 같이 갖고 오기

function fetchCreepy() {
  return getGhost() //
    .then((ghost) =>
      getAilen() //
        .then((ailen) => [ghost, ailen])
    );
}

fetchCreepy() //
  .then((creepy) => console.log(creepy));

위에서 유령이랑 외계인 같이 갖고 오기 부분을 보자. 결국 중첩이 추가되면 프로미스도 콜백지옥과 다름 없는 형태가 된다.

async/await 함수를 적용하여 더 가독성을 개선해보겠다.

// async로 바꾸기
async function fetchCreepy() {
  const ghost = await getGhost();
  const ailen = await getAilen();
  return [ghost, ailen];
}

fetchCreepy() //
  .then((creepy) => console.log(creepy));

비동기 코드를 더 쉽게 작성하고 관리하기에 좋다.

async 함수 내에서 await 키워드를 사용하여 비동기 작업의 완료를 기다릴 수 있다. await을 사용하면 코드 실행이 일시 중단되며 해당 작업이 완료될 때까지 기다린 후 결과를 반환한다.

Promise를 기반으로한 비동기 작업을 읽기 쉽게 작성하는 데 사용된다.

 

fetch API

Fetch API는 HTTP 파이프라인을 구성하는 요청과 응답 등의 요소를 JavaScript에서 접근하고 조작할 수 있는 인터페이스를 제공한다. Fetch API가 제공하는 전역 fetch() 메서드로 네트워크의 리소스를 쉽게 비동기적으로 얻을 수 있다.

 

console.log로 fetch API 불러오기

 

console.log로 fetch API를 불러보면 Promise가 뜬다. 이를 통해 프로미스 기반인 것을 확인할 수 있었다.

 

dummy json을 통한 테스트

 

.then을 사용하여 promise 내부 리소스를 확인할 수 있다.

이 상태로는 Body 내부를 볼 수 없고, 사용이 어렵기에 json 형식으로 변환해주어야 한다. 이 또한 .then을 사용한다.

 

fetch('https://dummyjson.com/products/1')
.then(res => res.json())
.then(data => console.log(data))

json 양식의 더미데이터

 

그리고 오류를 총 두 개를 잡아줘야하는데, 먼저 fetch 함수가 네트워크 요청을 보내는 중에 발생할 수 있으므로 try, catch를 사용하여 처리하고, HTTP 응답 상태의 코드가 200~299사이인지 나타내는 reponse.ok가 true하면 HTTP 요청이 성공적으로 처리되었다는 의미로 둘다 써주면 좋다. 어디에 오류가 났는지 상세하게 확인할 수 있다고 하니.. 🤔

하지만 또 이건 테스트 코드로 추후에 적용하게 되면 코드가 간결해질 수 있겠다.

 

오늘 배운 것을 기반으로 리팩토링을 해보았다. 특히 오류는 어떻게 처리해야될 지 몰라 헤맸는데 다행이었다.

 

Before

function onGeoOK(position) {
  const { latitude: lat, longitude: lon } = position.coords;
  const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric`;

  fetch(url)
    .then((response) => response.json())
    .then(displayWeather);
}

function onGeoError() {
  alert("Can't find you");
}

navigator.geolocation.getCurrentPosition(onGeoOK, onGeoError);

After

function onGeoOK(position) {
  const { latitude: lat, longitude: lon } = position.coords;
  const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric`;

  fetch(url)
    .then((response) => {
      if (!response.ok) {
        throw new Error("Network response was not OK");
      }
      return response.json();
    })
    .then(displayWeather)
    .catch(onGeoError);
}

function onGeoError() {
  alert("Sorry, we couldn't find your location.");
}

navigator.geolocation.getCurrentPosition(onGeoOK, onGeoError);

아직 완전히 익히지는 못한 것 같다. 계속 연습해봐야겠다.

 

fetch 쉽게 알려주는 유튜브를 찾았다.

https://www.youtube.com/watch?v=cuEtnrL9-H0


 

[자바스크립트 Promise 쉽게 이해하기]

[Using promises]

[Fetch API 사용하기]

[Promise() 생성자]

[Promise.resolve()]

[async function]

[드림코딩 아카데미 자바스크립트 마스터리 ES6]

ChatGPT 🤖