5 minute read

비동기 처리

![https://github.com/hy6219/TIL-Today-I-Learned-/blob/main/FE/Javascript/%EB%8F%99%EA%B8%B0%EC%A0%81%EB%B9%84%EB%8F%99%EA%B8%B0%EC%A0%81.PNG?raw=true](https://github.com/hy6219/TIL-Today-I-Learned-/blob/main/FE/Javascript/%EB%8F%99%EA%B8%B0%EC%A0%81%EB%B9%84%EB%8F%99%EA%B8%B0%EC%A0%81.PNG?raw=true)

동기적, 비동기적 처리에 대한 이해-Velopert님 강의

  • 동기적 : 하나의 작업이 끝나야 다음 작업을 수행할 수 있음
  • 비동기적: 기다리는 과정에서 다른 작업을 수행할 수도 있음 && 동시에 여러 작업을 수행할 수도 있음

예를 들어, 연산량이 많은 작업을 수행한다고 가정해보자

//연산량이 많은 작업을 실행해볼 것!
function work(){
    const start =Date.now();//현재 날짜를 숫자 형태로 제공
    for(let i = 0 ; i < 1000000000;i++){

    }
    const end = Date.now();
    console.log(`${(end-start)}ms`);
}

work();//1022ms
console.log('다음작업');

이때, 콘솔을 확인해보면, work 함수가 먼저 수행되고 나서야, ‘다음 작업’을 출력하는 작업이 수행되었음을 확인해볼 수 있다!

이 작업에 대해서, 어떤 작업이 동시에 이루어지게 하려면

비동기 형태로 전환하기 위해서 setTimeOut 메서드를 이용할 것이다

setTimeOut(함수, 특정 작업을 수행하도록 하기까지의 시간(ms))

만약, 이때 시간에 대해서 값을 0을 넣어준다고 할지라도, 기본적으로 브라우저 내부에는 4ms 가 설정되어 있기 때문에 4ms 간격이 존재할 것이다

//연산량이 많은 작업을 실행해볼 것!
function work() {
    setTimeout(() => {
        const start = Date.now(); //현재 날짜를 숫자 형태로 제공
        for (let i = 0; i < 1000000000; i++) {

        }
        const end = Date.now();
        console.log(`${(end-start)}ms`);
    }, 0); //함수, 특정 시간이 흐른 후 작업을 하도록 하는 시간 값(ms단위)
}
console.log('작업 시작!');
//하지만 실질적으로 4ms(브라우저에서 정해진 기본 시간) 후에 진행될 것!
work(); //1022ms
console.log('다음작업');

위에 처럼 실행해보면, 작업 시작!에 대한 수행 이후,

for 루프를 실행하는 동안에 다음작업이 이루어져서 다음작업이 실행되고,

그 이후 work() 부분이 수행되었음을 알 수 있다!

만약 work 함수 수행 이후 다른 함수가 작동되도록 하고 싶다면 callback 함수를 넣어주면 된다!!

  1. work 함수의 파라미터로 callback함수를 의미하는 파라미터를 넣어준다!(callback이라는 이름이 아니어도 된다!)
  2. work 함수 내부에 존재하는 setTimeOut의 메서드 내부 최하단에 1에서 정의한 변수명을 이용해서

파라미터명(~);

처럼 마치 리터럴 함수처럼 생긴 함수를 적어준다!

  1. 그러고 나서 work함수를 실행하는 부분에 함수를 정의해준다!
//연산량이 많은 작업을 실행해볼 것!
function work(callback) {
    setTimeout(() => {
        const start = Date.now(); //현재 날짜를 숫자 형태로 제공
        for (let i = 0; i < 1000000000; i++) {

        }
        const end = Date.now();
        console.log(`${(end-start)}ms`);
        callback(end-start);
    }, 0); //함수, 특정 시간이 흐른 후 작업을 하도록 하는 시간 값(ms단위)
}
console.log('작업 시작!');
//하지만 실질적으로 4ms(브라우저에서 정해진 기본 시간) 후에 진행될 것!
work((elapsed)=>{
    console.log('work 작업이 끝났어요!');
    console.log(`${elapsed} ms 걸렸습니다!`);
}); //1022ms
console.log('다음작업');

비동기 작업으로 처리할 수 있는 일들

  • Ajax Web API 요청
  • 파일 읽기
  • 암호화/복호화
  • 작업 예약

Promise

  • 비동기 작업을 편하게 할 수 있도록 마련된 ES6의 기능!
  • 위의 콜백함수를 이용하는 경우, 비동기 작업이 많아지면 복잡해지는 경우가 많았는데 Promise가 이를 해결

-기존 방법(위의 방법처럼) setTimeOut을 이용하여 1부터 5까지 순차적으로 증가시키면서 출력해야하는 경우가 있다면,

function increaseAndPrint(n,callback){
    setTimeout(()=>{
        const increased=n+1;
        console.log(increased);
        if(callback){
            callback(increased);
        }
    },1000);
}

increaseAndPrint(0, n=>{
    increaseAndPrint(n, n=>{
        increaseAndPrint(n, n=>{
            increaseAndPrint(n,n=>{
                increaseAndPrint(n,n=>{
                    console.log('작업 끝!');  
                })
            })
        })
    })
});

와 같이 작성할 수 있는데, 이 방법으로는 코드가 복잡해진다!(callback hell=콜백 지옥)

Promise는 아래와 같은 두 가지 파라미터를 갖는다

  • resolve: 성공했을 때 호출되는 함수
  • reject : 실패했을 때 호출되는 함수

그리고

Promise 객체.then(함수)

로 promise 작업 후 실행될 작업을 정의해줄 수 있다!

(1) 성공했을 때

const myPromise = new Promise((resolve, reject)=>{
    //성공할 때 resolve 호출
    //실패하면 reject 호출
    setTimeout(()=>{
        resolve('result');
    },1000)
});

//프로미스가 끝났을 때 할 작업 정의
myPromise.then(result=>{
    console.log(result);
})//result

(2) 실패했을 때

const myPromise = new Promise((resolve, reject)=>{
    //성공할 때 resolve 호출
    //실패하면 reject 호출
    setTimeout(()=>{
        reject(new Error());
        /**
sync.js:48 Uncaught (in promise) Error
    at sync.js:48
*/
    },1000)
});

//프로미스가 끝났을 때 할 작업 정의
myPromise.then(result=>{
    console.log(result);
}).catch(e=>{
     console.error(e);   
})
/**
 * sync.js:60 Error
    at sync.js:48
(anonymous) @ sync.js:60
Promise.catch (async)
(anonymous) @ sync.js:59
 * 
 */

Promise를 만드는 함수를 작성해보자!

//값이 5가 되면 실패하는 프로미스를 만들것
function increaseAndPrint(n){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            const value = n + 1;
            if(value===5){
                const error=new Error();
                error.name='ValueIsFiveError';//에러명 정해주기
                reject(error);
                return;
            }else{
                console.log(value);
                resolve(value);
            }
        })
    },1000);
}

//프로미스는 연달아서 then을 사용 가능!
increaseAndPrint(0).then(n=>{
    return increaseAndPrint(n);
}).then(n=>{
    return increaseAndPrint(n);
}).then(n=>{
    return increaseAndPrint(n);
}).then(n=>{
    return increaseAndPrint(n);
}).then(n=>{
    console.log('result: ',n);
    return increaseAndPrint(n);
}).catch(e=>{
    console.error(e);
})
/**
 * sync.js:101 ValueIsFiveError
    at http://127.0.0.1:5501/sync/sync.js:76:29
 */

위와 같이 함수 내부에 new 생성자로 Promise 클래스 인스턴스를 만들어 반환하고, 나머지는

이전의 내용과 동일하다!

그리고 promise의 장점은 연달아서 then을 이용해서 작업을 수행할 수 있다는 것이다!

위의 코드 중 하단의 .then 부분은 공통적으로 n이 전달되기 때문에 아래와 같이 단축시킬 수 있다!

increaseAndPrint(0).then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.catch(e=>{
    console.error(e);
})
  • 코드의 깊이가 많이 깊어지지 않아서 좋지만, 어떤 부분에서 에러가 발생했는지 확인하기가 어렵고, 분기하는 과정을 고려하기 어렵고 번거로워서 async/await이 더 나을 수 있음!

Async/Await

  • Promise를 더욱 쉽게 만들어줄 수 있는 방법! ES8에서 적용되었음!
  • Promise를 이용한 작업에 대해서 .then을 붙여서 프로미스 전후 작업에 대한 처리에 대한 수고스러움을 하지 않아도 됨
//프로미스를 생성하는 함수 만들기
function sleep(ms){
    return new Promise(resolve=>setTimeout(resolve,ms));
}

async function process(){
    console.log('안녕하세요!');
    await sleep(1000);
    console.log('반갑습니다');
}

process();
/**
 * 안녕하세요
 * 반갑습니다
 */
  • 수행 단위 대상에 대해서 async, Promise 등 일정 시간동안 작업을 대기상태로 두고 싶은 곳의 앞에는 await를 붙이면 된다!

그리고 이때 프로미스가 담긴 함수, 공간에서는 프로미스를 반환받게 되어서 프로미스의 기능 중 하나인 then을 사용할 수 있다!

async function process(){
    console.log('안녕하세요!');
    await sleep(1000);
    console.log('반갑습니다');
    return true;
}

process().then(value=>{
    console.log(value);
})
/**
 * 안녕하세요
 * 반갑습니다
	true
 */
  • Promise에서 에러를 발생하고 싶다면 throw를 사용하고
  • 반대로, 에러처리를 하고 싶다면 try-catch를 사용하면 된다!
async function makeError(){
    await sleep(1000);
    const error=new Error();
    throw error;
}

async function process(){
    try{
        await makeError();
    }catch(e){
        console.error(e);
    }
}

process();

Promise.all, Promise.race

*화살표 함수를 사용하는데 async를 사용하고 싶다면

//화살표 함수를 사용하는데 async를 사용하고 싶은 경우
const getDog = async()=>{
    await 프로미스 혹은 대기해야  대상;
}

와 같이 사용하자!

function sleep(ms){
    return new Promise(resolve=>setTimeout(resolve,ms));
}
//화살표 함수를 사용하는데 async를 사용하고 싶은 경우
const getDog = async()=>{
    await sleep(1000);
    return '멍멍이';
}
const getRabbit = async()=>{
    await sleep(500);
    return '토끼';
}

const getTurtle = async()=>{
    await sleep(3000);
    return '거북이';
}

async function process(){
    const dog = await getDog();
    console.log(dog);
    const rabbit = await getRabbit();
    console.log(rabbit);
    const turtle = await getTurtle();
    console.log(turtle);
}

process();
  • 위의 경우는 멍멍이부터 토끼, 거북이 프로미스가 순차적으로 실행되는데,

여러 개의 프로미스를 동시에 실행시키려면 Promise all 함수를 사용하면 된다!

  1. 배열에 Promise들을 다 담아준다
  2. 1의 앞에 Promise.all을 붙이고, 내부에 배열을 담는다
  3. 2의 앞에 await을 붙여줌으로써 모든 Promise가 끝나야 Promise.all이 끝나고, 그 결과로 모든 값들이 들어간 배열을 반환한다!
async function process(){

    const results = await Promise.all([getDog(), getRabbit(), getTurtle()]);
    console.log(results);
}

process();//["멍멍이", "토끼", "거북이"]
  • 위의 경우는 실제로 소요된 시간은 3초!(그 이유는 가장 오래 걸리는 거북이가 3초 걸리기 때문!)
  • 위의 프로미스 중 단 하나라도 에러가 발생하면 전체가 코드를 수행할 수가 없게 됨(전체에 대해서 에러가 발생한 것처럼 작동)

그래서 아래와 같이 예외처리를 해주어야 함

async function process(){
    //1,배열에 담아준 후 Promise.all()에 넣어주기
    //2.그러고 나서 Promise.all 앞에 await 붙여주기
    //Promise.all은 내부의 Promise들이 모두 끝나면 끝나게 됨
    //다 끝나면 배열 내부의 값 각각이 반환됨!
    try{
        const results = await Promise.all([getDog(), getRabbit(), getTurtle()]);
    }catch(e){
        console.log(e);
    }
    console.log(results);
}

Promise.all 매개변수로 전달된 각 프로미스에 대해서 변수에 저장하려면

고전적인 방법으로 접근한다면

async function process(){

    const results = await Promise.all([getDog(), getRabbit(), getTurtle()]);
    const dog = results[0];
		const rabbit= results[1];
	  const turtle= results[2];
}

비구조화 할당을 이용하면

async function process(){

    const [dog, rabbit,turtle] =
		 await Promise.all([getDog(), getRabbit(), getTurtle()]);
}

로 사용할 수 있다!

Promise.race는 가장 먼저 끝나는 프로미스를 변수로 저장할 수 있게 한다!

사용방법은 Promise.all과 동일하다!

async function process(){
    
    const first = await Promise.race([getDog(), getRabbit(), getTurtle()]);
    console.log(first);
    
}

process();//토끼-가장 먼저 끝난 것이 결과물이 됨

race는 all과 다르게 가장 빨리 끝난게 에러일 때만 에러로 간주하고, 그 외에는 에러로 간주하지 않음

Updated: