만두의 부트캠프 🤔
  • 3-1. [JavaScript] 비동기
    2023년 11월 08일 14시 20분 19초에 업로드 된 글입니다.
    작성자: 만두33

    # 동기와 비동기

     

    동기 (Synchronous)와 비동기 (Asynchronous)는 

    프로그램이 작업을 어떻게 처리하는지에 대한 두 가지 기본적인 접근 방식입니다. 

     

      동기 (Synchronous) 비동기 (Asynchronous)
    실행 방식 순차적 (Sequential) 비순차적 (Non-sequential)
    대기 여부 작업 완료까지 대기 (Blocking) 작업 완료를 기다리지 않고 다른 작업 수행 가능 (Non-blocking)
    다른 작업 수행 가능 불가능 가능
    코드의 간결성 상대적으로 간결함 복잡성이 증가할 수 있음

     

     

     

    JavaScript는 싱글 스레드 기반으로 동작하는 언어입니다. 따라서 동기적으로 작동하게 됩니다. 

    그러나 JavaScript에서도 비동기 처리가 가능하다고 배우셨는데요. 어떻게 된 걸까요?

    그 이유는 JavaScript가 작동하는 환경(런타임)에서 비동기 처리를 도와주기 때문에 특별한 작업 없이 비동기 처리를 할 수 있는 것입니다. 

     

     

    JavaScript의 비동기 프로그래밍은 이벤트 기반 및 콜백 함수를 통해 구현되며, 

    최근에는 Promise와 async/await를 사용한 비동기 코드 작성이 더 일반적으로 쓰입니다. 

     

    비동기 프로그래밍은 웹 브라우저 환경에서 이벤트 처리, AJAX 요청, 타이머 기반 작업, 

    서버 측 코드에서는 파일 시스템 액세스 및 데이터베이스 쿼리 등과 같은 여러 상황에서 사용됩니다.

     

     


    # callback

     

    비동기 함수의 순서를 제어하고 싶을 때 콜백(Callback)을 이용할 수 있습니다.

     

    콜백 함수 (Callback): 

    JavaScript에서 비동기 프로그래밍의 가장 기본적인 형태는 콜백 함수를 사용하는 것입니다. 

    콜백은 특정 이벤트가 발생하거나 비동기 작업이 완료되면 호출되는 함수입니다.

     

    예제코드)

    function fetchData(callback) {
      setTimeout(() => {
        const data = "데이터가 도착했습니다.";
        callback(data);
      }, 1000);
    }
    
    fetchData((data) => {
      console.log(data);
    });

     


    # promise

     

    비동기로 작동하는 코드를 제어할 수 있는 다른 방법은 Promise를 활용하는 방법입니다. 

    또한 Callback Hell을 방지하는 역할도 수행합니다.

     

     

    Promise: 

    Promise는 비동기 작업의 최종 결과를 나타내는 객체로, 성공 또는 실패 상황에 대한 처리를 지원합니다.

     


    new Promise


    Promise는 class이기 때문에 new 키워드를 통해 Promise 객체를 생성합니다. 또한 Promise는 비동기 처리를 수행할 콜백 함수(executor)를 인수로 전달받는데 이 콜백 함수는 resolve, reject 함수를 인수로 전달받습니다.

    Promise 객체가 생성되면 executor는 자동으로 실행되고 작성했던 코드들이 작동됩니다. 코드가 정상적으로 처리가 되었다면 resolve 함수를 호출하고 에러가 발생했을 경우에는 reject 함수를 호출하면 됩니다.

     

    let promise = new Promise((resolve, reject) => {
    	// 1. 정상적으로 처리되는 경우
    	// resolve의 인자에 값을 전달할 수도 있습니다.
    	resolve(value);
    
    	// 2. 에러가 발생하는 경우
    	// reject의 인자에 에러메세지를 전달할 수도 있습니다.
    	reject(error);
    });

     


    Promise 객체의 내부 프로퍼티

    new Promise가 반환하는 Promise 객체는 state, result 내부 프로퍼티를 갖습니다.

    하지만 직접 접근할 수 없고 .then, .catch, .finally의 메서드를 사용해야 접근이 가능합니다.

    State

    기본 상태는 pending(대기)입니다.

    비동기 처리를 수행할 콜백 함수(executor)가 성공적으로 작동했다면 fulfilled(이행)로 변경이 되고,

    에러가 발생했다면 rejected(거부)가 됩니다.

    Result

    처음은 undefined입니다.

    비동기 처리를 수행할 콜백 함수(executor)가 성공적으로 작동하여 resolve(value)가 호출되면 value로, 

    에러가 발생하여 reject(error)가 호출되면 error로 변합니다.

     


     

    then, catch, finally

     

    Then

    executor에 작성했던 코드들이 정상적으로 처리가 되었다면 resolve 함수를 호출하고

     .then 메서드로 접근할 수 있습니다. 

    또한 .then 안에서 리턴한 값이 Promise면 Promise의 내부 프로퍼티 result를 다음 .then 의 콜백 함수의 인자로 받아오고, 

    Promise가 아니라면 리턴한 값을 .then 의 콜백 함수의 인자로 받아올 수 있습니다. 

    아래의 .then 과 Promise chaining의 예시를 살펴보면서 동작 방식을 확인해 보세요.

    let promise = new Promise((resolve, reject) => {
    	resolve("성공");
    });
    
    promise.then(value => {
    	console.log(value);
    	// "성공"
    })

     

    Catch


    executor에 작성했던 코드들이 에러가 발생했을 경우에는 reject 함수를 호출하고 .catch 메서드로 접근할 수 있습니다.

    let promise = new Promise(function(resolve, reject) {
    	reject(new Error("에러"))
    });
    
    promise.catch(error => {
    	console.log(error);
    	// Error: 에러
    })

     

    Finally


    executor에 작성했던 코드들의 정상 처리 여부와 상관없이 .finally 메서드로 접근할 수 있습니다.

    let promise = new Promise(function(resolve, reject) {
    	resolve("성공");
    });
    
    promise
    .then(value => {
    	console.log(value);
    	// "성공"
    })
    .catch(error => {
    	console.log(error);
    })
    .finally(() => {
    	console.log("성공이든 실패든 작동!");
    	// "성공이든 실패든 작동!"
    })

    Promise chaining

     

    Promise chaining가 필요하는 경우는 비동기 작업을 순차적으로 진행해야 하는 경우입니다.

     Promise chaining이 가능한 이유는 .then, .catch, .finally 의 메서드들은 Promise를 리턴하기 때문입니다. 

    따라서  .then을 통해 연결할 수 있고, 에러가 발생할 경우 .catch 로 처리하면 됩니다.

    let promise = new Promise(function(resolve, reject) {
    	resolve('성공');
    	...
    });
    
    promise
      .then((value) => {
        console.log(value);
        return '성공';
      })
      .then((value) => {
        console.log(value);
        return '성공';
      })
      .then((value) => {
        console.log(value);
        return '성공';
      })
      .catch((error) => {
        console.log(error);
        return '실패';
      })
      .finally(() => {
        console.log('성공이든 실패든 작동!');
      });

     


    Promise.all()

    Promise.all()은 여러 개의 비동기 작업을 동시에 처리하고 싶을 때 사용합니다.
    인자로는 배열을 받습니다. 

    해당 배열에 있는 모든 Promise에서 executor 내 작성했던 코드들이 정상적으로 처리가 되었다면 

    결과를 배열에 저장해 새로운 Promise를 반환해 줍니다.

    function fetchData1() {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve("데이터 1");
        }, 2000);
      });
    }
    
    function fetchData2() {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve("데이터 2");
        }, 1500);
      });
    }
    
    function fetchData3() {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve("데이터 3");
        }, 1000);
      });
    }
    
    async function fetchAllData() {
      try {
        const [data1, data2, data3] = await Promise.all([fetchData1(), fetchData2(), fetchData3()]);
        console.log("전체 데이터:", [data1, data2, data3]);
      } catch (error) {
        console.error("에러 발생:", error);
      }
    }
    
    fetchAllData();

    이 코드에서 Promise.all은 fetchData1(), fetchData2(), fetchData3()의 모든 Promise가 완료될 때까지 기다립니다.

    그 후에는 각 Promise의 결과를 배열로 받아와서 해당 결과를 출력합니다.

     

    추가적으로 Promise.all()은 인자로 받는 배열에 있는 Promise 중 하나라도 에러가 발생하게 되면 나머지 Promise의 state와 상관없이 즉시 종료됩니다. 

    아래의 예시와 같이 1초 후에 에러가 발생하고 Error: 에러1이 반환된 후로는 더 이상 작동하지 않고 종료됩니다.

    Promise.all([
    	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러1'))), 1000),
    	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러2'))), 2000),
    	new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러3'))), 3000),
    ])
    	.then((value) => console.log(value))
      .catch((err) => console.log(err));
    	// Error: 에러1

    promise 전체 예제코드)

    function fetchData() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const success = true;
          if (success) {
            const data = "데이터가 도착했습니다.";
            resolve(data);
          } else {
            reject("데이터를 불러오는 데 실패했습니다.");
          }
        }, 1000);
      });
    }
    
    fetchData()
      .then((data) => {
        console.log(data);
      })
      .catch((error) => {
        console.error(error);
      });

     


    Promise Hell

    Promise를 통해 비동기 코드의 순서를 제어할 수 있지만 

    Callback 함수와 같이 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있습니다. 


    # async/await

    JavaScript는 ES8에서 async/await키워드를 제공하였습니다. 

    이를 통해 복잡한 Promise 코드를 간결하게 작성할 수 있게 되었습니다. 

    사용법은 간단합니다. 함수 앞에 async 키워드를 사용하고 async 함수 내에서만 await 키워드를 사용하면 됩니다. 

    이렇게 작성된 코드는 await 키워드가 작성된 코드가 동작하고 나서야 다음 순서의 코드가 동작하게 됩니다.

     

     

    async/await: 

    async 함수는 비동기 함수를 정의하고, await 키워드는 비동기 작업의 완료를 기다립니다. 

    이는 코드를 동기적으로 작성하면서 비동기 작업을 처리할 수 있도록 도와줍니다.

     

    // 함수 선언식
    async function funcDeclarations() {
    	await 작성하고자 하는 코드
    	...
    }
    
    // 함수 표현식
    const funcExpression = async function () {
    	await 작성하고자 하는 코드
    	...
    }
    
    // 화살표 함수
    const ArrowFunc = async () => {
    	await 작성하고자 하는 코드
    	...
    }

     

     

    다른 예제코드)

    function fetchData() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const success = true;
          if (success) {
            const data = "데이터가 도착했습니다.";
            resolve(data);
          } else {
            reject("데이터를 불러오는 데 실패했습니다.");
          }
        }, 1000);
      });
    }
    
    fetchData()
      .then((data) => {
        console.log(data);
      })
      .catch((error) => {
        console.error(error);
      });

     

     

     

     

    댓글