[JavaScript 시리즈 5편] async/await와 fetch로 데이터 불러오기

English version

DOM을 움직일 수 있게 됐다면 이제 서버 데이터와 화면을 연결해야 합니다. fetch는 브라우저 내장 네트워크 함수, async/await는 Promise 흐름을 한 줄씩 읽히게 만드는 문법입니다.

이번 글에서 새로 나오는 용어

  1. async/await: Promise를 순서대로 읽히게 해 주는 키워드로, await 줄에서 잠시 멈췄다가 다음 줄을 실행합니다.
  2. AbortController: 너무 오래 걸리는 네트워크 요청을 중간에 취소할 수 있게 만드는 도구입니다.

핵심 개념

  • Promise: “나중에 결과를 알려주는 상자”입니다. pending에서 시작해 성공(fulfilled) 또는 실패(rejected) 상태로 바뀝니다.
  • async/await: Promise를 조금 더 자연어처럼 읽게 도와주는 문법입니다. await는 해당 Promise가 끝날 때까지 다음 줄 실행을 잠시 멈춥니다.
  • fetch: HTTP 요청을 보내는 브라우저 API입니다. 요청이 네트워크까지 도달하면 성공으로 간주하므로 response.ok를 반드시 확인해야 합니다.
  • 로딩/에러 상태: 사용자에게 지금 어떤 단계인지 알려 주는 문구나 UI입니다. 스켈레톤, 스피너, 텍스트 등을 조합합니다.
  • AbortController: 느린 네트워크에서 요청을 취소하는 도구입니다. signal을 전달하면 타임아웃이나 버튼 클릭으로 요청을 멈출 수 있습니다.

코드로 확인하기

async function loadPost(id) {
  const response = await fetch(`/api/posts/${id}`);
  if (!response.ok) {
    throw new Error("게시글을 불러오지 못했습니다");
  }
  return response.json();
}

loadPost(1)
  .then((post) => console.log(post.title))
  .catch((error) => console.error(error.message));

response.ok는 HTTP 상태 코드가 200~299인지 알려 줍니다. 네트워크는 괜찮지만 서버가 404를 돌려줄 수 있으므로 꼭 확인해야 합니다.

const status = document.querySelector(".status");
const list = document.querySelector(".post-list");

async function loadPosts() {
  try {
    status?.textContent = "불러오는 중";
    const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
    if (!response.ok) throw new Error("네트워크 오류");
    const posts = await response.json();

    if (list) {
      list.innerHTML = posts
        .map((post) => `<li class="post-item">${post.title}</li>`)
        .join("");
    }
    if (status) status.textContent = "완료";
  } catch (error) {
    if (status) status.textContent = error instanceof Error ? error.message : "알 수 없는 오류";
  }
}

loadPosts();

상태 텍스트만으로도 사용자에게 요청 상황을 알려 줄 수 있습니다. 나중에는 로딩 스켈레톤, 에러 안내 모달을 붙이면 됩니다.

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 4000);

try {
  const response = await fetch("/api/attendance", { signal: controller.signal });
  clearTimeout(timeout);
  const data = await response.json();
  renderAttendance(data);
} catch (error) {
  if (error.name === "AbortError") {
    showToast("네트워크가 느립니다. 다시 시도하세요.");
  }
}

모바일 데이터 환경처럼 네트워크가 느린 경우에는 일정 시간이 지나면 요청을 취소하고 사용자에게 재시도를 권하는 것이 좋습니다.

async function loadDashboard() {
  const [profile, todos] = await Promise.all([
    fetch("/api/profile").then((res) => res.json()),
    fetch("/api/todos").then((res) => res.json()),
  ]);
  return { profile, todos };
}

Promise.all은 독립적인 여러 요청을 동시에 보낼 때 사용합니다. 응답이 모두 도착하면 배열 순서대로 결과를 돌려줍니다.

왜 중요한가

  • 학교 행사 신청 페이지처럼 외부 데이터를 불러야 하는 UI에서는 로딩·에러 상태가 곧 사용자 신뢰입니다.
  • async/await 구조를 익히면 코드 리뷰나 발표에서 “이 함수는 무엇을 기다리고, 언제 화면을 업데이트하는가”를 짧게 설명할 수 있습니다.
  • 느린 와이파이나 기숙사 네트워크처럼 품질이 들쭉날쭉한 환경에서 AbortController를 적용하면 앱이 멈춘 듯 보이는 상황을 줄일 수 있습니다.

실습

  • 따라 하기: JSONPlaceholder posts 엔드포인트를 호출해 로딩/완료/에러 텍스트를 순서대로 갱신합니다.
  • 확장하기: Promise.all/posts, /users를 동시에 불러와 카드 UI와 작성자 이름을 함께 렌더링합니다.
  • 디버깅: 잘못된 URL을 넣어 HTTP 404를 발생시키고, response.ok 검사를 뺐을 때 어떤 문제가 생기는지 비교합니다.
  • 완료 기준: 로딩 문구 → 데이터 렌더 → 에러 메시지 또는 재시도 버튼 흐름을 콘솔 경고 없이 재현하면 실습이 끝납니다.

마무리

fetchasync/await만 확실히 잡아도 백엔드와 화면을 직접 연결할 수 있습니다. 다음 글에서는 받은 데이터를 구조화하고 모듈로 나누는 방법을 살펴보겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요