[JS] Promise와 async (with Promise 객체 만들어보기)

uuranus·2025년 2월 10일
post-thumbnail

동기 vs 비동기

동기(synchronous)

코드 실행의 종료를 기다리는 제어권이 있음

  • 보통 프로그래밍은 이전 코드가 다 진행된 후 다음 코드가 실행된다. (순차적)

비동기 (asynchronous)

코드 실행의 종료를 관여하지 않음

  • 비동기 코드를 실행한 후 해당 코드의 종료 여부에 상관없이 다음 코드로 넘어감 (병렬적 느낌)

block vs non-block

  • block은 다른 작업을 호출한 후 다른 작업을 진행하지 못하는지의 여부이다.

Promise

비동기 작업을 동기처럼 체이닝할 수 있게 해주는 객체

  • 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있음
  • 당장 리턴은 아니고 나중에 비동기처리가 끝난 후에 결과값을 제공하겠다는 Promise 객체를 반환
  • promise.then과 같이 체이닝 방식으로 비동기 호출 이후의 과정을 동기적 프로그래밍으로 구현할 수 있어 가독성이 높아짐
    • 콜백 지옥을 피할 수 있게함
  • 동기적으로 프로그래밍할 수 있게 해주는 것일 뿐 promise 내 로직이 끝난 후 promise 이후 코드가 실행되는 것은 아니다
const myFirstPromise = new Promise((resolve, reject) => {
  // do something asynchronous which eventually calls either:
  //
  //   resolve(someValue)        // fulfilled
  // or
  //   reject("failure reason")  // rejected
});

state

  • pending
    • promise는 생성되었으나 아직 비동기 로직이 끝나지 않음
  • fulfill
    • 비동기 로직이 끝났고 성공적으로 완료됨. resolve를 호출
  • reject
    • 비동기 로직이 끝났고 실패하였거나 오류가 발생. rejected를 호출

method

all

여러 promise를 받아서 처리하는 함수

  • 전달받은 promise들이 전부 resolve로 성공해야만 실행된다.
Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 프라미스 전체가 처리되면 1, 2, 3이 반환됩니다. 각 프라미스는 배열을 구성하는 요소가 됩니다.

then

여러 개의 promise를 체이닝해주는 함수

  • then은 Promise를 반환하여 체이닝 방식으로 비동기 처리 과정을 연결할 수 있다.
  • then 자체는 콜백을 등록해주는 동기함수로 처음에 Promise가 생성될 때 call stack에 들어갔다가 사라진다.
p.then(onFulfilled, onRejected);

p.then(function(value) {
  // 이행
}, function(reason) {
  // 거부
});

catch

에러를 처리해주는 함수

  • promise에서 reject가 호출되거나 throw로 에러를 날린 경우를 처리해주는 함수
  • promise를 리턴하기 때문에 체이닝이 가능하다.
  • resolve된 경우는 호출되지 않는다.
p1.then((value) => {
  console.log(value); // "Success!"
  return Promise.reject("oh, no!");
})
  .catch((e) => {
    console.error(e); // "oh, no!"
  })
  .then(
    () => console.log("after a catch the chain is restored"), // "after a catch the chain is restored"
    () => console.log("Not fired due to the catch"),
  );

fetch

promise 패턴을 이용해 네트워크 요청 비동기 처리를 동기적으로 구현할 수 있도록 해주는 함수

fetch('http://some-site.com/cors-enabled/some.json', {mode: 'cors'})  
  .then(function(response) {  
    return response.text();  
  })  
  .then(function(text) {  
    console.log('Request successful', text);  
  })  
  .catch(function(error) {  
    log('Request failed', error)  
  });
  • fetch 내부적으로 비동기 함수인 네트워크 요청이 되고 응답이 오면 순차적으로 then의 콜백함수들이 실행된다.

aysnc 와 await

  • await를 호출하면 해당 작업이 끝날 때까지 async 함수를 blocking
  • Promise의 chaining 없이 키워드 하나로 진짜 동기 프로그래밍처럼 작성 가능
  • but, 비동기 이후 코드 중 비동기를 기다리지 않아도 되는 코드가 있는 경우는 await를 통해서 기다리게 될 수도 있으니까 잘 생각하고 써야 함

Promise 만들어보기

const PROMISE_STATES = {
    PENDING: "PENDING",
    FULFILLED: "FULFILLED",
    REJECTED: "REJECTED",
};

class MyPromise {
    state = PROMISE_STATES.PENDING;

    resolveCallback = null;

    rejectCallback = null;

    constructor(executor) {
        this.state = PROMISE_STATES.PENDING;
        try {
            executor(this.resolve.bind(this), this.reject.bind(this));
        } catch (error) {
            this.reject(error);
        }
    }

    resolve(value) {
        if (this.state !== PROMISE_STATES.PENDING) return; // Prevent duplicate calls
        this.state = PROMISE_STATES.FULFILLED;
        this.value = value;

        if (this.resolveCallback) {
            this.resolveCallback(value); // Execute registered callback
        }
    }

    reject(error) {
        if (this.state !== PROMISE_STATES.PENDING) return; // Prevent duplicate calls
        this.state = PROMISE_STATES.REJECTED;
        this.value = error;

        if (this.rejectCallback) {
            this.rejectCallback(error); // Execute registered callback
        }
    }

    then(resolve, reject) {
        return new MyPromise((rs, rj) => {
            if (this.state === PROMISE_STATES.FULFILLED) {
                try {
                    const result = resolve ? resolve(this.value) : this.value;
                    rs(result);
                } catch (error) {
                    rj(error);
                }
            } else if (this.state === PROMISE_STATES.REJECTED) {
                try {
                    const result = reject ? reject(this.value) : this.value;
                    rs(result);
                } catch (error) {
                    rj(error);
                }
            } else {
                // Save callbacks if the state is PENDING
                this.resolveCallback = () => {
                    try {
                        const result = resolve(this.value);
                        rs(result);
                    } catch (error) {
                        rj(error);
                    }
                };

                this.rejectCallback = () => {
                    try {
                        const result = reject(this.value);
                        rj(result);
                    } catch (error) {
                        rj(error);
                    }
                };
            }
        });
    }

    catch(callback) {
        return this.then(null, callback);
    }
}
  • resolve, reject를 bind 시켜주는 이유
    • 그냥 this.resolve를 하면 MyPromise 내부에 있는 resolve를 가져오는데 이는 따로 클래스로부터 분리가 되는거라 나중에 resolve가 호출될 때 this가 사라지게 됨 (거슬러 올라가다 window까지 갈 것)
  • bind(this)로 this.resolve의 this를 MyPromise로 만들어주는 것
    • executor(this.resolve.bind(this), this.reject.bind(this))
    • 이렇게 하면 executor에 MyPromise.resolve,reject callback이 저장된다.

동작과정

const promise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve("Step 1"); // 첫 번째 단계에서 성공
        // reject("Error at Step 1"); // 이걸로 테스트하려면 주석 해제
    }, 1000);
});

promise
    .then((value) => {
        console.log("First then:", value); // Step 1 출력
        return "Step 2";
    })
    .then((value) => {
        console.log("Second then:", value); // Step 2 출력
        throw new Error("Error at Step 2"); // 오류 발생
    })
    .then((value) => {
        console.log("Third then:", value); // 이 코드는 실행되지 않음
        return "Step 3";
    })
    .catch((error) => {
        console.error("Caught an error:", error.message); // Error at Step 2 출력
    });
  1. promise생성
  2. then을 통해 pending상태라 resolveCallback을 추가한 MyPromise1생성
    1. promise1은 생성하면서 pending이므로 promise에다가 resolve, reject일 때 다음 콜백을 등록함
  3. then을 통해 pending상태라 resolveCallback을 추가한 MyPromise2생성
    1. promise2은 생성하면서 pending이므로 promise1에다가 resolve, reject일 때 다음 콜백을 등록함
  4. then을 통해 pending상태라 resolveCallback을 추가한 MyPromise3생성
    1. promise3은 생성하면서 pending이므로 promise2에다가 resolve, reject일 때 다음 콜백을 등록함
  5. catch를 통해 pending상태라 rejectCallback을 추가한 MyPromise4생성
    1. promise4은 생성하면서 pending이므로 promise3에다가 resolve, reject일 때 다음 콜백을 등록함
  6. setTimeout이 첫번째 promise의 resolve를 호출 (클로져)
  7. 상태값이 FULFILLED가 되면서 resolveCallback을 호출
  8. resolveCallback은 then에서 입력한 콜백을 호출하고 결과값을 promise2의 resolve를 호출함
  9. promise2의 resolve는 호출되면서 promise3이 등록한 resolveCallback을 등록함
  10. promise2의 resolveCallback이 실행되면서 promise3이 생성되면서 호출한 then의 resolve함수를 호출하고 생성자의 rs()를 호출함
  11. rs는 promise3의 resolve함수로 다시 promise4가 등록한 resolveCallback을 호출
  12. …반복
promise (PENDING) ── resolve("Step 1") ──> FULFILLED
                                │
                                ▼
                       promise2 (PENDING) ── resolve("Step 2") ──> FULFILLED
                                            │
                                            ▼
                                   promise3 (PENDING) ── throw Error ──> REJECTED
                                                        │
                                                        ▼
                                             promise4 (catch) 실행
profile
Frontend Developer

0개의 댓글