1. 비동기/동기

  • blocking (블로킹) : 이전 작업이 끝날 때까지 기다렸다가 다음 작업을 수행
  • non-blocking (논블로킹) : 이전 작업이 완료될 때까지 대기 하지 않고 다른 작업을 동시에 수행할 수 있는 것

노드에서는 동기와 블로킹이 유사하고 비동기와 논 블로킹이 유사하다.

✔️ Node.js에서 비동기 처리

Node.js에서는 대부분의 메서드들이 비동기 방식으로 처리한다.

하지만 Javascript가 기본적으로 단일 스레드 (Single thread)이기 때문에 한 번에 한 작업만 수행한다.

What is Single Thread ?
: 프로세스 내 하나의 쓰레드가 하나의 요청만을 수행하는 것을 말한다.
즉, 들어온 요청이 돌아가고 있을 때 다른 요청을 함께 수행할 수 없다 !

Node.js에서는 싱글 스레드 논블로킹 모델을 적용해서 사용.

따라서 하나의 Thread이지만 이 Thread를 이용해 비동기 처리를 하지 않고,
논블로킹 I/O 작업을 통해 동시에 들어온 많은 요청을 비동기적으로 처리할 수 있다.

논블로킹 I/O 는 Event-driven (이벤트 기반) 으로 동작 가능하다.

Event-driven는 또 뭐여 ?
: 이벤트가 발생할 때 미리 지정해둔 작업을 수행하는 방식을 의미한다.

이를 도와주는 기능이 총 3가지가 있다 !

1) Callback Function

어떤 이벤트 발생 시, 특정 시간이 지난 뒤 시스템에서 호출하는 함수이고 다른 함수의 인자로 사용한다.

//* Callback Function

console.log("Ready ...");

setTimeout((): void => { 
    console.log("Set ..."); //3초 뒤에 출력
}, 3000);

console.log("Go !");

//* 출력
//Ready ...
//Go !
//Set ...

이처럼 콜백 함수를 통해 비동기 처리를 할 수 있다.
하지만 ! 이런 callback function을 이용한 비동기 처리는, 콜백 지옥을 만들어낸당 ㅎㅎㅎ..

이를 해결하기 위해 ES2015부터는 Promise 를 사용한다.

2.1) Promise

Promise에는 총 3단계의 상태가 존재한다.

  • Pending (대기) - 비동기 처리가 완료되지 않은 상태
  • Fullfiled (이행) - 비동기 처리가 완료되어 Promise 결과를 반환
  • Rejected (실패) - 비동기 처리 도중 실패했거나 오류가 발생함
const condition : boolean = false; // true면 resolve, false면 reject

//* 최초 생성 시점
const promise = new Promise((resolve, reject) => {
    if (condition) {
        resolve("우와 Promise다 !");
    } else {
        reject(new Error("비동기 처리 도중 실패!"));
    }
});

/*
다른 코드 들어갈 수 있다
!
!
!
*/

//* 비동기 처리 성공(then), 비동기 처리 실패(catch)
//resolve와 reject에 넣어준 인수는 각각 then과 catch의 매개변수에서 받을 수 있다.
promise
.then((resolveData): void => console.log(resolveData))
.catch((error): void => console.log(error)); //현재 condition 의 값이 false 이므로, error("비동기 처리 도중 실패!")를 출력

new Promise로 프로미스를 생성할 수 있으며, 그 내부에 resolve와 reject를 매개변수로 갖는 콜백 함수를 넣는다.

  • 프로미스 내부에서 resolve가 호출되면 -> then이 실행 / reject가 호출되면 catch가 실행

  • finally 부분은 성공/실패 여부와 상관없이 실행

resolve와 reject에 넣어준 인수는 각각 then과 catch의 매개변수에서 받을 수 있다.

  • resolve('성공')이 호출되면 then의 message가 '성공'이 됨.

  • reject('실패')가 호출되면 catch의 error가 '실패'가 됨.

위 코드에서 condition 변수를 true로 바꿔보면 catch에서 message가 로깅된다.

즉, 프로미스를 쉽게 설명하자면, 실행은 바로 하되 결과값은 나중에 받는 객체이다.
결과값은 실행이 완료된 후 then이나 catch 메서드를 통해 받는다.

2.2) Promise Chaining

여러 개의 promise 를 연결해서 사용할 수 있다.

앞서 확인했던 <Promise>.then()<Promise>.catch()를 이용하면 된다.

아침에 일어나서 어렵게.. 어렵게.. 양치를 하는 나를 Promise Chaining을 이용해 만들어보자

//* 아침에 어렵게,, 어렵게,, 일어나는 나를 표현한 함수
const me = (callback: () => void, time: number) => { 
    setTimeout(callback, time);
};

//* 기상
const wakeUp = (): Promise<string> => { //Promise 객체를 반환하는 함수
    return new Promise((resolve, reject) => {
        me(() => {
            console.log("[현재] 일어남");
            resolve("일어남"); 
        }, 1000);
    });
};

//* 화장실 감
const goBathRoom = (now: string): Promise<string> => {
    return new Promise((resolve, reject) => {
        me(() => {
            console.log("[현재] 화장실로 이동함");
            resolve(`${now} -> 화장실로 이동함`);
        }, 1000);
    });
};

//* 칫솔과 치약을 준비함
const ready = (now: string) : Promise<string> => {
    return new Promise((resolve, reject) => {
        me(() => {
            console.log("[현재] 칫솔과 치약을 준비함");
            resolve(`${now} -> 칫솔과 치약을 준비함`)
        }, 1000);
    });
};

//* 양치함
const startChikaChika = (now: string) : Promise<string> => {
    return new Promise((resolve, reject) => {
        me(() => {
            console.log("[현재] 양치함");
            resolve(`${now} -> 양치함`)
        }, 1000);
    });
};

//* 나 자신한테 칭찬함
const goodjob = (now: string) : Promise<string> => {
    return new Promise((resolve, reject) => {
        me(() => {
            console.log("[현재] 나 자신에게 칭찬중");
            resolve(`${now} -> 칭찬중`)
        }, 1000);
    });
};

wakeUp() //resolve가 chaining 되어서 화살표가 이어져서 출력된다. (now 값에 문자열들이 추가되고, 추가되고,,,)
    .then((now) => goBathRoom(now))
    .then((now) => ready(now))
    .then((now) => startChikaChika(now))
    .then((now) => goodjob(now))
    .then((now) => console.log(`\n${now}`)); //출력값: 일어남 -> 화장실로 이동함 -> 칫솔과 치약을 준비함 -> 양치함 -> 칭찬중

여기서 주목할 값은 Promise의 resolve가 넘겨주는 인수 now 값이다.
이 now 값은 결국 Promise.then() 함수가 불릴 때 사용되는데, 여기서 chaining 이 이뤄지는 것을 볼 수 있다 !

이해를 쉽게 하기 위해 그림을 그려서 나타내보았다.

resolve 말고도 reject에도 체이닝이 그럼 가능할까 ?..
다음 코드를 돌려보자 !

Promise.resolve(true)
    .then((response) => {
        throw new Error("비동기 처리 중 에러 발생!");
    })
    .then((response) => {
        console.log(response);
        return Promise.resolve(true);
    })
    .catch((error) => {
        console.log(error.message); //출력값: "비동기 처리 중 에러 발생!"
    });

여러개의 프로미스 체인 중 하나라도 reject되면 바로 마지막에 달린 catch()로 내려가서 에러를 처리한다. 불필요하게 나머지 프로미스까지 차례차례 확인하지 않는다.

3) async - await

async는 ES2017부터 제공되고 알아두면 엄청나게 편리한 기능이다.

Promise가 콜백 지옥을 해결했다고 하지만 then과 catch가 계속 반복되기 때문에 여전히 코드가 장황하다. 이를 async/await 문법으로 프로미스를 사용하여 깔끔하게 줄일 수 있다.

  • async : 암묵적으로 Promise를 반환한다.
  • await : resolve, reject 같은 Promise 객체를 기다린다. 이 때, async가 정의된 내부에서만 사용 가능하다.

asyncawait를 이용하여 함수를 선언하고 표현하는 방법은 다음과 같다

//* async - await

//함수 선언식
async function foo1() {
  
}

//함수 표현식
const foo2 = async () => {

}

다음 코드로 왜 async/await를 알고 있어야 하는지 살펴보자 !

//* 이전에 치카치카 코드와 비슷한 Promise를 이용한 비동기 처리 코드
// 보기에 복잡해보이는 코드,,

let asyncFunc1 = (something: string): Promise<string> => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(`resolved ${something} from func1 ...`);
        }, 1000);
    });
};

let asyncFunc2 = (something: string): Promise<string> => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(`resolved ${something} from func2 ...`);
        }, 1500);
    });
};

const promiseMain = (): void => {
    asyncFunc1("test")
        .then((resolveData: string) => {
            console.log(resolveData);
            return asyncFunc2("testttt")
        })
        .then((resolveData: string) => {
            console.log(resolveData);
        });
};

promiseMain(); 
//resolved test from func1 ...
//resolved testttt from func2 ...

promiseMain 함수를 보자. 정확히 어떤 데이터가 어떤 시점에서 출력되는지 이해하기 어렵다..

이 promiseMain 함수를 async/await로 바꾼다면 ?!?!??!

const main = async (): Promise<void> => {
    let result = await asyncFunc1("wow!");
    console.log(result);
    result = await asyncFunc2("holy moly");
    console.log(result);
};

main();
//resolved wow! from func1 ...
//resolved holy moly from func2 ...

위와 같이 더 직관적이고 깔끔하게 쓸 수 있다.

아래 코드와 같이 for문과 async/await문을 같이 써서 프로미스를 순차적으로 실행할 수 있다. for문과 함께 쓰는 것은 노드 10 버전부터 지원하는 ES2018 문법이다.

const promise1 = Promise.resolve('성공1'); 
const promise2 = Promise.resolve('성공2');
(async () => {
  for await (promise of [promise1, promise2]) {
    console.log(promise);
  }
})();

for await of 문을 이용해서 프로미스 배열을 순회하는 코드이다. async 함수의 반환값은 항상 Promise로 감싸진다.

따라서, 실행 후 then을 붙이거나 또 다른 async 함수 안에서 await을 붙여 처리할 수 있다.

profile
코딩 해라 스리스리 예스리 얍!

0개의 댓글