20210217_JavaScript08 async, Callback function, Promise Object

6 분 소요

JavaScript08

  • JavaScript에 대해 공부하기 위해서 유튜브 ‘드림코딩by엘리’ JavaScript 강의를 듣고 글을 쓰고 있다.(강의가 엄청 잘되 있으니 강의를 찾아보자!)




JavaScript Callback 비동기 처리의 시작 콜백 이해하기

동기(sync)와 비동기(async)

  • Synchronous(동기) : 코드 블럭이 쓰여진 순으로 즉시실행하는 것
  • javaScript는 동기적인데, 호이스팅 이후에 코드블럭을 순서대로 실행한다. (JavaScript is synchronous., Execute the code block in oder after hoisting.)
    • hoisting(호이스팅): var, function declaration 이 제일 위로 가는 것


  • Sync
console.log('1');
console.log('2');
console.log('3');
// 브라우저 콘솔창에는 1, 2, 3 순서대로 나타난다



  • Asynchronous(비동기) : 코드 블럭이 순차적으로 실행 되나 비동기 부분의 경우 브라우저에 요청만 하고 브라우저에 나타나는 시간이 달라 브라우저 상에서는 다른 순서로 출력 되는 경우


  • Async

    • setTimeout : 콜백함수와 밀리세컨을 param으로 받고 시간이 밀리세컨 만큼 지났을때 콜백함수 호출
    • 1000 밀리세컨드(1초 = 1000ms) 이후에 function을 호출 -> 브라우저가 시간에 맞게 실행하도록 요청해 놓고 다음 코드로 일단 넘어감
    • setTimeout 과 같은 브라우저 API들은 브라우저에게 먼저 요청을 보내게 되고 응답 안기다리고 다음 블럭으로 패스함
console.log('1');
setTimeout(() => console.log('2'), 1000); // 비동기 부분
console.log('3');
// 
  • 하지만, 항상 Callback 함수를 비동기시에만 사용하는 것이 아님




Callback Function

  • Callback 함수도 sync, async 모두 있음
  • Callback 함수 표현은 각 언어 마다 다름


  • Synchronous callback
function printImmediately(print) {
    print();
} // 이건 선언이니까 hoisting 되고
printImmediately(() => console.log('hello'));



  • Asynchronous callback
function printWithDelay(print, timeout) {
    setTimeout(print, timeout);
}
printWithDelay(() => console.log('async callback'), 2000); // 비동기


  • 하지만, 이러한 콜백함수를 너무 남발해서 쓰면 안좋음!!




Callback Hell example (callback 함수 사용의 잘못된 예)

  • hell example

    • 예제 구현사항 1) 사용자에게 id , password 받아오기 2) login 하기 3) login 성공시 Roles 가져오기 4) name: raccoon, role: admin 에서 가져온 name과 role값을 활용하여 로그인 성공 안내창 띄우기


  • 예제 설명
    • UserStorage 클래스 생성
      • loginUser 함수 : id, password를 받아 조건에 맞으면 성공 함수(받은 id를 활용), 안맞으면 실패함수(Error 처리) , 서버라고 생각하고 어느정도 시간 걸리게 함 (비동기)
      • getRoles 함수 : user 를 받아 조건에 맞으면 성공함수(배열을 활용함), 그외 Error처리
    • 연결)
      • prompt 함수를 통해 input 을 받아 id, password 할당
      • UserStorage 클래스를 userstorage 이름으로 instance화 시킴
      • userstorage에서 loginUser를 실행시킴
      • loginUser의 각 param으로 들어가는 콜백함수를 가공활용하여 창 띄우는 기능을 구현하게 연결시킴


class UserStorage {
    loginUser(id, password, onSuccess, onError) {
        setTimeout(() => {
            if (
                (id === 'raccoon' && password === 'dream') ||
                (id === 'coder' && password === 'academy')
            ) {
                onSuccess(id);
            } else {
                onError(new Error('not found'));
            }
        }, 2000);
    }

    getRoles(user, onSuccess, onError) {
        setTimeout(() => {
            if (user === 'raccoon') {
                onSuccess({ name: 'raccoon', role: 'admin' });
            } else {
                onError(new Error('no access'));
            }
        }, 1000);
    }
}


const userStorage = new UserStorage();
const id = prompt('enter your id!');
const password = prompt('enter your password!');
userStorage.loginUser(
    id,
    password,
    user => {
        userStorage.getRoles(
            user,
            userWithRole => {
                alert(`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`)
            },
            error => {
                console.log(error);
            }
        );
    },
    error => {console.log(error)}
);


  • 콜백 체인의 문제점
    • 가독성 제로
    • 에러발생 및 디버깅에서 어려움, 분석도 어렵고 유지보수도 어려움
  • 그래서 Promise가 필요함




JavaScript Promise Object

Promise

  • 비동기를 간편하게 처리할수 있도록 도와주고자 js에서 제공하는 Object 이다. (promise is a JavaScript object for asynchronous operation.)
  • 실행 성공하면 처리된 결과값 전달, 문제 발생시 error 전달


  • 공부 포인트 1) state (상태) : 기능실행중인지, 성공했는지, 실패인지 2) producer vs consumer


  • State (상태): pending(수행중) -> fulfilled(실행 성공) or rejected(파일 없거나, 네트워크 문제가 생기면)
  • promise Object의 활용법은 producer, consumer가 있음
    • producer : 원하는 기능을 수행해 해당하는 데이터를 만들어 내는 promise Object
    • consumer : 원하는 데이터를 소비하는 promise Object



Producer

  • Promise 객체는 param으로 executor 콜백을 받음
    • excutor 콜백은 param으로 resolve(정상 수행시), reject(실패시) 콜백함수를 받음


  • when new promise is created, the executor runs automatically.
    • promise를 만들자 마자 executor가 실행이 됨 -> 즉, 만들자마자 네트워크 통신을 시작함 (django view의 Request get느낌?)
    • 네트워크 통신을 사용자가 요구할때만 해야 되는 경우 (사용자가 버튼을 눌렀을 때) 가 있기 때문에
    • 불필요한 네트워크가 없게 유의하며 promise를 만들어야함


const promise = new Promise((resolve, reject) => {
    // doing some heavy work (network, read files)
    console.log('doing somthing...');
    setTimeout(() => {
        resolve('raccoon success!'); // 성공시 return 하는 값
        // reject(new Error('no network')) // 실패시 return Error
    }, 2000);
}); 



Consummers

  • producer에서 정의한 데이터 로직들을 통해 출력을 어떻게 할지 정하거나, 오류 예외처리 및 유기적으로 연결하여 기능을 구현
  • then 함수 : promise 객체로직이 성공적으로 수행시 resolve의 return을 받아 param으로 활용하는 콜백 함수를 param을 가짐
  • catch 함수 : promise 객체 로직에 erorr 가 생기거나, 성공하지 못하는 경우 (결국 그냥 reject 만나면)코드 실행을 멈추고 reject의 return을 받아 param으로 활용하는 함수를 param으로 가짐
  • finally 함수 : 성공하던 실패하던 무조건 호출 되는 함수


promise
    .then(value => {
    console.log(value);
    })
    .catch(error => {
        console.log(error); // error가 출력되지 않고 받아온 에러를 console.log에 출력됨을 알수 있음
    }) // then을 호출하면 다시 promise를 호출하게 되고 return된 promise에 catch를 등록함
    .finally(() => {console.log('finally')}) // 성공하든 안하든 무조건 호출



Promise chaning (프로미스 연결하기)

  • fetchNumber이름의 promise 객체 생성
  • 해당 객체 접근하여 promise를 다루는 함수 then가 어떻게 동작하는지 보기
    • producer 부분에서 resolve를 숫자 1로 넣음
    • then 함수안의 콜백함수의 로직을 통해서 원하는 기능 구현
    • then 함수들을 차례대로 나열하면서 기능 구현 (묶어서 가능하단 이야기)
"producer"
const fetchNumber = new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000);
});

"consumer"
fetchNumber
    .then(num => num * 2) // then은 값을 전달해도 되고 다른 비동기 promise를 전달해도 됨
    .then(num => num * 3)
    .then(num => {
        return new Promise((resolve, reject) => {
            setTimeout(() => resolve(num -1), 1000);
        });
    })
    .then(num => console.log(num)); // 5

// then들을 나열해서 다른 비동기적인 것을 묶어서 처리 가능



Error Handling (에러처리)

  • producer part
const getHen = () =>
    new Promise((resolve, reject) => {
        setTimeout(() => resolve('🐓'), 1000);
    });
const getEgg = hen =>
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(`${hen} => 🥚`), 1000);
        // setTimeout(() => reject(new Error(`error ${hen} => 🥚`)), 1000); // error가 발생한 경우
    });
const cook = egg =>
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(`${egg} => 🥮`), 1000);
    });


  • consumer part

  • then을 통한 연쇄 가공, 활용

    • then은 return된 resolve를 받아와 가공할수 있는 함수 이기 때문에
    • then안의 콜백함수가 promise을 이용하면 return 되는 것을 계속 연결 시킬수 있음
    • 즉, 연쇄적으로 가공, 활용이 가능하다는 것
getHen()
.then(hen => getEgg(hen)) 
.then(egg => cook(egg))
.then(meal => console.log(meal)) // 닭 => 계란 => 빵
.catch(console.log);
  • 꿀팁) resolve받아와서 같은 이름의 param을 바로 또 다른 함수에 넣는 경우 앞에 param, Arrow 생략 가능
getHen()
  .then(getEgg) 
  .then(cook)
  .then(console.log) // 닭 => 계란 => 빵
  .catch(console.log);



  • 에러 처리
    • catch를 통해서 에러 처리를 함 catch에서 발견할 수 있는데, 해당 producer part에서 어떤 에러인지 알기위해서 처리가 필요함
    • reject를 가공해서 Error를 일으키고 error안내를 설정할 수 있다.
const getEgg = hen =>
    new Promise((resolve, reject) => {
        // setTimeout(() => resolve(`${hen} => 🥚`), 1000);
        setTimeout(() => reject(new Error(`error ${hen} => 🥚`)), 1000); // 의도 하지 않은 것은 error를 일으켜 error처리 가능하고 reject의 콜백함수를 통해 error 메세지 가공이 가능
    });


  • Error를 그대로 가져오면 코드 실행이 멈추기 때문에 consumer part에서 then 과정에서 error가 날수 있는 부분에 catch안의 콜백함수를 통해서 받아오는 error를 다른 정상적인 값을 호출시켜 전체 프로세스에 영향을 주지 않고 값을 내게 만들수 있음
  • 주의) error 발생후 error를 바꿔서 return 시켜도 error 이전의 것은 모두 없어지는 것이라서 return 값 부터 반영되어 최종 값을 만들어냄
getHen()
.then(hen => getEgg(hen))
.catch(error => {
        return '😅'; // egg 받아오는게 문제가 생겨도 전체적이 프로세스는 진행이 되게 하여
    }) //error 발생 자체를 resolve로 다른것을 return 하게해서 진행시킴) 
.then(egg => cook(egg))
.then(meal => console.log(meal)) // 웃음 => 빵
.catch(console.log);




Callback hell -> Promise 바꾸기


class UserStorage {
    loginUser(id, password) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (
                    (id === 'raccoon' && password === 'dream') ||
                    (id === 'coder' && password === 'academy')
                ) {
                    resolve(id);
                } else {
                    reject(new Error('not found'));
                }
            }, 2000);
        })
    }

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



const userStorage = new UserStorage();
const id = prompt('enter your id!');
const password = prompt('enter your password!');
userStorage
    .loginUser(id, password)
    .then(userStorage.getRoles)
    .then(user => alert(`Hello ${user.name}, you have a ${user.role} role`))
    .catch(console.log);

  • 뭐, 엄청 크게 달라지진 않고 comsumer part가 좀더 이해하기 쉽게 바뀌고 깔끔함
  • 더 좋은 방식은 다음 시간에 알아보자~