ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JavaScript] 프로미스 (feat. 동기/비동기, 콜백 함수, async~await)
    👩🏻‍💻 정리/JavaScript 2021. 8. 17. 23:00

    목차

    1. 동기와 비동기
        1.1 동기와 비동기란?
        1.2 동기와 비동기의 장단점
        1.3 비동기 처리가 필요한 이유
    2. 콜백 함수 (Callback Function)
        2.1 콜백 함수란?
        2.2 콜백 지옥
    3. 프로미스 (Promise)
        3.1 프로미스란?
        3.2 reject와 resolve
        3.3 Chaining
        3.4 state
        3.5 에러 처리 
        3.6 더 알아보기
    4. async & await
    5. 더 알아보기
        5.1 오늘 정리한 내용에서 확장하여 공부하면 좋은 내용
        5.2 위의 내용까지도 다 공부하고 나서 추가적으로 공부하면 좋은 내용


    프로미스 공부를 들어가기에 앞서, 먼저 짚고 넘어갈 내용들을 살펴보고, 프로미스에 대해서 공부해보도록 하자!

    1. 동기와 비동기

    1.1 동기와 비동기란?

    웹 앱을 만들다 보면 처리할 때 시간이 걸리는 작업들이 있는데, 예를 들어 클라이언트 쪽에서 서버 쪽 데이터가 필요할 때는 Ajax를 사용해 서버의 API를 호출하여 데이터를 수신하는 경우가 있다. 이렇게 서버의 API를 사용해야 할 때는 네트워크 송수신 과정에서 시간이 걸리기 때문에 작업이 즉시 처리되는 게 아니라 응답을 받을 때까지 기다렸다가 처리하는데, 이 과정에서 해당 작업을 비동기적으로 처리하게 된다. 그렇다면 동기와 비동기, 그 정확한 뜻은 뭘까?

    동기란, 말 그대로 동시에 일어난다는 뜻으로 요청과 그 결과가 동시에 일어난다는 약속으로, 바로 요청을 하면 시간이 얼마가 걸리든지 요청한 자리에서 결과가 주어진다. 즉, 현재 실행 중인 코드가 끝나야 다음 코드가 실행된다는 것이다. 대표적인 예로는 확인 버튼을 눌러주지 않으면 끝나지 않는 alert가 있다.

    반면에 비동기란, 동시에 일어나지 않는다는 뜻으로 요청과 결과가 동시에 일어나지 않는다. 즉, 현재 실행 중인 코드가 완료되지 않아도 다음 코드로 넘어간다. 이때 비동기 task는 브라우저에게 실행하라고 맡겨 놓고 다음 task로 넘어가는 구조이다.

    만약 작업을 동기적으로 처리한다면, 요청 결과가 나올 때까지 다른 작업을 할 수가 없는데, 비동기적으로 처리를 하면 웹 앱이 멈추지 않기 때문에 여러 가지 요청을 할 수 있고, 기다리는 과정에서 다른 함수도 호출할 수 있다.

     

    1.2 동기와 비동기의 장단점

    동기와 비동기 모두 장단점이 있다.

    * 동기
    - 장점: 코드를 순서대로 하나씩 실행하기 때문에 실행 순서가 보장된다.
    - 단점: 현재 실행 중인 task가 종료될 때까지 다음 task가 실행이 되지 않는다는 점. 이를 task가 블로킹(blocking)된다고 표현한다.

    * 비동기
    - 장점: 현재 실행 중인 task가 완료되지 않더라도 다음 task를 실행하기 때문에 블로킹이 발생하지 않는다.
    - 단점: task의 실행 순서가 보장되지 않는다.

     

    1.3 비동기 처리가 필요한 이유

    자바스크립트 엔진은 한번에 하나의 task만 실행할 수 있는 싱글 스레드 방식을 채택하고 있다. 그렇기 때문에 시간이 걸리는 task를 실행하는 경우 블로킹(blocking, 작업 중단)이 발생한다. 그렇기 때문에 비동기 처리가 필요한 것이다.

     

    2. 콜백 함수 (Callback Function)

    2.1 콜백 함수란?

    자바스크립트에서 비동기 작업을 할 때, 가장 흔히 사용하는 방법이 콜백 함수이다. 콜백 함수란 이름 그대로 나중에 호출되는 함수를 말하는데, 함수의 매개변수가 함수일 때, 매개변수로 받은 함수를 콜백 함수라고 부른다. 즉,  콜백 함수는 콜백 함수라는 유니크한 문법적 특징을 가지고 있는 것이 아니라, 호출 방식에 의한 구분이다. 예를 들어, setTimeOut이나 EventHandler가 대표적인 예이다.

    추가적으로 고차 함수(Higher Order Function)매개변수를 함수로 받은 함수를 말한다. 즉, 외부에서 콜백함수를 전달받은 함수를 뜻하는데, 

    // 고차함수 = 매개변수로 함수를 받은 함수 = showMessage
    function showMessage(msg, closeFn) {
    	// 로직...
    	// closeFn = 콜백 함수
    	closeFn(true);
    }

    위 코드를 보면 showMessage에는 두번째 매개변수로 closeFn이 들어온다. 이때 showMessage를 고차 함수라고 말한다.

    ❗ 콜백 함수에 대한 오해?

    매개변수로 넘겨주는 콜백 함수는 어쩔 땐 비동기로, 어쩔 땐 동기로 작동한다. 즉, 콜백 함수라고 해서 항상 비동기 처리에만 쓰이는 것은 아니다! 예를 들어, 아래 같은 코드의 경우, map 메소드가 돌면서 바로 실행하고 끝나고, 실행하고 끝나는 식이기 때문에 비동기가 아닌 동기 콜백(sync callback)이다!

    [1, 2, 3].map(el => el * 2);

    비동기 처리를 위해서 콜백 함수를 사용하다 보니 콜백 함수는 비동기 처리를 할 때 사용하는 함수라고 오해를 하는 경우가 있다. 하지만 콜백 함수는 동기 처리를 하는 경우에도 사용된다!

     

    2.2 콜백 지옥

    그러나, 이 콜백 함수에는 치명적인 단점이 있는데, 

    계속 쓰다가는 위 사진과 같이 콜백 지옥같은 코드가 생길 수 있다는 점이다. 이러한 콜백 지옥이 생기는 경우는 아래와 같다.

    - 하나의 비동기 요청이 완료된 뒤, 완료로 인해 얻어진 값을 사용해 다음 비동기 요청이 이루어지는 경우.
    - 여러 번의 비동기 호출이 이루어지는데 각 처리는 비동기로 이루어지나, 각 비동기 호출 간의 실행 순서는 동기적이었으면 하는 경우.

    그렇기 때문에 굉장히 비효율적이다.

    🔗 콜백 지옥 아티클 번역본

     

    3. 프로미스 (Promise)

    3.1 프로미스란?

    비동기 동작을 처리하기 위해 ES6에 도입된 문법이다. 콜백 지옥 같은 코드가 형성되지 않게 하는 방안으로, 자바스크립트 내장 클래스이다. Promise 클래스를 인스턴스화해서 promise 객체를 만들어 사용한다. 반환된 promise 객체로 원하는 비동기 동작을 처리한다.

     

    3.2 resolve와 reject

    let promise = new Promise(function (resolve, reject) {
      // 비동기 로직을 작성한다.
    });

    promise 객체를 생성할 때 Promise 클래스 인자로 해당 요청에 대한 콜백함수를 넘겨준다. 콜백 함수에는 2개의 함수가 인자로 전달되는데, 첫 번째는 정상 수행 후 실행할 resolve이고, 두 번째는 실패 후 실행할 reject이다.

    참고로, resolve와 reject는 미리 정의하지 않더라도 자바스크립트 엔진에서 미리 정의해놓는다. 그렇기 때문에 전달을 하지 않더라도 호출하는 데에 문제가 발생하지 않는다.

    let promise = new Promise(function(resolve, reject) {
      resolve(); // resolve와 reject가 정의되지 않았음에도
      reject(); // 에러가 발생하지 않는다
      console.log('실행 됨');
    });

     

    3.2.1 resolve 이해하기

    let promise = new Promise(function(resolve, reject) {
      // 여기 비동기 로직을 작성한다!
    
      // 성공하면 -> resolve가 호출되고
      // 실패하면 -> reject가 호출된다.
    });
    
    promise.then(function() {
      // 이 함수가 바로 위의 resolve!
      // 위의 비동기 로직이 성공하면 호출된다!
    });

    그렇다면 비동기 처리가 성공했을 때, 즉 resolve 함수에서는 어떤 작업을 할지에 대해서 정의를 해놓아야 한다.
    이때 반환된 promise 객체에 then이라는 메소드를 사용하여 그 안에 콜백 함수를 넘겨주는 식으로 작성하면 된다. 넘겨지는 콜백 함수가 바로 resolve에 해당한다!

     

    3.2.2 reject 이해하기

    let promise = new Promise(function(resolve, reject) {});
    
    promise.then(function() {}, function() {});
    // then으로 받게 되는 두 개의 인자 모두 콜백 함수
    // 첫 번째는 성공했을 때 실행할 resolve 함수 정의
    // 두 번째는 실패했을 때 실행할 reject 함수 정의

    비동기 처리가 실패했을 때의 reject 함수도 정의해줘야 한다. promise.then() 객체 안에 두 번째 인자로, 실패했을 때의 처리 함수를 넣어주면 된다.(첫 번째 인자는 resolve 함수!)

     

    3.2.3 주의해야할 부분

    resolve와 reject는 여러 번 호출하더라도 첫 번째로 호출된 코드만 실행되고 종료된다. 또한 reject와 resolve를 같이 쓰게 되는 경우, 둘 중 먼저 쓰인 것이 호출되고 종료된다.

     

    3.3 Chaining

    체이닝이란 순차적으로 각각의 작업이 이전 단계 비동기 작업이 성공하고 나서 그 결과값을 이용하여 다음 비동기 작업을 실행해야 하는 경우에 사용된다! 

    getProducts()   // 상품 가져오고
      .then(getComments)   // 그리고 후기 가져오고
      .then(getLikes)   // 그리고 좋아요 가져오고

    위 코드와 같이 순차적으로 처리하고 싶을 때 사용한다.

    let promise = new Promise(function(resolve, reject) {
      setTimeout(function() {
      resolve(1);
      }, 1000);
    });
    
    promise
      .then(function(first) {
        console.log('first', first);
        return 2;
      })
      .then(function(second) { 
      	console.log('second', second);
    
      	return new Promise(function(resolve, reject) {
        	setTimeout(function() {
        		resolve(3);
        	}, 1000);
      	});
      })
      .then(function(third) {
      	console.log('third', third);
    });

    좀 더 복잡한 예제를 살펴보자! 리턴 값으로 일반값을 주는 경우도 있고, 새로운 프로미스를 반환하는 경우도 있다. 이때는 리턴하는 값이 일반 값이면 다음 then에서 매개변수로, promise인 경우에는 그다음 then의 resolve가 되는 거라고 생각하면 된다.

     

    3.4 state

    promise 객체는 3가지의 상태를 가지고 있다.

    - pending (대기) : 비동기 로직이 아직 완료되지 않은 상태를 말한다.

    - Fulfilled (이행) : 비동기 처리가 완료되어 프로미스가 결과값을 반환해 준 상태를 말한다.

    - Rejected (실패) : 비동기 처리가 실패하거나 오류가 발생한 상태를 말한다.

     

    3.5 에러 처리

    비동기 로직의 경우 실행 순서가 보장이 되지 않기 때문에 어디서 에러가 발생했는지 파악하는 것도 힘들다. 그렇기 때문에 코드를 작성할 때 사전에 에러 처리를 꼭 해줘야 한다.

    에러 처리의 종류로는 rejectcatch가 있는데, reject의 경우,

    let promise = new Promise(function(resolve, reject) {
      setTimeout(function() {
      	reject('으악!');
      }, 2000);
    });
    
    promise.then(function() {}, function(msg) {
      console.log(msg);
    });

    개발자가 의식적으로 에러가 난다는 상황을 생각하고 미리 reject을 호출한다. 그러면 promise.then()에서 두 번째 인자의 콜백 함수가 실행된다.

    두 번째 인자에 reject 함수를 쓰는 대신에, catch를 쓸 수도 있다.

    let promise = new Promise(function(resolve, reject) {
      setTimeout(function() {
      	reject('으악!');
      }, 2000);
    });
    
    promise
      .then(function() {})
      .catch(function(err) {
      	console.log(err);
      });

    then()에 성공했을 때의 resolve 함수를 쓰고, catch()에 실패했을 때의 reject 함수를 작성하는 것인데, 위에 reject를 두 번째 인자로 넣어주는 방식보다 가독성이 훨씬 좋기 때문에 에러는 대부분 catch를 사용하여 처리한다.

     

    3.6 더 알아보기

    3.6.1 Promise.finally()

    then()과 catch() 다음으로 적어주는 메소드인데, 성공하든 실패하든 항상 실행하는 코드이다.

    🔗 MDN finally 메소드

    3.6.2 Promise.all()

    여러 프로미스를 다 하고 그다음에 하고 싶은 경우에 사용한다. 즉, 순차적으로 진행하는 게 아니라 비동기 함수를 동시에 날려서 그게 다 끝났을 때 받고 싶은 경우! 그때 사용하는 것이 바로 Promise.all() 메소드이다. 응답 값이 배열로 한꺼번에 온다.

    🔗 MDN all 메소드

    3.6.3 놀라운 사실 💢

    인터넷 익스플로러에서는 Promise를 사용할 수 없다! 하지만 CRA를 사용하여 개발한 경우에는 Promise에 대한 처리를 다 해줬기 때문에 상관없었지만, 혹시라도 순수 자바스크립트로 개발을 해야 하는 경우에는 직접 폴리필까지 구현을 해야 한다. 이때는 바벨을 사용하자!

    💡 바벨(Babel)이란?
    자바스크립트의 문법을 확장해주는 도구로, 아직 지원되지 않는 최신 문법이나, 편의상 사용하거나 실험적인 자바스크립트 문법들을 정식 자바스크립트 형태로 변환해줌으로써 구형 브라우저 같은 환경에서도 제대로 실행할 수 있게 해주는 역할을 한다.

     

    4. async & await

    async와 await는 Promise를 더욱 쉽게 사용할 수 있도록 해 주는 ES2017(ES8)에 도입된 문법이다. 이 문법을 사용하려면 함수의 앞부분에 async 키워드를 추가해야 하는데, 그러면 항상 프로미스를 반환한다. 심지어 프로미스가 아닌 값을 반환하더라도 "이행 상태의 프로미스(resolved promise)"로 값을 감싸서 이행된 프로미스가 반환되게 한다.

    또한 await는 프로미스가 처리될 때까지 기다리게 하고, 결과는 그 이후에 반환된다. (await = '기다리다'라는 뜻을 가짐) 사용 시 주의할 점으로는 await은 반드시 async 함수 안에 있어야 한다는 것이다!

     

    5. 더 알아보기

    5.1 오늘 정리한 내용에서 확장하여 공부하면 좋은 내용

    - 실행 컨텍스트
    - 이벤트 루프
    - 스택, 큐

    5.2 위의 내용까지도 다 공부하고 나서 추가적으로 공부하면 좋은 내용

    - 제너레이터
    - iterator
    - iterable object