동기, 비동기, 동기, 비동기, 동기,,,

 

SKALA 과정에서 수업을 듣던 중, 오랜만에 JS 기본 개념에 대해 공부하며 흐릿해진 개념에 대해 다시 정리해보려한다.

 

각각 파트에 대해서 배경, 문제점, 해결방법 그리고 예시 코드를 곁들인,, 느낌으로 정리해보려한다.

 


 

동기 (Synchronous) vs 비동기 (Asynchronous)

 

자바스크립트는 기본적으로 싱글 스레드 기반의 언어로, 코드가 한 줄씩 순차적으로 실행된다.

 

하지만, 네트워크 요청, 파일 입출력, 타이머 등의 작업은 시간이 걸리므로, 이러한 작업이 실행되는 동안 프로그램이 멈추지 않도록 비동기 처리 방식이 필요하다.

 

동기 방식에는 실행 시간이 긴 작업 (서버 요청, 파일 읽기 등)이 있으면 전체 코드 실행이 멈춰버리는 문제가 발생한다.

console.log("시작");

// 동기 코드 (블로킹)
// ex) 파일 읽기, 네트워크 요청, 데이터베이스 쿼리
for (let i = 0; i < 100000000000; i++) {
    console.log(i);
}

console.log("끝");

// 시작
// ...
//
// 끝이 출력 안됨

 

비동기 방식은 특정 작업이 끝날 때까지 기다리지 않고, 다음 코드가 실행되도록 처리한다.

 

이를 위해 콜백 함수(Callback func), 프로미스(Promise), async/await 같은 기법이 사용된다.

 

console.log("시작");

// 비동기 코드
setTimeout(() => {
    console.log("비동기 실행");
}, 1000);

conosole.log("끝");

// 시작
// 끝
// 비동기 실행

 

비동기 코드에서는 setTimeout이 실행된 후에도 다음 코드console.log("끝") 이 즉시 실행된다.

 

동기/비동기와 비동기가 처리될 때 자바스크립트 내부 엔진 동작 원리는 다음 글을 참고하길 바란다.

 

 


Callback Function (콜백 함수)

 

동기 방식의 blocking 을 해결하기 위해 비동기 방식을 고려했지만, 비동기의 경우 실행 순서를 보장하지 못하기 때문에 개발자가 원하는대로 컨트롤 하기 어렵다.

 

비동기 작업의 실행 순서를 보장하기 위해서 특정 작업이 끝난 후 실행할 함수를 전달하는 방식이 필요했다.

 

"콜백 함수를 통해 해결해보자"

function fetchData(callback) {
        setTimeout(() => {
            console.log("데이터 가져오기 완료");
            callback("success");
    }, 1000)
}

function fetchData2(callback) {
    setTimeout(() => {
        console.log("데이터2 가져오기 완료");
        callback("success2");
    }, 1000)
}

console.log("시작");

fetchData((data) => {
        console.log(data);
        fetchData2((data) => {
            console.log(data);
        });
    }
);

console.log("끝");

 

위와 같은 예시의 경우 콜백 함수를 사용하여 fetchData와 fetchData2의 실행 순서를 보장해준다.

 

하지만 함수의 개수가 많아지게 되면 콜백이 중첩되고, 코드가 깊어지고 가독성이 떨어지는 콜백 지옥(Callback Hell) 문제가 발생한다.

 

function getUser(userId, callback) {
  setTimeout(() => {
    console.log("사용자 정보 가져오기");
    callback({ id: userId, name: "John Doe" });
  }, 1000);
}

function getOrders(user, callback) {
  setTimeout(() => {
    console.log(`${user.name}의 주문 정보 가져오기`);
    callback(["주문 1", "주문 2", "주문 3"]);
  }, 1000);
}

function processPayment(order, callback) {
  setTimeout(() => {
    console.log(`${order} 결제 처리`);
    callback("결제 완료");
  }, 1000);
}

// 콜백 지옥 발생
getUser(1, (user) => {
  getOrders(user, (orders) => {
    processPayment(orders[0], (paymentStatus) => {
      console.log(paymentStatus);
      console.log("모든 작업 완료");
    });
  });
});

 

코드를 실행해보면 각각 setTimeout비동기를 사용하는 함수들이 순서에 맞게 동작하는 것을 확인할 수 있지만, 위와같이 가독성이 떨어지고 읽기 싫은 코드가 되어버린다.

 

이러한 문제를 해결하기 위해 콜백 대신Promise 또는 async/await을 사용하여 가독성을 높인다.

 

 


Promise (프로미스)

 

위에서 봤던 콜백 지옥 문제를 해결하고, 더 직관적으로 비동기 코드를 작성하기 위해 ES6 부터 Promise가 등장했다.

 

콜백 방식에서는 여러 개의 비동기 작업을 처리할 때 코드가 복잡해지고 유지보수가 어려워졌다.

 

Promise 객체를 사용하면 then()을 이용해 가독성을 높이고, catch()를 통해 에러 처리를 간편하게 할 수 있다.

 

function getUser(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("사용자 정보 가져오기");
      resolve({ id: userId, name: "John Doe" });
    }, 1000);
  });
}

function getOrders(user) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`${user.name}의 주문 정보 가져오기`);
      resolve(["주문 1", "주문 2", "주문 3"]);
    }, 1000);
  });
}

function processPayment(order) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`${order} 결제 처리`);
      resolve("결제 완료");
    }, 1000);
  });
}

getUser(1)
  .then(getOrders)
  .then((orders) => processPayment(orders[0]))
  .then((paymentStatus) => {
    console.log(paymentStatus);
    console.log("모든 작업 완료");
  })
  .catch((error) => console.log("에러 발생:", error));

 

위에서 봤던 콜백 지옥에 빠졌던 코드를 Promise를 활용하여 다시 작성해보았다.

 

thencatch를 통해 한결 순서에 맞게 읽기 쉬운 코드가 된 것을 볼 수 있다.

 


async / await

 

하지만 Promise 역시 then() 체인이 길어지면 역시 코드의 가독성이 떨어질 수 있기에 좀 더 직관적인 문법이 필요했다.

 

이를 해결하기 위해 또 async/await가 등장했다.

 

위에서 Promise를 활용하여 작성한 코드를 async/await를 활용하여 적어보면 동기 코드처럼 더욱 직관적으로 작성할 수 있다.

 

function getUser(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("사용자 정보 가져오기");
      resolve({ id: userId, name: "John Doe" });
    }, 1000);
  });
}

function getOrders(user) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`${user.name}의 주문 정보 가져오기`);
      resolve(["주문 1", "주문 2", "주문 3"]);
    }, 1000);
  });
}

function processPayment(order) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`${order} 결제 처리`);
      resolve("결제 완료");
    }, 1000);
  });
}

// Promise만 사용
// getUser(1)
//  .then(getOrders)
//  .then((orders) => processPayment(orders[0]))
//  .then((paymentStatus) => {
//    console.log(paymentStatus);
//    console.log("모든 작업 완료");
//  })
//  .catch((error) => console.log("에러 발생:", error));


// async/await 사용
async function processOrder() {
  try {
    const user = await getUser(1);
    const orders = await getOrders(user);
    const paymentStatus = await processPayment(orders[0]);

    console.log(paymentStatus);
    console.log("모든 작업 완료");
  } catch (error) {
    console.log("에러 발생:", error);
  }
}

processOrder();

 

한결 읽기 쉬운 코드가 된 것을 확인할 수 있다.

 


정리

 

방식 코드 가독성 유지보수성 에러 처리
콜백 함수 매우 나쁨 😵 어려움 ❌ 어렵고 복잡 ❌
Promise 개선됨 🙂 유지보수 쉬움 ✅ catch()로 편리 ✅
async/await 가장 좋음 😃 매우 쉬움 ✅✅ try/catch로 직관적 ✅✅

 

지피티를 통해 JS의 비동기 처리 방식에 대해 정리해보았다.

 

각각의 장단점을 비교하며 적절한 방식으로 활용하면 좋겠다.

 

실제 프로젝트에서 비동기 함수를 정의할 때 async/await를 많이 활용하고,  가끔 직접 프로미스를 리턴 해주어야할 경우 Promise를 활용했던거 같다.

'Javascript' 카테고리의 다른 글

this 키워드  (0) 2023.07.06
Javascript 공부 #6  (0) 2023.01.20
Javascript 공부 #5  (1) 2023.01.16
Javascript 공부 #4  (0) 2023.01.15
Javascript 공부 #2  (0) 2023.01.12