JavaScript 독학 가이드 10 - 비동기 처리와 API 호출

learning by Seven Fingers Studio 18분
JavaScript비동기PromiseasyncawaitfetchAPI프로그래밍

서버에서 데이터 가져오기, 파일 업로드, 타이머 등은 시간이 걸려요. 이런 작업을 비동기로 처리하지 않으면 브라우저가 멈춰버립니다. JavaScript에서 비동기 처리는 필수예요!

동기 vs 비동기

동기 (Synchronous)

한 작업이 끝나야 다음 작업 시작해요.

console.log("1");
console.log("2");
console.log("3");

실행 결과:

1
2
3

순서대로 1, 2, 3이 출력됩니다

비동기 (Asynchronous)

작업을 기다리지 않고 바로 다음으로 넘어가요.

console.log("1");
setTimeout(() => {
    console.log("2");
}, 1000);
console.log("3");

실행 결과:

1
3
// 1초 후...
2

2는 1초 후에 출력됩니다. setTimeout은 비동기라서 기다리지 않고 바로 다음 코드를 실행해요

콜백 지옥

옛날엔 비동기를 콜백으로 처리했어요.

// 사용자 정보 가져오기 → 주문 내역 가져오기 → 상세 정보 가져오기
getUser(userId, (user) => {
    getOrders(user.id, (orders) => {
        getOrderDetails(orders[0].id, (details) => {
            console.log(details);
        });
    });
});

이게 콜백 지옥이에요. 읽기도 어렵고 에러 처리도 복잡해요.

Promise

Promise는 비동기 작업의 결과를 나타내는 객체예요.

Promise 기본

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("성공!"); // 성공
        // reject("실패!"); // 실패
    }, 1000);
});

promise
    .then(result => {
        console.log(result); // "성공!"
    })
    .catch(error => {
        console.log(error);
    });

실행 결과:

// 1초 후...
성공!

Promise가 resolve되면 then이 실행됩니다

Promise 체이닝

function getUser(id) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ id, name: "철수" });
        }, 1000);
    });
}

function getOrders(userId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve([{ id: 1, product: "노트북" }]);
        }, 1000);
    });
}

getUser(123)
    .then(user => {
        console.log(user);
        return getOrders(user.id);
    })
    .then(orders => {
        console.log(orders);
    })
    .catch(error => {
        console.error(error);
    });

실행 결과:

// 1초 후...
{ id: 123, name: '철수' }
// 1초 후...
[ { id: 1, product: '노트북' } ]

Promise 체이닝으로 순차적으로 비동기 작업을 처리합니다. 콜백 지옥보다 훨씬 읽기 쉽죠?

async/await

Promise를 더 쉽게 쓰는 문법이에요.

기본 사용법

// Promise 방식
function getData() {
    return fetch("https://api.example.com/data")
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(error => console.error(error));
}

// async/await 방식
async function getData() {
    try {
        let response = await fetch("https://api.example.com/data");
        let data = await response.json();
        console.log(data);
    } catch (error) {
        console.error(error);
    }
}

비교:

// Promise 방식: .then() 체인
// async/await 방식: 동기 코드처럼 작성 가능
{ data: "..." }

async/await를 사용하면 비동기 코드를 동기 코드처럼 읽기 쉽게 작성할 수 있어요. await는 async 함수 안에서만 쓸 수 있어요

여러 비동기 작업

async function fetchUserData(userId) {
    try {
        let user = await getUser(userId);
        let orders = await getOrders(user.id);
        let details = await getOrderDetails(orders[0].id);

        return { user, orders, details };
    } catch (error) {
        console.error("에러 발생:", error);
    }
}

fetchUserData(123);

실행 결과:

// 1초 후 user 가져옴
// 1초 후 orders 가져옴
// 1초 후 details 가져옴
Promise { <pending> }

각 작업이 순차적으로 완료되면서 총 3초가 소요됩니다

fetch API

서버에서 데이터를 가져오는 API예요.

GET 요청

async function getTodos() {
    try {
        let response = await fetch("https://jsonplaceholder.typicode.com/todos");
        let todos = await response.json();
        console.log(todos);
    } catch (error) {
        console.error("에러:", error);
    }
}

getTodos();

실행 결과:

[
{ userId: 1, id: 1, title: 'delectus aut autem', completed: false },
{ userId: 1, id: 2, title: 'quis ut nam...', completed: false },
// ... 200개 항목
]

실제 API에서 할 일 목록 200개를 가져옵니다

POST 요청

async function createTodo(todo) {
    try {
        let response = await fetch("https://jsonplaceholder.typicode.com/todos", {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(todo)
        });

        let data = await response.json();
        console.log(data);
    } catch (error) {
        console.error("에러:", error);
    }
}

createTodo({
    title: "새로운 할 일",
    completed: false,
    userId: 1
});

실행 결과:

{
title: '새로운 할 일',
completed: false,
userId: 1,
id: 201
}

서버에 데이터를 전송하고 생성된 객체를 받습니다 (id가 자동으로 추가됨)

에러 처리

async function fetchData(url) {
    try {
        let response = await fetch(url);

        if (!response.ok) {
            throw new Error(`HTTP 에러! 상태: ${response.status}`);
        }

        let data = await response.json();
        return data;
    } catch (error) {
        console.error("데이터 가져오기 실패:", error);
        return null;
    }
}

실행 결과 (에러 발생 시):

> fetchData('https://api.example.com/invalid')
데이터 가져오기 실패: Error: HTTP 에러! 상태: 404
null

에러가 발생하면 catch 블록에서 처리하고 null을 반환합니다

Promise.all

여러 Promise를 동시에 실행해요.

async function getAllData() {
    try {
        let [users, posts, comments] = await Promise.all([
            fetch("https://jsonplaceholder.typicode.com/users").then(r => r.json()),
            fetch("https://jsonplaceholder.typicode.com/posts").then(r => r.json()),
            fetch("https://jsonplaceholder.typicode.com/comments").then(r => r.json())
        ]);

        console.log("사용자:", users);
        console.log("게시글:", posts);
        console.log("댓글:", comments);
    } catch (error) {
        console.error(error);
    }
}

getAllData();

실행 결과:

// 3개 요청이 동시에 실행됨
사용자: [ { id: 1, name: 'Leanne Graham', ... }, ... ] (10개)
게시글: [ { userId: 1, id: 1, title: 'sunt aut...', ... }, ... ] (100개)
댓글: [ { postId: 1, id: 1, name: 'id labore...', ... }, ... ] (500개)

Promise.all은 여러 비동기 작업을 병렬로 실행합니다. 모두 완료되면 결과를 배열로 반환해요. 하나라도 실패하면 전체가 실패해요

실전 예제

1. 사용자 검색

<!DOCTYPE html>
<html>
<body>
    <input type="text" id="searchInput" placeholder="사용자 검색">
    <div id="results"></div>

    <script>
        let input = document.querySelector("#searchInput");
        let results = document.querySelector("#results");

        input.addEventListener("input", async () => {
            let query = input.value.trim();
            if (!query) {
                results.innerHTML = "";
                return;
            }

            try {
                let response = await fetch(`https://jsonplaceholder.typicode.com/users`);
                let users = await response.json();

                let filtered = users.filter(user =>
                    user.name.toLowerCase().includes(query.toLowerCase())
                );

                results.innerHTML = filtered
                    .map(user => `<p>${user.name} (${user.email})</p>`)
                    .join("");
            } catch (error) {
                results.innerHTML = "<p>에러 발생</p>";
            }
        });
    </script>
</body>
</html>

2. 로딩 스피너

<!DOCTYPE html>
<html>
<head>
    <style>
        .loading {
            display: none;
        }
        .loading.active {
            display: block;
        }
    </style>
</head>
<body>
    <button id="loadBtn">데이터 로드</button>
    <div class="loading" id="loading">로딩 중...</div>
    <div id="data"></div>

    <script>
        let loadBtn = document.querySelector("#loadBtn");
        let loading = document.querySelector("#loading");
        let dataDiv = document.querySelector("#data");

        loadBtn.addEventListener("click", async () => {
            loading.classList.add("active");
            dataDiv.innerHTML = "";

            try {
                let response = await fetch("https://jsonplaceholder.typicode.com/posts");
                let posts = await response.json();

                dataDiv.innerHTML = posts
                    .slice(0, 10)
                    .map(post => `<h3>${post.title}</h3><p>${post.body}</p>`)
                    .join("");
            } catch (error) {
                dataDiv.innerHTML = "<p>데이터 로드 실패</p>";
            } finally {
                loading.classList.remove("active");
            }
        });
    </script>
</body>
</html>

3. 날씨 앱 (실제 API 사용)

async function getWeather(city) {
    const API_KEY = "your_api_key_here"; // OpenWeatherMap API 키

    try {
        let response = await fetch(
            `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric&lang=kr`
        );

        if (!response.ok) {
            throw new Error("도시를 찾을 수 없습니다");
        }

        let data = await response.json();

        return {
            city: data.name,
            temp: Math.round(data.main.temp),
            description: data.weather[0].description,
            humidity: data.main.humidity
        };
    } catch (error) {
        console.error("날씨 정보 가져오기 실패:", error);
        return null;
    }
}

// 사용
getWeather("Seoul").then(weather => {
    if (weather) {
        console.log(`${weather.city}: ${weather.temp}°C, ${weather.description}`);
    }
});

4. 이미지 갤러리

<!DOCTYPE html>
<html>
<body>
    <button id="loadPhotos">사진 로드</button>
    <div id="gallery"></div>

    <script>
        let loadBtn = document.querySelector("#loadPhotos");
        let gallery = document.querySelector("#gallery");

        loadBtn.addEventListener("click", async () => {
            try {
                let response = await fetch("https://jsonplaceholder.typicode.com/photos");
                let photos = await response.json();

                gallery.innerHTML = photos
                    .slice(0, 20)
                    .map(photo => `
                        <div>
                            <img src="${photo.thumbnailUrl}" alt="${photo.title}">
                            <p>${photo.title}</p>
                        </div>
                    `)
                    .join("");
            } catch (error) {
                gallery.innerHTML = "<p>사진 로드 실패</p>";
            }
        });
    </script>
</body>
</html>

자주 하는 실수

1. await 빠뜨리기

// 틀림
async function getData() {
    let response = fetch(url); // Promise 객체 반환
    console.log(response); // Promise 출력됨
}

// 맞음
async function getData() {
    let response = await fetch(url);
    console.log(response); // 실제 응답 출력됨
}

실행 결과 비교:

// await 없이 (틀림)
Promise { <pending> }
// await 사용 (맞음)
Response { status: 200, ok: true, ... }

await를 빠뜨리면 Promise 객체 자체가 반환되고, await를 사용하면 실제 결과값을 받습니다

2. async 없이 await 사용

// 틀림
function getData() {
    let data = await fetch(url); // 에러!
}

// 맞음
async function getData() {
    let data = await fetch(url);
}

실행 결과:

// async 없이 await 사용 (틀림)
SyntaxError: await is only valid in async functions
// async 함수에서 await 사용 (맞음)
✓ 정상 작동

await는 반드시 async 함수 안에서만 사용할 수 있습니다

3. 에러 처리 안 하기

// 나쁜 예
async function getData() {
    let response = await fetch(url);
    let data = await response.json();
    return data;
}

// 좋은 예
async function getData() {
    try {
        let response = await fetch(url);
        if (!response.ok) throw new Error("HTTP 에러");
        let data = await response.json();
        return data;
    } catch (error) {
        console.error("에러:", error);
        return null;
    }
}

실행 결과 비교:

// 에러 처리 없음 (나쁜 예)
Uncaught (in promise) TypeError: Failed to fetch
→ 앱 전체가 멈춤!
// 에러 처리 있음 (좋은 예)
에러: Error: HTTP 에러
null
→ 앱은 계속 실행됨

에러 처리를 하지 않으면 예상치 못한 에러로 앱이 중단될 수 있습니다. 항상 try-catch로 에러를 처리하세요

다음 단계

JavaScript 독학 가이드 시리즈를 모두 완주하셨어요!

이제 뭘 해야 할까요?

  1. 프로젝트 만들기: To-Do 앱, 날씨 앱, 계산기 등 실제로 만들어보세요
  2. 프레임워크 배우기: React, Vue, 또는 Svelte를 시작해보세요
  3. Node.js: JavaScript로 서버도 만들 수 있어요
  4. TypeScript: JavaScript에 타입을 추가한 언어예요

JavaScript는 계속 발전하고 있어요. 꾸준히 공부하고 실습하면서 성장하세요!


JavaScript 독학 가이드 시리즈:

← 블로그 목록으로