frontend/javascript

🟨 2-8. 비동기(Asynchronous) 완벽 이해 — setTimeout, Promise, async/await로 흐름 제어 배우기

mirabo01 2025. 11. 7. 08:50

1. 비동기란 무엇인가

자바스크립트는 단일 스레드(Single Thread) 로 동작한다.
즉, 한 번에 한 가지 작업만 처리할 수 있다.

하지만 실제 웹 환경에서는 동시에 많은 일이 일어난다.
예를 들어,

  • 서버에서 데이터를 불러오면서
  • 버튼 클릭도 감지하고
  • 애니메이션도 실행되어야 한다.

이걸 가능하게 하는 게 바로 비동기 처리(Asynchronous Processing) 다.

💡 “비동기란, 기다리지 않고 다음 일을 진행하는 프로그래밍 방식”이다.


2. 동기 vs 비동기

console.log("1️⃣ 첫 번째");
console.log("2️⃣ 두 번째");
console.log("3️⃣ 세 번째");

이건 동기(Synchronous) — 순서대로 실행된다.

반면, setTimeout 같은 비동기 코드를 섞으면 결과가 달라진다.

console.log("1️⃣ 시작");
setTimeout(() => console.log("2️⃣ 1초 뒤 실행"), 1000);
console.log("3️⃣ 종료");

출력:

1️⃣ 시작
3️⃣ 종료
2️⃣ 1초 뒤 실행

→ “기다리지 않고 다음 줄로 넘어갔다.”
이게 바로 비동기 실행의 본질이다.


3. 자바스크립트의 실행 구조 — 콜 스택과 이벤트 루프

자바스크립트는 아래 구조로 동작한다.

📦 Call Stack (호출 스택)
📬 Event Queue (이벤트 대기열)
🔁 Event Loop (순환하며 대기열을 비움)
  • Call Stack: 즉시 실행되는 코드가 쌓이는 곳
  • Event Queue: setTimeout, 이벤트, Promise 등의 콜백이 기다리는 곳
  • Event Loop: 스택이 비면 대기열에서 하나씩 꺼내 실행

즉, 자바스크립트는 실제로 “동시에 실행되는 것처럼 보이지만”,
사실은 순차적으로 빠르게 돌면서 비동기 콜백을 처리하는 구조야.


4. setTimeout / setInterval

비동기의 가장 기본형.

console.log("A");
setTimeout(() => console.log("B"), 2000);
console.log("C");

결과:

A
C
B

✅ setTimeout()은 지정된 시간 후 실행
✅ setInterval()은 일정 간격으로 반복 실행

let count = 0;
const timer = setInterval(() => {
  console.log(`타이머 실행: ${++count}`);
  if (count === 3) clearInterval(timer);
}, 1000);

5. 콜백 함수의 한계

비동기 코드를 콜백만으로 처리하면 이런 코드가 된다.

getUser(() => {
  getPosts(() => {
    getComments(() => {
      console.log("모든 데이터 로드 완료!");
    });
  });
});

이런 구조를 “콜백 지옥(Callback Hell)”이라 부른다.
→ 가독성, 유지보수, 디버깅 모두 최악.

이를 해결하기 위해 등장한 게 Promise 다.


6. Promise — 비동기의 약속

Promise는 “나중에 결과를 돌려줄게”라는 약속 객체다.

const promise = new Promise((resolve, reject) => {
  const success = true;
  setTimeout(() => {
    success ? resolve("✅ 성공!") : reject("❌ 실패!");
  }, 1000);
});

promise
  .then((result) => console.log(result))
  .catch((err) => console.error(err))
  .finally(() => console.log("작업 완료"));

✅ then : 성공 시 실행
✅ catch : 실패 시 실행
✅ finally : 무조건 실행


7. Promise 체이닝

비동기 작업을 순서대로 연결할 수도 있다.

getUser()
  .then((user) => getPosts(user.id))
  .then((posts) => getComments(posts[0].id))
  .then((comments) => console.log("결과:", comments))
  .catch((err) => console.error("에러:", err));

→ 콜백 지옥 대신 “논리적 흐름” 으로 읽힌다.


8. async / await — 비동기를 동기처럼 쓰는 문법

async/await은 Promise 기반 문법을 더 직관적으로 만든 문법이다.

function getData() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("서버 응답 완료!"), 1000);
  });
}

async function main() {
  console.log("데이터 요청 중...");
  const result = await getData();
  console.log(result);
  console.log("로직 종료");
}

main();

출력:

데이터 요청 중...
(1초 후)
서버 응답 완료!
로직 종료

✅ await은 Promise가 끝날 때까지 기다린다.
✅ async 함수 안에서만 사용할 수 있다.
✅ 코드 가독성이 획기적으로 개선된다.


9. 에러 처리

비동기에서 에러를 다루는 방법은 try-catch가 핵심이다.

async function main() {
  try {
    const res = await fetch("https://invalid.url");
    const data = await res.json();
    console.log(data);
  } catch (error) {
    console.error("에러 발생:", error.message);
  }
}
main();

✅ 비동기 함수에서도 try-catch 문법으로 안전하게 처리 가능.


10. 실무 예시 — API 데이터 불러오기

async function loadPosts() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=3");
  const posts = await res.json();

  const container = document.createElement("div");
  posts.forEach((p) => {
    const item = document.createElement("div");
    item.innerHTML = `

${p.title}

${p.body}

`;
    container.appendChild(item);
  });

  document.body.appendChild(container);
}

loadPosts();

✅ 실제 서비스에서 “데이터 불러오기 → DOM 출력”의 기본 패턴이다.
✅ React의 useEffect + fetch도 사실상 이 구조를 단순화한 것.


11. Promise.all — 병렬 처리

여러 비동기 작업을 동시에 실행하고 모두 끝날 때까지 기다릴 수 있다.

async function loadData() {
  const [users, posts] = await Promise.all([
    fetch("https://jsonplaceholder.typicode.com/users").then((r) => r.json()),
    fetch("https://jsonplaceholder.typicode.com/posts").then((r) => r.json()),
  ]);
  console.log("유저 수:", users.length);
  console.log("포스트 수:", posts.length);
}
loadData();

✅ 병렬 실행으로 성능 향상
✅ API 연속 호출이 아닌, 독립적인 요청 처리에 적합


12. 정리

개념 설명

비동기 기다리지 않고 다음 코드 실행
콜백 기본 구조지만 중첩 문제 발생
Promise 콜백 지옥 해결, 체이닝 가능
async/await 비동기를 동기처럼 표현
try/catch 비동기 오류 제어
Promise.all 여러 비동기 병렬 처리

13. 마무리

이제 자바스크립트의 진짜 흐름 제어 방식을 이해했다.
async/await을 완벽히 다루면,
React의 데이터 패칭, Next.js의 서버 액션, API 통신도 어렵지 않다.

자바스크립트는 “언제 실행되는가”를 이해할 때 비로소 통제할 수 있다.
비동기를 제어할 줄 아는 개발자는, 코드를 기다리게 만들 수 있다.