[TIL] 211012

📝 오늘 한 것

  1. promise / promiseStatus / executor / resolve / reject / then / catch / finally / promise chaining(프로미스 연결) / 에러 처리 / 콜백 지옥 탈출하기 / async

📖 학습 자료

  1. 드림코딩 유튜브 '자바스크립트 기초 강의' 12 ~ 13편

📚 배운 것

1. Promise(프로미스)

  • 비동기 처리를 간편하게 하기 위해 사용되는, 자바스크립트 내장 오브젝트

  • 정해진 장시간의 기능을 수행하고 나서, 정상적으로 기능이 수행됐다면 처리된 결과 값을 전달하고, 문제가 발생했다면 에러를 전달한다.

    ex) 등록 버튼을 눌러 이메일을 입력해 게임을 사전 예약 해두면, 게임이 오픈되자마자 이메일로 공지를 받을 수 있다. 한편, 게임이 오픈된 후 등록 버튼을 누르면, 이미 게임이 오픈된 후이므로 기다릴 필요 없이 바로 이메일로 공지를 받게 된다.


1) 공부 point !

1) status

프로세스가 처리를 수행 중인지
수행이 완료되어 성공했는지 실패했는지

  • pending
    지정한 처리를 수행 중일 때
  • fullfilled
    처리를 성공적으로 끝냈을 때
  • rejected
    파일을 찾을 수 없거나 네트워크에 문제가 생겼을 때

2) Producer와 Consumer

  • Producer
    지정한 처리를 수행하여 데이터를 제공한다
    여기서는 promise가 해당된다
  • Consumer
    원하는 데이터를 소비한다

2) Promise 생성

promise는 우리가 원하는 기능을 비동기적으로 수행한다
지정한 처리를 수행해 consumer에게 데이터를 제공하는 producer에 해당한다


const promise = new Promise((resolve, reject) => {
  // 보통 다소 시간이 걸리는 무거운 기능을 수행한다 (네트워크 통신, 파일 읽어오기)
});

💡 promise는 오브젝트이므로, Promise 생성자 함수를 통해 생성할 수 있다.

promise 생성자 함수는 executor라는 콜백 함수를 인자로 전달받는다. executor는 resolvereject라는 2가지 콜백 함수를 인자로 전달받는다.

  • resolve
    기능을 정상적으로 수행한 후 마지막에 최종 데이터를 전달할 때 호출된다.
  • reject
    기능을 수행하다 문제가 생기면 호출된다.

const promise = new Promise((resolve, reject) => {
  console.log('doing something...');
});
// console 창에 바로 출력된다.

💡 그런데 promise가 생성된 순간, Promise 생성자 함수에 인자로 전달한 executor(콜백 함수)가 바로 실행된다.

따라서, 만약 네트워크 요청을 사용자가 요구했을 때만 해야 하는 경우라면, 사용자가 요구하지도 않았는데 불필요한 네트워크 통신이 일어나게 된다. 항상 이를 유의해서 promise를 작성해야 한다.


const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('syong');
  }, 2000);
});

💡 setTimeout을 통해 네트워크 통신을 하는 것처럼 delay를 줘서 위와 같이 promise를 생성할 수 있다.

promise는 2초 동안 어떤 기능을 수행하는데(실제라면 '어떤 기능'은 네트워크 통신, 파일 읽어오기 등이 해당한다.)
결국 이를 성공적으로 수행한 경우, resolve라는 콜백 함수를 호출한다.
resolve 함수는 syong이라는 데이터를 최종적으로 전달해준다.


3) then, catch, finally

then, catch, finally를 이용해 promise로부터 원하는 데이터를 받아올 수 있다
이들은 promise를 이용하는 consumer에 해당한다


💡 promise에서 기능이 성공적으로 수행됐다면, then()을 이용해 그 데이터(value)를 받아와 콜백 함수를 실행한다.

promise.then(value => {
  console.log(value);
});

위 예제의 경우, value에는 promise에서 resolve 함수를 통해 받아온 syong이라는 데이터가 전달된다.
promise가 성공적으로 수행된 후(2초 뒤에) console 창에 syong이 출력된다.


💡 promise에서 기능을 수행하다 문제가 생겼다면, catch()를 이용해 그 오류 메시지(error)을 받아와 콜백 함수를 실행한다.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    // resolve('syong');
    reject(new Error('no network'));
  }, 2000);
});

promise
  .then(value => { // 정상적으로 기능 수행 완료 시
    console.log(value);
  })
  .catch(error => { // 기능 수행 중 문제 발생 시
    console.log(error);
  });

위 코드는 chaining을 이용한 코드이다.
promise 오브젝트에 then() 메서드를 호출하면 promise 오브젝트를 반환한다.
반환된 promise에 다시 catch() 메서드를 호출한 것이다.

위 예제의 경우, error에는 promise에서 reject 함수를 통해 받아온 'no network'라는 오류 메시지가 전달된다.
promise가 기능 수행 중 문제가 발생하면(2초 뒤에) console 창에 no network가 출력된다.


💡 finally()는 promise가 기능 수행에 성공하든 실패하든 상관없이, then() 혹은 catch()가 호출된 후, 무조건 마지막에 호출된다.

promise
  .then(value => {
    console.log(value);
  })
  .catch(error => {
    console.log(error);
  })
  .finally(() => {
    console.log('finally');
  });

// console 창 출력 결과

// Error: no network
// finally

4) 프로미스 연결(Promise Chaining)

then() 메서드를 이용해 다수의 비동기 처리들을 묶어서 처리할 수 있다.

// 서버에서 숫자를 받아오는(setTimeout으로 대체) promise를 생성한다.
const fetchNumber = new promise((resolve, reject) => {
  setTimeout(() => resolve(1), 1000);
});

// `then()`을 이용해 그 데이터(value)를 받아와 콜백 함수를 실행한다.
// then()은 promise의 값을 전달할 수도 있고, 새로운 promise를 전달할 수도 있다.

fetchNumber
  .then(num => num * 2) // then()은 promise로부터 1의 값을 받아와, 콜백 함수를 실행한 후, 2의 값을 가지는 promise를 반환한다.
  .then(num => num * 3) // then()은 promise로부터 2의 값을 받아와, 콜백 함수를 실행한 후, 6의 값을 가지는 promise를 반환한다.
  .then(num => { // then()은 promise로부터 6의 값을 받아온 후, 새로운 promise를 반환한다.
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(num - 1), 1000);
      // 숫자를 다른 서버에 보내(setTimeout으로 대체) 다른 숫자로 변환된 값을 받아온다.
      // 새로운 promise의 resolve 함수는, 6 - 1이라는 값을 전달한다.
    });
  })
  .then(num => console.log(num)); // then()은 새로운 promise로부터 5라는 값을 받아와, 이를 출력한다.

5) 에러 처리(Error Handling)

에러 상황에 반응하고, 에러를 복구하는 것을 말한다


💡 아래의 getHen, getEgg, cook 함수는, 각각 다른 값을 가지는 promise를 리턴한다.

const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve('🐓'), 1000);
  });

const getEgg = hen =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${hen} => 🥚`), 1000);
  });

const cook = egg =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍳`), 1000);
  });

getHen() // getHen 함수를 실행하여 '🐓'을 값으로 가지는 promise를 만든다.
  .then(hen => getEgg(hen)) // '🐓'이라는 값(hen)을 받아와 getEgg 콜백 함수를 실행한 후 '🐓 => 🥚'을 값으로 가지는 promise를 반환한다.
  .then(egg => cook(egg)) // '🐓 => 🥚'이라는 값(egg)을 받아와 cook 콜백 함수를 실행한 후 '🐓 => 🥚 => 🍳'를 값으로 가지는 promise를 반환한다.
  .then(meal => console.log(meal)); // '🐓 => 🥚 => 🍳'이라는 값(meal)을 받아와 이를 출력한다.

페이지를 새로고침 한 후 3초가 지나면, console 창에 '🐓 => 🥚 => 🍳'가 출력된다.


💡 이때, getEgg 함수에서 promise가 기능을 수행하다가 문제가 생겼다고 가정하자. getEgg 함수의 setTimeout 안의 resolve 함수를 reject 함수로 바꿔적는다.

const getEgg = hen =>
  new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error(`error! ${hen} => 🥚`)), 1000);
  });

의도한 대로라면, console 창에 reject 함수를 통해 받아온 오류 메시지가 떠야 한다.
그러나, 페이지를 새로고침 한 후 2초가 지나면, console 창에 아래와 같은 진짜 오류가 뜬다.

Uncaught (in promise) Error: error! 🐓 => 🥚

위 오류는 promise에서 에러가 잡히지 않는다는 뜻이다.
이를 해결하기 위해 promise가 성공적으로 기능을 수행했을 때 실행되는 then() 말고, promise가 기능을 수행하는 중에 문제가 발생했을 때 실행되는 catch()를 등록해야 한다.

getHen()
  .then(hen => getEgg(hen))
  .then(egg => cook(egg))
  .then(meal => console.log(meal))
  .catch(error => console.log(error));
  // getEgg 함수의 setTimeout 안의 reject 함수가 전달하는 오류 메시지가 catch()에 전달된다

비로소 console 창에 아래와 같이, reject 함수를 통해 받아온 오류 메시지가 뜨는 것을 확인할 수 있다.

Error: error! 🐓 => 🥚

한편, 위와 아래의 코드는 같다.

getHen()
  .then(getEgg)
  .then(cook)
  .then(console.log)
  .catch(console.log);

💡 위 예제는 getEgg 함수에서 promise가 기능을 수행하다가 문제가 생겼을 때, reject 함수를 통해 받아온 오류 메시지를 띄워주고, 더 이상의 코드는 실행되지 않는 경우이다.

한편, getEgg 함수에서 promise가 기능을 수행하다가 문제가 생겼을 때, 아예 오류 메시지를 다른 값으로 대체하고, 끝까지 코드가 실행되도록 할 수도 있다.

예를 들어, '달걀'을 받아올 때 문제가 생길 경우 다른 재료로 대체하고 싶다면 아래와 같이 코드를 수정할 수 있다.
then(getEgg) 바로 밑에 연달아 catch()를 적어 바로 문제를 해결하도록 한다.

getHen()
  .then(getEgg)
  .catch(error => {
    return '🥓';
  })
  .then(cook)
  .then(console.log)

// 🥓 => 🍳

6) 콜백 지옥 탈출하기

[TIL] 211011 3. 콜백 함수 3) 콜백 지옥 참고

  • 콜백 지옥 예제
class UserStorage {
  loginUser (id, password, onSuccess, onError) {
	setTimeout(() => {
      if (id === 'syong' && password === 'happy' ||
          id === 'corder' && password === 'nice') {
        onSuccess(id);
      } else {
        onError(new Error('not found'));
      }
    }, 2000);
  }
  
  getRoles (user, onSuccess, onError) {
	setTimeout(() => {
      if (user === 'syong') {
        onSuccess({ name: 'syong', role: 'admin' });
      } else {
        onError(new Error('no access'));
      }
    }, 1000);
  }
}

const userStorage = new UserStorage();

const id = prompt('id를 입력해주세요');
const password = prompt('password를 입력해주세요');

// 콜백 지옥
userStorage.loginUser(id, password, user => {
  userStorage.getRoles(user, userWithRoles => {
    alert(`${userWithRoles.role} ${userWithRoles.name}님, 환영합니다!`);
  }, error => {
    throw error;
  });
}, error => {
  throw error;
});
  • promise를 이용해 콜백 지옥 탈출하기
class UserStorage {
  loginUser (id, password) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (id === 'syong' && password === 'happy' ||
            id === 'corder' && password === 'nice') {
          resolve(id);
        } else {
          reject(new Error('not found'));
        }
      }, 2000);
    });
  }

  getRoles (user) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (user === 'syong') {
          resolve({ name: 'syong', role: 'admin' });
        } else {
          reject(new Error('no access'));
        }
      }, 1000);
    });
  }
}

const userStorage = new UserStorage();

const id = prompt('id를 입력해주세요');
const password = prompt('password를 입력해주세요');

userStorage.loginUser(id, password)
  .then(userStorage.getRoles)
  .then(user => alert(`${user.role} ${user.name}님, 환영합니다!`))
  .catch(console.log);

2. async · await

  • promise를 동기적으로 실행되는 것처럼 보이도록 만들어준다.

  • promise chaining을 할 때, 계속해서 다른 promise에 연결할 경우 코드가 난잡해질 수 있다.

    이때 async와 await을 이용하면, 코드를 동기식으로 즉, 순서대로 보다 간단하고 간편하게 작성할 수 있다.

  • 단, 무조건 async와 await으로 바꿀 수 있는 것은 아니다. 그대로 promise를 써야 할 때가 있다. 프로젝트를 해보면서 감을 읽혀야 한다.

cf. syntactic sugar(문법적 설탕)

기존에 존재하는 것 위에 혹은 기존에 존재하는 것을 감싸서, 좀 더 읽기 쉽고 이해하기 편하도록 만드는 것을 말한다.
ex) async, await, class 등


1) async

function fetchUser () {
  // 10초에 걸쳐 서버로부터 데이터를 받아오는 코드가 있다고 가정
  return 'syong'; // 서버로부터 'syong'이라는 데이터를 받아옴
}

const user = fetchUser();
console.log(user);

// 다른 코드 생략

💡 위와 같이 동기식으로 순서대로 작성해주면, 이 코드 뒤에 작성된 다른 코드들은, fetchUser 함수가 실행되어 syong이라는 데이터를 리턴하기까지 10초 동안 실행되지 못한다.

뒤에 웹 페이지의 UI 관련 코드가 있다면 사용자는 10초 동안 빈 페이지만을 보게 되는 문제가 발생한다. 따라서 이를 비동기적으로 수정해줄 필요가 있다.


function fetchUser () {
  return new Promise((resolve, reject) => {
    // 10초에 걸쳐 서버로부터 데이터를 받아오는 코드
    return 'syong';
  });
}

const user = fetchUser();
console.log(user);

💡 promise를 생성해 코드를 비동기적으로 수정해주었다. 그런데 위의 코드에서는 promise가 수행을 완료하여 성공 혹은 실패했을 때 실행되는 resolve나 reject를 작성해주지 않았다. 그저 데이터 'syong'을 return 하면서 끝난다.

이 경우, console 창에는 아래와 같이 뜬다.

[[PromiseState]]: "pending" // promise가 지정한 처리를 계속해서 수행 중이다
[[PromiseResult]]: undefined // promise는 서버로부터 어떤 데이터도 받지 않았다.

function fetchUser () {
  return new Promise((resolve, reject) => {
    // 10초에 걸쳐 서버로부터 데이터를 받아오는 코드
    resolve('syong');
  });
}

const user = fetchUser();
console.log(user);

💡 문제를 해결하기 위해 executor 함수 안에서 resolve 함수를 호출하도록 수정했다. 이제 promise는 기능을 정상적으로 수행한 후 최종 데이터를 전달하고자 resolve를 호출한다.

이 경우, console 창에는 아래와 같이 뜬다.

[[PromiseState]]: "fulfilled" // promise가 지정한 처리를 성공적으로 완료했다
[[PromiseResult]]: "syong" // promise에 syong이라는 데이터를 전달한다.

async function fetchUser () {
  // 10초에 걸쳐 서버로부터 데이터를 받아오는 코드
  return 'syong';
}

const user = fetchUser();
console.log(user);

// 다른 코드 생략

💡 한편, 함수 앞에 async를 추가하면 함수의 코드 블럭이 자동으로 promise로 바뀐다. 이 경우에 코드를 동기식으로 즉, 순서대로 작성하더라도 처리는 비동기적으로 이뤄진다.

따라서, promise가 지정된 처리를 수행하는 동안, 이 코드 뒤에 작성된 다른 코드들도 promise가 처리를 완료될 때까지 기다릴 필요 없이, 함께 실행된다.


✨ 내일 할 것

  1. 강의 계속 듣기 (await)

좋은 웹페이지 즐겨찾기