31기 IN SOPT 서버파트 2차 세미나 (1)


콜백 지옥

여러 수준의 콜백이 중첩되어 지저분하게 보이는 것으로, 순차적으로 실행되는 비동기 작업이 여러 개 있는 경우 발생한다. 이러한 경우, 가독성이 떨어지고, 연쇄적으로 실행되는 작업 중 일부를 수정해야할 경우, 수정 작업이 굉장히 어려워진다는 단점이 존재한다. 따라서, 비동기 처리에는 주로 promise, async를 사용한다.

Promise

콜백함수를 사용하지 않고 비동기 처리를 하기 위해 JS에서 제공하는 object로, 성공과 오류에 대한 응답 처리로 이루어져 있다.

Promise의 상태

  1. 대기(pending) : 초기 상태
  2. 이행(fullfilled) : 비동기 처리 완료
  3. 거부(rejected) : 비동기 처리 실패

promise는 아래와 같이 resolve(성공상태)와 reject(실패 상태)의 인수를 전달하는 실행 함수를 매개변수로 받는다.

이 때 arraw function을 사용한다면 기본적인 구조는 아래와 같다

const promise = new Promise((resolve, reject) =>{});

then, catch, finally

.then : 비동기 처리가 성공할 경우, resolve 내의 값을 받아온다

.catch : 비동기 처리가 실패하였을 때, reject 내의 값을 받아온다

.finally : 비동기 처리의 성공 여부와 관계 없이 실행하고자 하는 기능이 있다면 .finally에 넣는다

const promise = new Promise((resolve, reject) => {
  if (condition) {
    resolve("비동기 처리 성공");
  } else {
    reject("비동기 처리 실패");
  }
});

//* 비동기 처리 성공(then), 비동기 처리 실패(catch)
promise
    .then((resolvedData): void => console.log(resolvedData))
    .catch((error): void => console.log(error));
    .finally(() => {console.log("finally")})

아래 사진은 위 코드에서 condition이 true, false로 선언되어 있는 경우 각각의 출력이다. true일 때에는 resolve 내의 값이, false일 때는 reject 내의 값이 출력되나, 그 여부와 관계 없이 finally가 실행되는 것을 볼 수 있다.

그런데 ! error 반환을 위와 같이 하는 건 좋지 않음

error을 반환할 때 console.log(error) 와 같이 단순한 문자열을 출력하는 것으로 처리하는 것은 옳지 않다. 보통 Error 객체와 throw 문법을 사용하기에, 이를 활용하여 위의 코드를 간단히 수정해보면 아래와 같다. (Error 객체를 커스텀하거나, catch문 내에서 logger을 사용하는 경우도 많다. 그리고 상황에 따라 다양한 error 타입이 존재하나 이번 포스팅에서는 생략 ,,,🫥)

promise
.then((resolvedData): void => console.log(resolvedData))
.catch((error): void => {throw new Error(error)})
.finally(() => {console.log("finally")});

promise chaining

promise를 사용할 때에는 then, catch, finally를 이용하여 값을 받아오는데, 이를 활용하여 비동기 처리되는 것들을 모아서 처리한다면, 이를 promise chaining 이라고 하며, 아래와 같은 형태를 지닌다.

promise
.then()
.then()
.then()

위와 같이 여러 개의 promise가 연결되어 있을 때, 어디에서 발생할 지 모르는 에러를 해결하기 위해 모든 .then 다음에 .catch를 적어줄 필요는 없다. catch는 한 번의 사용으로 연결된 모든 promise에 대한 reject를 해결할 수 있기 때문이다.

promise
.then()
.then()
.then()
.catch() // 이렇게 한 번만 써줘도 된다 !!

async & await

async : function 앞에 위치하며 async 가 붙어있는 함수는 항상 promise를 반환한다.

await : async 함수 내에서만 사용할 수 있으며, 이 키워드는 promise가 처리될 때까지 기다리도록 한다.

이 때 아래의 주석처리된 코드와 같은 형태로는 await을 사용할 수 없다 ! await을 사용하고자 한다면 주석 아래의 코드와 같이 직속 스코프의 함수에 async가 선언되어 있어야 한다.

// const temp = async (): Promise<string> => {
//     return new Promise((resolve, reject) => {
//         Me(() => {
//             const now = await WakeUp();
//             console.log('temp');
//             resolve(`${now} -> temp 완료`);
//         }, 3000)
//     });
// };

const temp = (): Promise<string> => {
    return new Promise((resolve, reject) => {
        Me(async () => {
            const now = await WakeUp(); // wakeup이 이행될 때까지 기다림
            console.log('temp');
            resolve(`${now} -> temp 완료`);
        }, 3000)
    });
};

위 코드에서 const now await WakeUp() 이라는 코드는 WakeUp이 처리된 후 실행이 재개되는데, 그 기다리는 시간동안 다른 스크립트를 처리할 수 있기 때문에 리소스 낭비가 최소화된다.

promise chaining 코드 바꿔보기

async await을 비동기 처리에 쓴다는 것은 알고 있었는데 세미나 때 휘리릭 지나가서 뭔가 ,, promise chaining 으로 짜둔 코드를 async await을 사용한 코드로 바꿔보고 싶었다. 그래서 아래의 코드 탄생 ! 결과는 제대로 출력되나 이게 맞는 건 지 모르겠다 ^_^

// 동기 코드와 비슷하게 생김 / 암묵적으로 promise를 반환
// await : resolve, reject 와 같은 promise 객체를 기다림 (async 내부에서만 사용 가능)

// 첫 인자 : callback 함수, 두번째 인자 time : 하나의 행동을 하기까지 걸리는 시간
const Me = (callback: () => void, time: number) => {
    setTimeout(callback, time);
};
  
//* 기상
const WakeUp = (): Promise<string> => {
    // 바로 promise 객체를 return
    // 성공 : resolve, 실패 : reject
    return new Promise((resolve, reject) => {
        Me(() => {
         console.log('[현재] 기상!');
         resolve('기상 성공');
       }, 1000);
    });
};

//* 화장실
const GoBathRoom = (): Promise<string> => {
    return new Promise((resolve, reject) => {
        Me(async () => {
            const now = await WakeUp();
            console.log('[현재] 화장실 도착');
            resolve(`${now} -> 화장실 도착 완료`);
        }, 3000)
    });
};

//* 양치 준비
const Ready = (): Promise<string> => {
    return new Promise((resolve, reject) => {
        Me(async () => {
            const now = await GoBathRoom();
            console.log('[현재] 칫솔 / 치약 준비');
            resolve(`${now} -> 칫솔 / 치약 준비 완료`);
        }, 1000)
    });
};

//* 양치
const Bruth_teeth = (): Promise<string> => {
    return new Promise((resolve, reject) => {
        Me(async () => {
            const now = await Ready();
            console.log('[현재] 양치 중');
            resolve(`${now} -> 양치 완료`);
        }, 1000)
    });
};

//* 완료
const Done = (): Promise<string> => {
    return new Promise((resolve, reject) => {
        Me(async () => {
            const now = await Bruth_teeth();
            console.log('[현재] 모두 완료함');
            resolve(`${now} -> 준비 완료 !`);
        }, 1000)
    });
};

Done()
   .then((now) => {
        console.log(`\n${now}`);
    })

마지막 호출 코드가 굉장히 깔끔해지긴 했으나 동작할 때에 Done -> prepare -> Ready -> GoBathRoom -> WakeUp 이 진행된 후 now를 넘겨주면서 다시 역순으로 실행 -> console.log() 가 되는 듯 하다. 그래서, 시간마다 각각의 출력문이 순서대로 출력되는 것이 아닌, 모든 time을 합친 7초가 지나면 모든 결과가 아래 사진과 같이 한 번에 짜잔 하고 나타난다. 이걸 promise chaining 에서처럼 순서대로 하나하나 출력되게 하려면 어떻게 해야할까 ,,