Promise 이해하기

정주·6일 전

자바스크립트

목록 보기
1/1
post-thumbnail

들어가며

관성적으로 api 연동할 때마다 사용했던 Promise 개념을 처음부터 끝까지 한 번에 정리해보고자 합니다.


Promise란?

  • 비동기 연산의 상태(state)와 결과(result)를 나타내는 객체
  • new 생성자를 통해 생성되며 생성과 동시에 실행
    //  new로 생성가능 -> 객체를 반환
    const promiseCallback = new Promise(()=>{
    // 인자로 콜백함수를 넣기 가능 -> 해당 콜백 함수 안에서 비동기 함수를 처리
    
    });  

Promise상태

  • Pending: 비동기 처리가 진행중이면 대기 상태
  • Fulfilled : 작업 성공시, 이행 상태
  • Rejected : 작업 실패시, 거부 상태

상태는 단 한 번만 바뀝니다.

WHY? 비동기 결과를 안정적으로 다루기 위해서입니다.

Promise는 지금은 없지만, 나중에 반드시 생기는 하나의 결과입니다.
결과는 성공 아니면 실패겠죠.

근데 만약에 결과가 여러번 바뀌면 어떻게 될까요?
지옥이 펼쳐집니다. 값을 예측하기 힘들겠죠. 그럼 디버깅 하기 힘들고 그럼 개발자는 눈물이 납니다.
SO, resolve, reject만 유효하고 이후 호출은 전부 무시합니다.

실제 코드로 실험해보면 다음과 같습니다.

const p = new Promise((resolve, reject) => {
  resolve('성공1');
  resolve('성공2');
  reject('실패');
});

p.then(console.log).catch(console.error);
//  "성공1"

Promise 상태 확정 함수

  • resolve : fulfilled 상태로 확정시키는 함수
  • reject : rejected 상태로 확정시키는 함수

Promise then / catch / finally 메소드

  • then(): 이행되었을 때
  • catch(): 거부되었을 때
  • finally() : 이행되거나 거부되더라도 항상 실행

→ 프로미스는 생성과 동시에 비동기 작업을 처리합니다.
then, catch일 때 비동기 작업을 처리하는 것이 아닙니다.

const myPromise = new Promise((resolve, reject)=>{
  setTimeout(()=>{
    const text = prompt('hello를 입력해봐~')
    if(text === 'hello'){
      resolve('success')
    } else {
      reject('hello 입력 안함 ㅠ')
    }
  }, 2000)
}); 

myPromise
  .then((result)=> {
    console.log('result:', result)
  })
  .catch((error)=>{
    console.log('error:', error)
  })
  .finally(()=>{
    console.log('final')
  })

// hello를 입력하면 콘솔창에 
// result: success 

// hello를 입력하지 않으면 콘솔창에 
// error: hello 입력 안함 ㅠ

Promise 체이닝 규칙

  • 체이닝이란? then이 Promise를 반환하기 때문에, 다음 then을 계속 연결할 수 있는 구조
    • promise.then(…) → 항상 새로운 Promise를 반환합니다.
      promise
        .then(...)
        .then(...)
        .then(...)
      SO, 위와 같은 코드를 쓸 수 있게됩니다. ⇒ 같은 Promise를 이어 쓰는게 아니라 새로운 then마다 새로운 Promise가 생기는 구조입니다.
  • then 안에서 return 하면 무슨 일이 일어날까?
    • 값을 반환합니다. → 다음 then에서 반환된 값을 사용할 수 있습니다
      WHY? promise 객체를 새로 만드니까

      myPromise
      	.then((result)=> {
      		console.log('result:', result)
      		return `결과는? ${result}`
      	})
      	.then((result)=> {
      		console.log('result:', result)
      	})
      
      	// hello를 입력하면 콘솔창에 아래와 같이 나옴 
      	// result: success
      	// result: 결과는? success
      	// final
      	
  • then은 여러개 붙여도 순서가 보장될까?
    • 예 보장됩니다. then은 이전 Promise가 fulfilled 상태가 되어야 다음 Promise가 실행됩니다.
      WHY? promise는 체이닝 구조로 비동기 흐름을 제어하니까요

      	.catch((error)=>{
      		console.log('error:', error)
      	})
      	.finally(()=>{ //결과에 상관없이 무조건 실행됨
      		console.log('final')
      	})
      	
  • then은 왜 항상 비동기일까?
    • Promise의 then은 “이미 끝난 Promise”라도 반드시 다음 턴에 실행됩니다.

      Promise.resolve().then(() => {
        console.log('then');
      });
      
      console.log('sync');
      
      // 결과
      // sync
      // then
      

      이는 실행 순서를 예측 가능하게 하고, 동기/비동기 혼합으로 인한 문제를 방지하기 위함이며, Microtask Queue 규칙에 따라 처리되기 때문입니다.

      Microtask Queue란?

      • 현재 실행 중인 코드가 끝나자마자, 가장 먼저 실행돼야 하는 작업들
      • 자바스크립트 실행 흐름은 Call stackMicrotask queueMacrotask queue입니다.
        console.log(1); // Call stack
        
        Promise.resolve().then(() => {
          console.log(2); // Microtask queue
        });
        
        setTimeout(() => {
          console.log(3);// Macrotask queue
        });
        
        console.log(4); // Call stack
        
        //결과
        1
        4
        2
        3
        SO, then은 Promise의 상태와 상관없이 항상 Microtask Queue에 등록되어 현재 실행 중인 동기 코드가 모두 끝난 뒤 실행되도록 설계되었습니다.

Promise 등장 이전의 비동기 처리 방식

  • 비동기 작업을 처리하는 함수에 성공 콜백과 실패 콜백을 각각 넘겨서 완료 상태에 따른 처리를 했습니다.

    doSomething((err, result) => {
      if (err) { /* 실패 */ }
      else { /* 성공 */ }
    });

    → BUT, 여러 비동기 작업이 순차적으로 의존해야 할 경우!

    콜백 함수안에 콜백 함수가 중첩되는 callback hell이 펼쳐집니다

    ⇒ SO, 코드 가독성 + 유지보수성 저하

    • 콜백 지옥과 체이닝 차이
      // 콜백 지옥 
      doA(() => {
        doB(() => {
          doC(() => {});
        });
      });
      
      // 체이닝
      doA()
        .then(doB)
        .then(doC);
      
      체이닝은 중첩이 아니라 흐름을 제어하는 것입니다.

Promise 조합 메서드

메서드언제 끝남실패 처리핵심 용도
Promise.all전부 끝나야하나라도 실패 → 즉시 reject다 성공해야 의미 있을 때
Promise.allSettled전부 끝나야실패해도 reject ❌성공/실패 전부 보고 싶을 때
Promise.race제일 먼저 끝난 하나그 결과 그대로타임아웃 / 경쟁
Promise.any제일 먼저 성공전부 실패해야 reject대안 서버, fallback
  • Promise.all
    • 모두 성공해야 성공! 하나라도 실패하면 즉시 reject됩니다

      await Promise.all([
        fetchUser(),
        fetchPosts(),
        fetchComments(),
      ]);
      
    • 언제 사용?

      • 페이지 렌더링에 전부 필요할 때
      • 하나라도 없으면 화면을 못 그릴 때

      → 하나라도 실패하면 나머지 결과는 무시됩니다.

      BUT 요청 자체가 취소되지는 않습니다.

      WHY? Promise에는 취소 개념이 없습니다.

      Promise가 할 수 있는 건 상태(pending / fulfilled / rejected)를 관찰하고 결과를 전달하는 것입니다.

      즉, Promise는 실행 중인 비동기 작업을 중단하지 못합니다.
      fetch, setTimeout, ajax를 강제 종료할 힘이 없어요.

      HOWEVER! 취소가 필요할 경우 AbortController 같은 별도 메커니즘을 사용해야 합니다.

  • Promise.allSettled

    • 성공/실패 여부 상관없이 모두 완료될 때까지 대기

      const results = await Promise.allSettled([
        uploadImage(),
        uploadVideo(),
      ]);
      
      // 결과
      [
        { status: 'fulfilled', value: ... },
        { status: 'rejected', reason: ... }
      ]
      
    • 언제 사용?

      • 로그 수집
      • 여러 요청 중 일부만 성공해도 되는 경우
      • 실패 이유까지 보고 싶을 때
  • Promise.race

    • 가장 먼저 끝난 것 하나만 반환

      Promise.race([
        fetch('/api'),
        timeout(3000),
      ]);
    • 언제 사용?

      • 타임아웃 구현
      • 가장 빠른 서버 선택
    • 성공이든 실패든 먼저 끝난 것 기준

      HOW? 상태가 제일 먼저 확정된 Promise를 그대로 따라갑니다.

      const fast = new Promise(resolve =>
        setTimeout(() => resolve('fast'), 100)
      );
      
      const slow = new Promise(resolve =>
        setTimeout(() => resolve('slow'), 1000)
      );
      
      Promise.race([fast, slow])
        .then(result => console.log(result));
      
      // 결과
      // fast

      BUT 나머지 코드 실행을 멈추는게 아니라 결과만 무시할 뿐 코드 실행은 계속됩니다.

  • Promise.any

    • 가장 먼저 성공한 Promise 반환

      Promise.any([
        fetchFromA(),
        fetchFromB(),
      ]);
      
    • 언제 사용?

      • CDN / 미러 서버
      • 백업 API
    • 모두 실패하면 AggregateError 발생

Promise의 then / catch 메서드와 all/ allSettled 메서드들은 뭐가 다른거임?

then / catch하나의 Promise 흐름을 이어가는 인스턴스 메서드입니다.

→ Promise의 결과를 받아 다음 비동기 작업으로 전달함으로써, 비동기 코드를 동기 코드처럼 읽히게 만들어 흐름을 제어합니다.

반면 Promise.all / Promise.allSettled여러 Promise를 하나의 Promise로 조합하는 정적 메서드입니다.

→ 각 Promise는 서로 결과를 공유하지 않고 병렬로 실행되며, 모든 작업의 완료 상태를 기준으로 하나의 결과를 반환합니다.

async - await란?

  • Promise의 완료를 기다리기 위한 문법
  • async 키워드로 정의한 함수 내에서 호출되는 promise 앞에 await 키워드를 쓰면 해당 promise가 완료될 때까지 코드의 실행을 일시중단할 수 있습니다.
    async function fetchData(){
    	try{
    		const response = await fetch('https:주소')
    		const data = await response.json()
    		console.log(data)
    	}catch(error){
    		console.log('Fetch Error:', error)
    	}
    	
    }
    fetch동작이 완료될 때까지 아래 부분을 실행하지 않아요
  • 비동기 코드를 마치 동기 코드처럼 쉽게 작성 가능합니다.

async - await를 사용시 주의사항

  • await 에러 핸들링은 반드시 try-catch 블록에서 해야합니다.

    try {
    	await asyncFn();
    } catch (e) {
    	// 에러는 이 안에서만 잡힘
    }

    BUT awaitpromise가 완료될 때까지 함수의 실행을 중단하기 때문에, 실행흐름을 잘 고려하여 적재적소에 써야합니다.

    ex) 여러 비동기 작업이 순차적으로 진행될 필요가 없는 경우

     async function fetchExp(){
    	 try{
    		 const result1 = await fetch('https://api/result1')
    		 const result2 = await fetch('https://api/result2')
    		 console.log(await result1.json(), await result2.json())
    	 }catch(error){
    		 console.log('Fetch Error:', error)
    	 }
     }
    • 실행흐름
      1. fetch('result1') 요청 보냄
      2. result1 응답이 올 때까지 대기
      3. fetch('result2') 요청 보냄
      4. result2 응답 대기
      5. 둘 다 끝나면 출력
      ⇒ 즉, 두 fetch가 "직렬(순차)"로 실행됩니다.

      BUT result1의 값이 다음 fetch 동작에 사용되지 않습니다. d
      result1 결과가 result2 요청에 전혀 영향 없기 때문에 순차적으로 실행될 필요가 없어요
      이럴 때는 async-await 보다는 promise.all을 사용하는 것이 더 바람직합니다.

    • promise.all 버전

      async function fetchExp() {
        try {
          const [result1, result2] = await Promise.all([
            fetch('https://api/result1'),
            fetch('https://api/result2')
          ])
      
          console.log(
            await result1.json(),
            await result2.json()
          )
        } catch (error) {
          console.log('Fetch Error:', error)
        }
      }
      • 실행흐름
      1. fetch1 즉시 요청

      2. fetch2 즉시 요청

      3. 둘 다 동시에 날아감

      4. 둘 다 끝날 때까지 기다림

      5. 결과 사용

        ⇒ 즉, 두 fetch가 "병렬"로 실행됩니다.

한 줄 요약 정리

  • Promise

    미래에 완료될 하나의 비동기 결과를 값처럼 다루기 위한 객체

  • Promise 상태

    pending → fulfilled / rejected로 한 번만 확정됨

  • resolve / reject

    Promise를 fulfilled 또는 rejected 상태로 확정하는 함수

  • then

    이전 Promise의 결과를 받아 다음 Promise로 이어주는 흐름 제어 메서드

  • catch

    체이닝 중 발생한 에러를 처리하는 메서드

  • finally

    성공/실패 여부와 관계없이 항상 실행되는 후처리 메서드

  • Promise 체이닝

    then이 새로운 Promise를 반환하면서 비동기 흐름을 연결하는 구조

  • then은 항상 비동기

    실행 순서의 일관성과 안정성을 보장하기 위함

  • Microtask Queue

    Promise의 then / catch 콜백을 실행하기 위한 우선순위가 높은 작업 큐

  • async / await

    Promise 체이닝을 동기 코드처럼 작성하게 해주는 문법

  • Promise.all

    모든 Promise가 성공해야 성공하는 병렬 조합 메서드

  • Promise.allSettled

    성공/실패와 무관하게 모든 Promise 결과를 수집하는 메서드

  • Promise.race

    가장 먼저 상태가 확정된 Promise 하나의 결과를 따르는 메서드

  • Promise.any

    가장 먼저 성공한 Promise 하나의 결과를 반환하는 메서드


마치며

"Promise를 쓴다" ≠ "Promise를 이해한다"

이제는 왜 이렇게 쓰는지 설명할 수 있으면 성공 ✨


참고자료

https://www.youtube.com/watch?v=cDu9A5dl1J8&list=PLBh_4TgylO6CI4Ezq3OLRRzg2NAn3FLPB&index=2

https://www.youtube.com/watch?v=iUGLyhbwYkU

https://www.youtube.com/watch?v=wGF7eEXtD8Y

profile
💡프론트엔드 공부 기록

0개의 댓글