자바스크립트 비동기 프로그래밍 -2-

캡틴 노드랭크·2021년 6월 15일
0

JavaScript의 비동기 처리방식

콜백(Callback)

ES6에서 Promise가 표준화 되기 전까지 비동기를 처리하는 방식이었다.

JavaScript에서의 콜백함수는 다른 함수를 인자로 받아 결과를 출력하는 로직을 콜백함수라고 한다. 레퍼런스 블로그를 참고하여 콜백의 작성예시를 확인해보자.

function blogging(post, cb) {
  alert(`${post}를/을 작성해주세요.`);
  cb();
}

blogging('블로그 글', function () {
  alert('작성완료!');
});

위의 예제에서 blogging()이라는 함수의 마지막(혹은 두번째) 매개변수를 추가해주고, 실행해주는 문법을 담아준다. blogging()을 호출할떈 두번째 자리에서 함수를 정의한다.

alert()을 실행해줬기 때문에 블로그 글를/을 작성해주세요.작성완료! 를 브라우저에서 alert 메세지로 받게된다.

아래와 같은 방법으로도 콜백함수를 정의할 수있다.

function blogging(post, cb) {
  alert(`${post}를/을 작성해주세요.`);
  cb();
}

function finished() {
  alert('작성완료!')
}

blogging('블로그 글', finished);

기능은 위의 예제와 동일하지만, blogging()을 호출 시 두번째 인자로 함수의 정의를 준다.

콜백 함수로 비동기 처리

자바스크립트의 대표적인 내장 비동기 함수인 setTImeout()이 있다. 참고한 블로그의 레퍼런스에서 38줄의 코드로 트위터 봇을 만드는 예제로 알아보자.트위터의 API에 요청을 보낼 시 받는 응답에 대해 콜백함수를 다룬 내용이 담겨있다.

let Twitter = require ( 'twitter'); 
let config = require ( './ config.js'); 
let T = new Twitter (config);
// 검색 매개 변수 설정 
let params = { 
  q : '#nodejs', 
  count : 10, 
  result_type : 'recent', 
  lang : 'en' 
}

T.get('search/tweets', params, function(err, data, res) {
   if(!err) {
     //아래의 예제가 이곳에 작성되어야함.
   } else {
     console.log(err);
   }
}

T.get 은 Twitter에 GET 요청을 보낸다는 뜻으로, 3개의 인자가 존재한다.
'search/tweets' : 요청의 경로
params: 검색인자들, 데이터가 담겨있다.
익명함수 : 콜백함수

Twitter가 응답하게 되면 콜백함수가 실행되는 코드로 에러(err) 혹은 응답(res)를 보낼것이고, if문을 통해 요청이 성공했는지 아닌지를 판단할 수있다.

// 반환된 트윗을 반복한다.
    for(let i = 0; i < data.statuses.length; i++){
      // 반환된 data에서 트윗 id를 가져온다.
      let id = { id: data.statuses[i].id_str }
      // 선택한 트윗을 콜백함수로 다시 즐겨찾기에 추가한다.
      T.post('favorites/create', id, function(err, res){
        // 즐겨찾기에 실패할 경우 에러메세지를 콘솔로 기록한다.
        if(err){
          console.log(err[0].message);
        }
        // 즐겨찾기에 성공하면 트윗 URL을 기록한다.
        else{
          let username = res.user.screen_name;
          let tweetId = res.id_str;
          console.log('Favorited: ', `https://twitter.com/${username}/status/${tweetId}`)
        }
      });
    }

이 코드는 T.get 요청 시 에러발생(존재하거나 이상없음)이 없을 경우 트윗을 즐겨찾기에 추가한다. 그렇기에 if(!err)문 블록 안으로 두번째 예제의 코드가 들어간다.

콜백지옥(Callback Hell)

기존 콜백 함수로 비동기처리를 위해선 콜백 함수를 연속으로 사용할 때 발생하는 문제가 콜백 지옥(Callback Hell)이다.

.get('url', function(response) {
	parseValue(response, function(id) {
		auth(id, function(result) {
			display(result, function(text) {
				console.log(text);
			});
		});
	});
});

이렇게 콜백함수가 꼬리의 꼬리를 무는 상황이 발생한다면 가독성이 떨어지게되고, 로직을 변경하기도 어려워진다.

프로미스(Promise)

프로미스는 자바스크립트 비동기 처리에 사용되는 객체이다. 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용하며, 웹 애플리케이션을 구현할 떄 서버에서 데이터를 요청하고 받아오기 위해 쓰인다.

기존 콜백방식

프로미스의 3가지 상태(state)

Pending(대기)

new Promise() 메서드를 호출하면 기본적으로 대기(Pending) 상태가 된다.

new Promise()

new Promise()를 호출할 경우 콜백함수 선언이 가능하며, 인자는 resolve, reject이다.

new Promise(function(resolve, reject){
  ....
})

Fullfield(이행)

콜백 함수의 인자 resolve를 실행하게 되면 이행(Fullfield)상태가된다.

function getData() {
 
 new Promise(function(resolve, reject){
    let data = 100;
    resolve(data);
  });
}

이행 상태가 된다면 .then()을 붙여 처리결과 값을 받을 수 있다.

getData().then(function(resData){
  console.log(resData)
})

Rejected(실패)

new Promise()에서 두번째 인자로 reject를 호출하게 되면 실패 상태가 된다.

function getData() {
  return new Promise(function(resolve, reject) {
    reject(new Error("Request is failed"));
  });
}

실패 상태가 되면 원인을 찾기 위해 .catch()를 사용하여 애러를 얻을 수 있다.

getData().then().catch(function(err) {
  console.log(err); // Error: Request is failed
});

프로미스 체이닝

function delay(time) {
    return new Promise(function(resolve, reject){
        setTimeout(resolve, time);
    });
}

delay(1000)
.then(function(){
    console.log("after 1000ms");
    return delay(2000);
})
.then(function(){
    console.log("after another 2000ms");
})
.then(function(){
    console.log("step 4 (next Job)");
    return delay(5000);
})

위 예제는 여러개의 프로미스를 연결하여 사용할 수 있다. .then() 이나 catch()메서드를 호출하고 나면 새로운 프로미스 객체가 반환되기때문에 사슬처럼 이어붙일수가 있다. Promise의 then()catch() 메서드는 결과 값을 수행할 로직을 담은 콜백 함수 또는 예외 처리 로직을 담은 콜백 함수를수를 인자로 받게된다.

delay(1000)을 호출하면 1초 후에 프로미스가 이행되고, 첫번째 then()이 호출된다. 첫번째 then()에서 delay(200)을 반환하면서 프로미스가 생성되고, 2초가 지나면 두번째then()이 호출된다. 이런 과정이 계속 이어진다.

다른 예시(fetch())

REST API를 호출할 떄 사용하는 브라우저 내장 함수인 fetch() 함수는 URL을 인자로, 호출 결과를 Promise 객체로 리턴한다. 실제로 어떤 서비스의 API를 호출 하게되면, 정상적인 응답 결과를 출력할 수 있다.

fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then((response) => console.log("response:", response))
  .catch((error) => console.log("error:", error)); error));

- 결과

response: Response {type: "cors", url: "https://jsonplaceholder.typicode.com/posts/1", redirected: false, status: 200, ok: true,}

실제로 유효한 URL을 fetch() 함수의 인자로 넘기면 예외가 발생하지 않고 then()으로 인자로 넘긴 콜백 함수가 호출되어 상태코드: 200이 출력된다.

어떠한 결과도 넘기지 않는다면 다음과 같은 에러가 뜬다

fetch()
  .then((response) => console.log("response:", response))
  .catch((error) => console.log("error:", error));

VM2096:3 error: TypeError: Failed to execute 'fetch' on 'Window': 1 argument required, but only 0 present. at <anonymous>:1:1

이번엔 then() 이 어떤 콜백함수 자체를 인자로 받지 못하자, .catch() 메서드의 인자로 넘어간 콜백함수가 호출되어 에러가 출력된다.

fetch()를 활용한 프로미스 체이닝

fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then((response) => response.json())
  .then((scroll) => console.log("scroll:",scroll))
  .catch((error) => console.log("error:", error));

이번에는 응답 결과가 아닌 전문을 json으로 출력하고 싶을땐 .then()을 추가로 작성하여 .json() 메서드를 사용하면 된다.

- 결과

scroll: {userId: 1, id: 1, title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", body: "quia et suscipit\nsuscipit recusandae consequuntur …strum rerum est autem sunt rem eveniet architecto"}

다른 방법으로, 특정한 데이터가 필요한 경우 아래와 같은 방식으로 체이닝이 가능하다.

fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then((response) => response.json())
  .then((scroll) => scroll.userId)
  .then((userId) => `https://jsonplaceholder.typicode.com/users/${userId}`)
  .then((url) => fetch(url))
  .then((response) => response.json())
  .then((user) => console.log("user:", user))
  .catch((error) => console.log("error:", error));

이 방법은 만약 포스트를 작성한 userId 1을 가진 유저의 데이터가 필요한 경우인데, 2번째~4번째 줄은 userId만 추출하여 유저의 상세 조회를 위한 API의 URL을 만들어 리턴해준다.

5번째 줄에선 다시 fetch(url)을 통해 새로운 Promise 객체를 반환하고 다음과 같은 결과가 출력된다.

user: {id: 1, name: "Leanne Graham", username: "Bret", email: "Sincere@april.biz", address: {…}, …}

하지만 이러한 방법들도 최근에는async/await 방식으로 비동기를 구현하고있다.

비동기함수(Async && Await)

Promise의 불편한 사항들을 해결하기 위해 ES7에서 추가되었으며, async/await 키워드를 사용하면 더 간단한 코드를 작성할 수 있다.

위의 Promise 예제를 함수형으로 바꿔보자.

function fetchName(postId) {
  return fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
    .then((response) => response.json())
    .then((post) => post.userId)
    .then((userId) => {
      return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
        .then((response) => response.json())
        .then((user) => user1.name);
    });
}

fetchName(1).then((name) => console.log("name:", name));

- 결과

name: Leanne Graham

마지막 줄의 fetchName()을 호출할 경우 프로미스 객체를 반환하기 때문에 then() 메서드를 사용하여 원하는 결과를 반환해야한다. 어쨋든 Promise를 사용하면 다음과 같은 문제점이 발생한다.

예외처리

Promise를 사용할 경우 try/catch대신 catch() 메서드를 사용하여 예외처리를 한다. 만약 동기코드와 비동기 코드가 섞여 있을 경우 예외 처리가 난해해지거나 예외 처리를 누락하는 경우가 발생한다.

들여쓰기

복잡한 구조의 비동기 처리 코드를 작성하게 될 경우, then() 메서드의 인자로 넘기는 콜백 함수 내에 조건문이나 반복문을 사용하거나, 여러개의 Promise를 중첩해서 호출하는 경우들이 발생한다. 이렇게 되면 다단계 들여쓰기를 해야할 확률이 높아지며 코드 가독성은 점점 높아지게 된다.

Promise hell 예시

return getCurrentUser()
  .then((user) => {
    if (attachFavouriteFood) {
      return getFood(user.favouriteFoodId)
        .then((food) => {
          user.food = food;
          return user;
        });
    }
    return user;
  .then((user) => {
    if (attachSchool) {
      return getSchool(user.schoolId)
        .then((school) => {
          user.school = school;
          if (attachFaculty) {
            return getUsers(school.facultyIds)
              .then((faculty) => {
                user.school.faculty = faculty;
                return user;
              });
          }
          return user;
        });
    }
    return user;
  });
  });

이제 다시 async/await으로 돌아와서 promise의 코드를 재작성해보자.

async function fetchName(postId) {
  const postRes = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
  const post = await postRes.json();
  const userId = post.userId;
  const userRes = await fetch(`https://jsonplaceholder.typicode.com/posts/${userId}`
)
  const user = await userRes.json();
  return user.name;
}

//아래와 같은 부분을
fetchName(1).then((name) => console.log("name:", name));

//이렇게 바꿔줄 수 있다.
async function printFetchName(postId) {
  const name = await fetchName(postId);
  console.log("name:", name);
}

printFetchName(1) 

- 결과

name: Leanne Graham

함수 선언시 asyncfunction 키워드 앞에 작성하며, Promise를 리턴하는 모든 비동기 함수 앞에는 await 키워드를 붙여야 한다.

await은 반드시 async 키워드가 붙어있는 함수 내부에서만 사용이 가능하며, 반드시 Promise를 반환하는 함수 앞에 사용해줘야한다.

예외 처리

동기/비동기 구분 없이 try/catch로 일관되게 예외 처리를 할 수 있는 부분도 async/await을 사용할 수 있다.

async function fetchName(postId) {
  const postRes = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
  const post = await postRes.json();
  const userId = post.userId;
 
  try {
  const userRes = await fetch(`https://jsonplaceholder.typicode.com/posts/${userId}`)
  const user = await userRes.json();
  return user.name;
 }catch {
    console.log("Faile to fetch user:", err);
    return "누군지 못찾겠음.";
 }
}

fetchName(1).then((name) => console.log("name:", name));

ref: "비동기처리1","비동기처리2","비동기처리3","비동기처리4","프로미스 지옥 탈출법","위에서 게시한 레퍼런스 블로그"

profile
다시 처음부터 천천히... 급할필요가 없다.

0개의 댓글