Skip to main content

왜 이 내용을 미리 배워야 할까

지금까지는 버튼을 누르면 글자가 바뀌고, 조건문이 실행되고, 반복문이 돌아가는 식으로 눈앞의 코드 흐름을 다뤘습니다. 그런데 실제 웹 개발에서는 화면 안에서만 노는 코드보다, 밖에서 데이터를 가져오는 코드를 훨씬 자주 만나게 됩니다.

예를 들어 날씨 정보, 로그인한 사용자 정보, 쇼핑몰 상품 목록, 댓글 데이터는 대부분 서버에서 받아옵니다. 이때 꼭 등장하는 것이 비동기, Promise, async/await, 그리고 fetch입니다.

처음 들으면 이름이 조금 무섭지만, 사실 핵심은 단순합니다. "시간이 걸리는 일을 시켜 두고, 결과가 오면 그때 처리한다" 입니다. 이 감각만 잡히면 다음 단계로 훨씬 편하게 넘어갈 수 있습니다.

동기와 비동기: 기다리면서 할까, 맡겨 두고 할까

먼저 동기와 비동기를 아주 가볍게 구분해 봅시다.

동기 방식은 한 줄씩 차례대로 처리합니다. 앞의 일이 끝나야 다음 줄로 넘어갑니다.

console.log("첫 번째");
console.log("두 번째");
console.log("세 번째");

이 코드는 예상대로 첫 번째, 두 번째, 세 번째 순서로 실행됩니다. 읽는 순서와 실행 순서가 같습니다.

반면 비동기 방식은 시간이 걸리는 일을 잠깐 다른 곳에 맡겨 두고, 그동안 다음 코드를 계속 실행할 수 있습니다.

console.log("라면 물 올리기");

setTimeout(() => {
  console.log("면 넣기");
}, 2000);

console.log("그동안 그릇 꺼내기");

실행 결과는 보통 이렇게 나옵니다.

라면 물 올리기
그동안 그릇 꺼내기
면 넣기

왜 그럴까요? setTimeout()은 "2초 뒤에 이 함수를 실행해 줘" 라고 예약만 걸어 둡니다. 자바스크립트는 그 예약이 끝날 때까지 가만히 멈춰 있지 않고, 아래 줄을 먼저 실행합니다. 이런 식으로 자바스크립트는 기본 실행 흐름과 별개로 타이머나 네트워크 요청 같은 작업을 처리하며, 나중에 실행 순서를 다시 맞춰 줍니다.1

초보자 입장에서는 이렇게 기억하면 충분합니다.

  • 동기: 앞일이 끝나야 다음 일로 감

  • 비동기: 오래 걸리는 일은 맡겨 두고 다른 일 먼저 함

  • 웹에서 데이터 요청은 대부분 비동기로 다룸

Promise: 미래의 결과를 담는 약속 상자

비동기 작업은 결과가 바로 나오지 않습니다. 그래서 자바스크립트는 Promise라는 방식을 사용합니다.

Promise는 쉽게 말해 "결과가 아직 없지만, 나중에 성공하거나 실패할 예정인 값" 입니다. Promise는 보통 세 가지 상태를 가집니다.2

  • pending: 아직 기다리는 중

  • fulfilled: 성공해서 결과가 준비됨

  • rejected: 실패함

예제를 보겠습니다.

function 주문하기() {
  return new Promise((resolve, reject) => {
    const 재고있음 = true;

    setTimeout(() => {
      if (재고있음) {
        resolve("아메리카노가 나왔습니다.");
      } else {
        reject("재고가 없습니다.");
      }
    }, 1000);
  });
}

주문하기()
  .then((result) => {
    console.log("성공:", result);
  })
  .catch((error) => {
    console.log("실패:", error);
  });

여기서 중요한 포인트는 이렇습니다.

주문하기()는 바로 커피를 주지 않습니다. 대신 Promise 객체를 돌려줍니다. 그리고 1초 뒤에 성공이면 resolve(), 실패면 reject()를 호출합니다. 우리는 .then()으로 성공 결과를 받고, .catch()로 실패를 처리합니다.

이 패턴은 아주 많이 등장합니다. 그래서 Promise를 처음부터 완벽히 외우기보다, 먼저 "비동기 결과를 나중에 받기 위한 표준 상자" 라고 이해하면 좋습니다.

async/await: 비동기를 읽기 쉽게 바꾸는 문법

Promise는 훌륭하지만 .then()이 여러 번 이어지면 초보자에게는 조금 정신없어 보일 수 있습니다. 그래서 나온 것이 async/await입니다.

async를 함수 앞에 붙이면 그 함수는 항상 Promise를 반환합니다. 그리고 await는 Promise가 끝날 때까지 그 함수 안에서만 다음 줄 실행을 잠시 미룹니다.3

function 기다리기(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function 시작하기() {
  console.log("시작");
  await 기다리기(1000);
  console.log("1초 뒤 실행");
}

시작하기();

이 코드는 읽을 때 거의 동기 코드처럼 보입니다.

  1. 시작 출력

  2. 1초 기다림

  3. 1초 뒤 실행 출력

바로 이 점이 async/await의 가장 큰 장점입니다. 비동기 코드를 사람이 읽기 편하게 만든다는 것입니다.

다만 꼭 기억할 규칙이 있습니다.

  • await는 보통 async 함수 안에서만 사용한다

  • async 함수는 항상 Promise를 반환한다

  • 에러 처리는 try...catch와 함께 쓰면 편하다

예를 들어 실패 가능성이 있는 비동기 작업은 이렇게 처리할 수 있습니다.

function 실패하는작업() {
  return Promise.reject("문제가 생겼습니다.");
}

async function 실행하기() {
  try {
    const result = await 실패하는작업();
    console.log(result);
  } catch (error) {
    console.log("에러 처리:", error);
  }
}

실행하기();

fetch: 서버에서 데이터를 가져오는 가장 흔한 방법

이제 오늘의 주인공인 fetch()를 만나 봅시다.

fetch()는 웹에서 데이터를 요청할 때 자주 쓰는 함수입니다. 주소(URL)를 넘기면, 그 요청의 결과를 담은 Promise를 돌려줍니다. 그리고 응답 본문을 JSON으로 읽는 response.json()도 비동기이기 때문에 또 Promise를 반환합니다.4

말이 복잡해 보여도, 실제 코드는 패턴이 비슷합니다.

async function 사용자불러오기() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const data = await response.json();

  console.log(data);
  console.log(data.name);
  console.log(data.email);
}

사용자불러오기();

흐름을 차근차근 풀어 보면 이렇습니다.

  1. fetch()가 서버에 요청을 보냅니다.

  2. 응답이 오면 response에 담습니다.

  3. response.json()으로 응답 본문을 자바스크립트 객체로 바꿉니다.

  4. 이제 data.name, data.email처럼 사용할 수 있습니다.

이때 많은 초보자가 fetch()가 바로 데이터를 주는 줄 압니다. 하지만 실제로는 응답(Response) 을 먼저 받고, 그 안의 본문을 다시 읽어야 합니다.

JSON: 데이터를 주고받을 때 쓰는 공용 언어

서버와 브라우저가 대화할 때 자주 쓰는 형식이 JSON입니다. 이름은 JavaScript Object Notation이지만, 자바스크립트 전용은 아니고 여러 언어에서 널리 씁니다.

JSON은 보기에 객체와 비슷하지만, 정확히는 문자열 형식의 데이터 표현 방법입니다.

const objectData = {
  name: "민수",
  age: 20,
};

const jsonText = '{"name":"민수","age":20}';

console.log(typeof objectData); // object
console.log(typeof jsonText);   // string

둘은 비슷하게 생겼지만 다릅니다.

자주 쓰는 변환도 함께 알아두면 좋습니다.

const jsonText = '{"name":"민수","age":20}';
const user = JSON.parse(jsonText);

console.log(user.name); // 민수

const text = JSON.stringify(user);
console.log(text); // {"name":"민수","age":20}
  • JSON.parse()는 JSON 문자열을 자바스크립트 값으로 바꿉니다.

  • JSON.stringify()는 자바스크립트 값을 JSON 문자열로 바꿉니다.

실전에서는 fetch() 후 response.json()을 자주 쓰기 때문에 직접 JSON.parse()를 덜 보기도 합니다. 그래도 원리를 알고 있으면 훨씬 덜 헷갈립니다.

fetch와 async/await를 함께 쓰는 기본 패턴

초보자에게 가장 추천하는 형태는 아래 패턴입니다.

async function loadUser() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users/1");

    if (!response.ok) {
      throw new Error(`HTTP 오류: ${response.status}`);
    }

    const user = await response.json();
    console.log(user);
  } catch (error) {
    console.error("데이터를 가져오지 못했습니다:", error);
  }
}

loadUser();

이 코드는 아주 중요합니다. 왜냐하면 실무에서도 구조가 크게 다르지 않기 때문입니다.

먼저 try 안에서 요청합니다. 응답이 왔을 때 response.ok를 확인합니다. 문제가 없으면 JSON을 읽습니다. 중간에 네트워크 문제나 우리가 직접 던진 에러가 있으면 catch가 처리합니다.

특히 꼭 기억할 점이 하나 있습니다. fetch는 404나 500 같은 HTTP 오류에서 자동으로 reject되지 않을 수 있습니다. 즉, 주소가 틀렸거나 서버에서 오류를 보냈더라도 catch로 바로 가지 않을 수 있으니, response.ok를 직접 확인하는 습관이 중요합니다.5

받아온 JSON을 화면에 보여 주기

이전 장에서 배운 DOM 조작과 합치면 꽤 재미있는 일이 됩니다. 서버에서 데이터를 받아서 화면에 꽂아 넣을 수 있기 때문입니다.

예를 들어 HTML이 이렇게 있다고 해 봅시다.

<h1 id="name">불러오는 중...</h1>
<p id="email"></p>

자바스크립트는 이렇게 쓸 수 있습니다.

async function showUser() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users/1");

    if (!response.ok) {
      throw new Error("사용자 정보를 찾지 못했습니다.");
    }

    const user = await response.json();

    document.querySelector("#name").textContent = user.name;
    document.querySelector("#email").textContent = user.email;
  } catch (error) {
    document.querySelector("#name").textContent = "오류 발생";
    document.querySelector("#email").textContent = error.message;
  }
}

showUser();

이제 자바스크립트는 단순히 계산만 하는 언어가 아니라, 밖에서 데이터를 가져와 화면을 채우는 언어처럼 느껴질 것입니다. 바로 이 감각이 현대 웹 개발의 핵심입니다.

Promise 방식과 async/await 방식 비교하기

같은 작업도 두 가지 방식으로 쓸 수 있습니다.

먼저 Promise 체인 방식입니다.

fetch("https://jsonplaceholder.typicode.com/users/1")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP 오류: ${response.status}`);
    }
    return response.json();
  })
  .then((user) => {
    console.log(user.name);
  })
  .catch((error) => {
    console.error(error);
  });

그리고 async/await 방식입니다.

async function loadUser() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users/1");

    if (!response.ok) {
      throw new Error(`HTTP 오류: ${response.status}`);
    }

    const user = await response.json();
    console.log(user.name);
  } catch (error) {
    console.error(error);
  }
}

loadUser();

둘 다 틀리지 않습니다. 다만 입문 단계에서는 async/await가 보통 더 읽기 쉽습니다. 그래서 이 책에서도 이후 예제에서는 async/await를 더 자주 쓰게 될 것입니다.

자주 하는 실수

비동기를 처음 배울 때는 거의 비슷한 자리에서 넘어집니다. 아주 정상입니다.

  • await를 빼먹고 Promise 자체를 출력하는 실수

  • response.json() 앞에 await를 안 붙여서 데이터를 못 쓰는 실수

  • response.ok를 확인하지 않아서 404 응답도 성공처럼 다루는 실수

  • JSON 문자열과 자바스크립트 객체를 같은 것으로 착각하는 실수

  • 로컬 파일을 그냥 file://로 열어 fetch()가 제대로 동작하지 않는 상황을 만나는 실수

예를 들어 아래 코드는 실수입니다.

async function loadData() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const user = response.json();

  console.log(user);
}

loadData();

response.json()도 Promise를 반환하므로, 위 코드는 사용자 객체가 아니라 Promise를 출력할 수 있습니다.

올바른 코드는 이렇습니다.

async function loadData() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const user = await response.json();

  console.log(user);
}

loadData();

한 글자 차이지만 결과는 꽤 다릅니다. 비동기에서는 이런 작은 차이가 매우 중요합니다.

오늘 장에서 꼭 가져가야 할 핵심

오늘 배운 내용을 한 문장씩 정리하면 이렇습니다.

  • 동기는 순서대로 기다리며 실행하고, 비동기는 오래 걸리는 일을 맡겨 두고 다음 일을 할 수 있다.

  • Promise는 미래에 도착할 결과를 표현하는 객체다.

  • async/await는 Promise를 더 읽기 쉽게 다루는 문법이다.

  • fetch()는 서버에 요청을 보내고, 응답을 Promise 형태로 돌려준다.

  • response.json()도 비동기이므로 await가 필요하다.

  • HTTP 오류를 제대로 처리하려면 response.ok를 확인해야 한다.

  • JSON은 서버와 브라우저가 데이터를 주고받을 때 자주 쓰는 형식이다.

여기까지 이해했다면, 이제 여러분은 단순히 버튼 클릭을 처리하는 수준을 넘어 데이터가 오가는 자바스크립트의 입구에 들어선 것입니다. 다음 장의 작은 프로젝트에서는 이 흐름이 훨씬 더 살아 움직이기 시작할 것입니다.

참고

1Synchronous vs Asynchronous JavaScript – Call Stack, Promises, and More

2How to use promises - Learn web development | MDN

4How to use promises - Learn web development | MDN

5JavaScript fetch API

책 목록

처음부터 제대로 배우는 JavaScript 기초

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기