글 작성자: bbangson
반응형

비동기 처리의 이해

 

- 동기적 (Synchronous)

작업이 끝날 때까지 기다린 후 다음 작업을 처리합니다.

다음 작업은 이전 작업이 끝날 때까지 멈추고 기다려야 합니다. 

 

- 비동기적 (Asynchronous)

작업의 흐름이 멈추는 일 없이 동시에 모든 작업을 처리할 수 있습니다.

기다리는 과정에서도 다른 함수를 호출할 수 있습니다.

 

간단하게 사진으로 비교해 보겠습니다. (벨로 퍼트님 강의에서 캡처했습니다.)

 

동기, 비동기 비교 사진

 

동기적인 코드 예시입니다.

function work() {
    
    const start = Date.now();
    
    for(let i=0; i<1000000000; i++) {
        
    }
    const end = Date.now();
    
    console.log(end - start + 'ms');
}

work();
console.log('다음 작업!');

// 521ms , 다음 작업! 이 순서대로 출력된다. 

위 코드는 동기적인 순서로 코드가 진행됩니다. 

 

work() 함수를 호출하여 모든 작업(반복문)을 마치고 '521ms'를 출력합니다. 

work() 함수를 끝마치고 나서 바로 밑에 있는 코드 console.log('다음 작업!')를 통해 

'다음 작업!'을 출력합니다. 

 

 

비동기적인 코드 예시입니다.

function work() {
    setTimeout( () => {
        
       const start = Date.now();
    
    	for(let i=0; i<1000000000; i++) {
        
    	}
    	const end = Date.now();
    
    	console.log(end - start + 'ms');
    }, 0) //0이어도 웹 브라우저 상에선 4ms로 처리됨.
}

console.log('작업 시작!');
work();
console.log('다음 작업!');

// 작업 시작!, 다음 작업!, 526ms로 출력됨.

위 코드는 비동기적으로 코드가 진행됩니다.

 

setTimeout( () => {}, 0) 함수를 통해 비동기적으로 코드를 구현할 수 있습니다. 

setTimeout 함수 안에 들어가는 숫자는 ms단위의 숫자로 ~ms 뒤에 함수를 실행하겠다는 의미를 담고 있습니다. 

0으로 지정해도 웹 브라우저 상에서는 4ms가 최소 값이기 때문에 4ms이후에 함수가 호출됩니다. 

 

먼저 '작업 시작!'을 출력하고 work() 함수를 호출합니다. 

work() 함수는 4ms 이후에 반복문을 실행합니다. 그 사이에 '다음 작업!'이 출력되고, 반복문이 다 실행되면 

setTimeout 함수 내부에 있는 출력문( 526ms 출력.)이 실행됩니다.

 

이처럼 work() 함수가 끝날 때까지 기다린 후, 다음 코드를 진행하는 것이 아니라 동시 다발적으로 코드를 진행하는 것을 '비동기'라고 합니다.  

 

 

비동기 내에서도 Callback 함수를 이용하여 동기적으로 작동하게끔 할 수 있습니다. 

 

Callback 함수는 함수 타입의 값을 인자로 넘겨주고 파라미터로 받은 함수를 특정 작업이 끝난 후 호출해주는 함수입니다. 

 

Callback 함수 사용 예시입니다.

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) //0이어도 웹 브라우저 상에선 4ms로 처리됨.
}

console.log('작업 시작!');
work((ms) => {
	console.log('작업이 끝났어요!');
    console.log(ms + 'ms 걸렸다고 해요!');
});
console.log('다음 작업!');


// 작업 시작!, 다음 작업!, 518ms, 작업이 끝났어요!, 518ms 걸렸다고 해요! 순으로 출력.

간단하게 동작 순서를 설명하자면, work() 함수를 실행할 때 work 함수의 인자로 화살표 함수를 넣어줍니다. 

work() 함수의 파라미터인 callback에 화살표 함수 자체를 넣어준 것입니다. 

그리고 callback 함수의 인자로 'end - start' 값을 넣어줍니다.

 

이 값이 callback 함수(화살표 함수)의 파라미터인 ms가 되고 이 값을 가지고 화살표 함수를 실행합니다. 

 

비동기 요청으로 주로 사용하는 작업은 대표적으로 4가지가 있습니다. 

 

1. Ajax Web API 요청 

- 서버 쪽에서 데이터를 받아와야 될 때, 요청을 하고 서버에서 응답을 할 때까지 대기를 해야 되기 때문에 비동기적으로 사용해야 합니다. 

 

2. 파일 읽기 

 

3. 암호화 / 복호화

 

4. 작업 예약

 

 

Promise

 

Promise 란? 

비동기 작업을 조금 더 편하게 처리할 수 있도록 도입된 ES6 기능입니다. 

 

이전에는 비동기 작업을 처리할 때는 callback 함수를 통해 했지만 비동기 작업이 많아질 경우에는 코드가 난잡해지는 경우가 있었습니다. 

 

이를 예방하기 위해 Promise가 나왔습니다. 

 

만약 Promise를 사용하지 않고 callback으로만 비동기 작업을 처리할 경우에는 코드가 어떤 식으로 보일까요? 예시를 보겠습니다. 

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

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

// 1, 2, 3, 4, 5, 작업 끝! 순서대로 출력된다.

위의 코드에서 increasAndPrint() 함수는 비동기적으로 동작하는 함수로, 들어온 파라미터 값에 +1을 하고 callback함수가 있다면 callback 함수를 다시 호출하는 함수입니다. 

 

참고로 1000ms = 1초입니다. 

 

밑에 increaseAndPrint를 반복적으로 호출하는 것은 5까지 더하는 callback 함수 중복 문입니다.

이런 코드를 callback 지옥이라고 불립니다. 정말 보기 좋지 않은 코드입니다. 

 

Promise를 사용하여 callback 지옥에서 벗어나 보겠습니다. 

먼저 Promise를 만드는 법부터 보여주는 예시 코드입니다. 

const myPromise = new Promise((resolve, reject) => {
    // 성공 코드
    setTimeout( () => {
        resolve('result');
    }, 1000)
    
    // 실패 코드 
    setTimeout( () => {
        reject(new Error());
    }, 1000);
});

myPromise.then(result => {
    console.log(result); // 성공시 resolve를 호출하고 'result'를 출력한다.
}).catch(e => {
    console.error(e); // Error 출력.
})

Promise는 성공할 경우에 resolve를 호출하고 실패할 경우에는 reject를 호출합니다. 

 

위의 코드에서 myPromise 안에 성공 코드와 실패 코드를 편의상 한 번에 넣어놨습니다. 

성공했을 경우 resolve 함수를 호출하고 resolve 함수의 인자로 'result'를 넣어줍니다. 

 

그다음 .then() 안에 있는 화살표 함수의 result의 파라미터로 'result'가 들어가고 'result'를 출력합니다. 

만약 실패했을 경우에는 reject 함수를 호출하고 Error를 출력합니다. 

 

 

Promise 예제 코드입니다.

function increaseAndPrint(n) {
    return new Promise((resolve, reject) => {
        setTimeout( () => {
            const value = n+1;
            if(value === 5) { // value가 5가 되면 reject를 호출한다.
                const error = new Error();
                error.name = 'ValueIsFiveError'; // 에러 이름을 정해줄 수 있다.
                reject(error);
                return;
            }
            console.log(value);
            resolve(value);
        }, 1000)
    })
}

increaseAndPrint(0).then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.catch(e => {
    console.log(e); // 'ValueIsFiveError' 출력.
})

위의 callback으로만 만든 코드와 유사합니다. 

차이점은 value값이 5가 되면 reject를 호출하고 그전까지는 resolve를 호출합니다. 

 

콘솔 창에는 1 , 2, 3, 4가 순서대로 출력되고 그다음에 'ValueIsFiveError'가 출력될 것입니다. 

 

반환 값이 그대로 동일하게 그다음 함수의 파라미터로 들어가기 때문에 이런 식으로 코드를 작성하는 것도 가능하기 때문에 increaseAndPrint를 중복적으로 호출하는 코드에 파라미터와 리턴 코드가 존재하지 않습니다.

 

하지만 사실, Promise를 사용하는 것도 불편한 점이 있습니다. 에러를 잡을 때 어느 부분에서 에러가 발생했는지 파악하기가 어렵고 분기를 나누는 작업에서도 불편합니다. 

 

또 이를 해결하기 위해 나온 것이 async, await입니다. 

 

 

 

async, await

 

async, await란?

자바스크립트에서 비동기 처리를 하게 될 때, Promise를 더욱 쉽게 사용할 수 있게 해주는 문법입니다.

이 문법은 ES8에서 소개됐습니다. 

 

먼저 예시를 통해 async, await의 사용법을 알아보겠습니다.

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

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

process().then(value => {
    console.log(value); // true 출력.
})

// '안녕하세요!' 출력하고 1초 뒤에 '반갑습니다!' 출력한다. 

async, await 문법을 사용할 때는 함수 앞에 async를 붙여주고 promise( sleep(1000) ) 앞에 await를 붙여주면 됩니다. 

 

async, await를 사용하면 분기점을 활용하는 것도 쉽고 값을 공유하는 데에도 용이한 장점이 있습니다.

 

async, await 함수의 결과물(return)은 promise를 반환합니다. 

그래서 process() 함수를 호출하고. then을 활용하여 반환된 함숫값을 파라미터로 사용할 수 있습니다. 

 

 

Error발생 시, 처리하는 예시를 보겠습니다. 

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function makeError() {
	await sleep(1000);
    const error = new Error();
    throw error;
}

async function process() {
    try {
        await makeError();
    } catch(e) { // e가 makeError() 함수에 있는 throw가 반환한 객체를 뜻한다.
        console.error(e); // Error 출력.
    }
}

process();

async, await에서 에러를 처리할 때는 try, catch문을 사용하면 됩니다. 

catch문의 파라미터인 (e)가 makeError() 함수의 throw 반환 값입니다. 

 

 

 

Promise.all, Promise.race

 

Promise.all이란?

여러 개의 promise들을 동시에 처리해야 될 때 유용하게 사용할 수 있는 문법입니다.

 

Promise.race란?

여러 개의 promise들 중 가장 빨리 반환하는 promise만 나타내고 싶을 때 사용할 수 있는 문법입니다.

 

 

만약 Promise.all을 사용하지 않고 코드를 작성한다면 어떤 식으로 작성하게 될까요?

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

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();
// '멍멍이', '토끼', '거북이' 순으로 출력된다.

위의 process() 함수는 한 번에 동시 다발적으로 코드를 실행시키지 않습니다. 

가장 빠르게 실행되는 rabbit 변수도 getDog() 함수를 호출한 다음 실행이 됩니다. 

 

그래서 위의 process()가 처리되는 총시간은 4.5초입니다.

 

동시 다발적으로 작업을 처리하고 싶으면 어떻게 하면 될까요? 

Promise.all 키워드를 사용하면 됩니다.  

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const getDog = async () => {
    await sleep(1000);
    return '멍멍이';
}

const getRabbit = async () => {
    await sleep(500);
    return '토끼';
}

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

async function process() {
    try {
	const results = await Promise.all([getDog(), getRabbit(), getTurtle()]);       
    	console.log(results); // ['멍멍이', '토끼', '거북이'] 출력.
    } catch (e) {
        console.log(e);
    }

    /*
    구조 분해 문법으로 아래와 같이도 할 수있다.
    const [dog, rabbit, turtle] = await Promise.all([getDog(), getRabbit(), getTurtle()]);
    */
}

process();

실행하고자 하는 promise들을 Promise.all 키워드 안에 배열 형태로 작성해줍니다. 

 

Promise.all은 모든 promise들을 동시에 실행하고, 모든 실행이 다 끝나면 각각의 반환 값이 배열 형태로 변수(result)에 저장됩니다. 

 

getTurtle()이 3초가 걸리기 때문에 실제가 걸린 시간은 3초입니다. 위에서 봤던 4.5초랑은 차이가 있습니다. 

 

Promise.all은 배열 안에 promise 중 하나라도 에러가 발생하면 전체 promise에 에러가 발생한 것으로 간주합니다. 

 

 

Promise.race

Promise.race는 Promise.all과는 사용법이 비슷하지만 역할은 완전히 다릅니다.

Promise.all은 배열 안의 promise들을 각각 전부 반환하는 반면, Promise.race는 배열 내에 가장 빨리 반환하는 값만 반환합니다. 

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const getDog = async () => {
    await sleep(1000);
    return '멍멍이';
}

const getRabbit = async () => {
    await sleep(500);
    return '토끼';
}

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

async function process() {
    try{
       const first = await Promise.race([getDog(), getRabbit(), getTurtle()]);
       console.log(first); // '토끼' 출력.
    } catch (e) {
        console.log(e);
    }
}

process();

Promise.race는 가장 빠르게 반환되는 값에 대하여 에러가 발생했을 경우에만 에러를 처리합니다. 

getRabbit()이 성공적으로 가장 빠르게 끝났는데, 그 뒤에 getDog()가 실패하면 이 경우는 에러가 발생했다고 간주하지 않습니다.

반응형