6장: 비동기 처리

📋 강의 개요


📚 6.1 동기 vs 비동기

강의 포인트

동기 (Synchronous) 실행

console.log('1. 첫 번째');
console.log('2. 두 번째');
console.log('3. 세 번째');

// 출력:
// 1. 첫 번째
// 2. 두 번째
// 3. 세 번째
// → 순서대로 실행

특징:

비동기 (Asynchronous) 실행

console.log('1. 시작');

setTimeout(() => {
    console.log('2. 2초 후 실행');
}, 2000);

console.log('3. 끝');

// 출력:
// 1. 시작
// 3. 끝
// 2. 2초 후 실행 (2초 후)
// → 순서가 바뀜!

특징:

비동기가 필요한 경우

// 1. 서버 통신 (시간이 오래 걸림)
fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => console.log(data));

// 2. 타이머
setTimeout(() => console.log('1초 후'), 1000);

// 3. 파일 읽기/쓰기
readFile('data.txt', (data) => console.log(data));

// 4. 사용자 입력 대기
button.addEventListener('click', () => console.log('클릭!'));

이벤트 루프 (Event Loop)

Call Stack (호출 스택)
  ↓
현재 실행 중인 함수
  ↓
Web APIs (setTimeout, fetch 등)
  ↓
Task Queue (작업 대기열)
  ↓
Event Loop (끊임없이 확인)
  ↓
Call Stack이 비면 Queue에서 가져옴

📚 6.2-6.3 콜백 함수와 콜백 지옥

강의 포인트

콜백 함수 기본

// 콜백 함수 = 다른 함수에 인자로 전달되는 함수
function greeting(name, callback) {
    console.log(`안녕, ${name}`);
    callback();
}

greeting('홍길동', function() {
    console.log('콜백 실행!');
});

// 출력:
// 안녕, 홍길동
// 콜백 실행!

비동기 콜백

console.log('시작');

setTimeout(function() {
    console.log('1초 후 실행');
}, 1000);

console.log('');

// 출력:
// 시작
// 끝
// 1초 후 실행 (1초 후)

콜백 지옥 (Callback Hell) 😱

// 사용자 정보 → 게시글 목록 → 첫 번째 게시글 → 댓글 목록
getUser(userId, function(user) {
    getPosts(user.id, function(posts) {
        getPost(posts[0].id, function(post) {
            getComments(post.id, function(comments) {
                console.log(comments);
                // 계단식 구조... 읽기 어려움 😢
            });
        });
    });
});

문제점:

  1. 가독성이 나쁨 (피라미드 구조)
  2. 에러 처리가 어려움
  3. 유지보수가 어려움
  4. 디버깅이 어려움

콜백 에러 처리

function loadData(callback) {
    setTimeout(() => {
        const error = Math.random() > 0.5 ? new Error('에러 발생!') : null;
        const data = { name: '홍길동' };
        
        // Node.js 스타일 콜백 (error-first)
        callback(error, data);
    }, 1000);
}

loadData((err, data) => {
    if (err) {
        console.error('에러:', err.message);
        return;
    }
    console.log('데이터:', data);
});

📚 6.4-6.6 Promise

강의 포인트

Promise 기본

// Promise 생성
const promise = new Promise((resolve, reject) => {
    // 비동기 작업
    setTimeout(() => {
        const success = true;
        
        if (success) {
            resolve('성공!');  // 성공 시
        } else {
            reject('실패!');   // 실패 시
        }
    }, 1000);
});

// Promise 사용
promise
    .then(result => {
        console.log(result);  // "성공!"
    })
    .catch(error => {
        console.error(error);  // "실패!"
    })
    .finally(() => {
        console.log('완료');  // 성공/실패 관계없이 실행
    });

Promise의 3가지 상태

// 1. Pending (대기) - 초기 상태
const promise1 = new Promise((resolve, reject) => {
    // 아직 resolve/reject 안 함
});

// 2. Fulfilled (이행) - 성공
const promise2 = new Promise((resolve, reject) => {
    resolve('성공!');
});

// 3. Rejected (거부) - 실패
const promise3 = new Promise((resolve, reject) => {
    reject('실패!');
});

Promise 체이닝

// 콜백 지옥을 Promise로 해결!
getUser(userId)
    .then(user => getPosts(user.id))
    .then(posts => getPost(posts[0].id))
    .then(post => getComments(post.id))
    .then(comments => console.log(comments))
    .catch(error => console.error(error));

// 훨씬 읽기 쉬움! ✨

Promise 체이닝 상세

new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000);
})
.then(result => {
    console.log(result);  // 1
    return result * 2;
})
.then(result => {
    console.log(result);  // 2
    return result * 2;
})
.then(result => {
    console.log(result);  // 4
});

// 출력:
// 1 (1초 후)
// 2
// 4

Promise 에러 처리

fetch('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error('HTTP 에러');
        }
        return response.json();
    })
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        // 모든 에러를 여기서 처리
        console.error('에러 발생:', error);
    })
    .finally(() => {
        console.log('완료');
    });

Promise.all() - 모든 Promise 완료 대기

const promise1 = fetch('https://api.example.com/user/1');
const promise2 = fetch('https://api.example.com/user/2');
const promise3 = fetch('https://api.example.com/user/3');

// 모든 Promise가 완료될 때까지 대기
Promise.all([promise1, promise2, promise3])
    .then(responses => Promise.all(responses.map(r => r.json())))
    .then(users => {
        console.log(users);  // [user1, user2, user3]
    })
    .catch(error => {
        // 하나라도 실패하면 catch 실행
        console.error(error);
    });

특징:

Promise.race() - 가장 빠른 Promise만

const promise1 = new Promise(resolve => setTimeout(() => resolve('1초'), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve('2초'), 2000));
const promise3 = new Promise(resolve => setTimeout(() => resolve('3초'), 3000));

Promise.race([promise1, promise2, promise3])
    .then(result => {
        console.log(result);  // "1초" (가장 빠른 것만)
    });

사용 예: 타임아웃 구현

function fetchWithTimeout(url, timeout = 5000) {
    return Promise.race([
        fetch(url),
        new Promise((_, reject) => 
            setTimeout(() => reject(new Error('타임아웃')), timeout)
        )
    ]);
}

Promise.allSettled() - 모든 결과 확인

const promises = [
    Promise.resolve('성공 1'),
    Promise.reject('실패'),
    Promise.resolve('성공 2')
];

Promise.allSettled(promises)
    .then(results => {
        console.log(results);
        // [
        //   { status: 'fulfilled', value: '성공 1' },
        //   { status: 'rejected', reason: '실패' },
        //   { status: 'fulfilled', value: '성공 2' }
        // ]
    });

특징:

Promise.any() - 첫 번째 성공한 Promise

const promises = [
    Promise.reject('실패 1'),
    Promise.resolve('성공!'),
    Promise.reject('실패 2')
];

Promise.any(promises)
    .then(result => {
        console.log(result);  // "성공!"
    });

📚 6.7-6.8 async / await

강의 포인트

async 함수 기본

// async 함수는 항상 Promise를 반환
async function myFunction() {
    return '안녕';
}

myFunction().then(result => console.log(result));  // "안녕"

// 위와 동일
function myFunction2() {
    return Promise.resolve('안녕');
}

await - Promise 완료 대기

async function fetchData() {
    // await은 Promise가 완료될 때까지 대기
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    
    console.log(data);
}

// Promise 체이닝과 비교
function fetchData2() {
    return fetch('https://api.example.com/data')
        .then(response => response.json())
        .then(data => console.log(data));
}

순차 실행 vs 병렬 실행

// ❌ 순차 실행 (느림)
async function sequential() {
    const user1 = await fetchUser(1);  // 1초
    const user2 = await fetchUser(2);  // 1초
    const user3 = await fetchUser(3);  // 1초
    // 총 3초 걸림
}

// ✅ 병렬 실행 (빠름)
async function parallel() {
    const [user1, user2, user3] = await Promise.all([
        fetchUser(1),
        fetchUser(2),
        fetchUser(3)
    ]);
    // 총 1초 걸림 (동시 실행)
}

try-catch 에러 처리

async function fetchData() {
    try {
        const response = await fetch('https://api.example.com/data');
        
        if (!response.ok) {
            throw new Error('HTTP 에러');
        }
        
        const data = await response.json();
        console.log(data);
        
    } catch (error) {
        console.error('에러 발생:', error.message);
        
    } finally {
        console.log('완료');
    }
}

실전 예제

// 사용자 정보 → 게시글 목록 → 댓글 목록
async function getUserData(userId) {
    try {
        // 순차 실행 (의존 관계가 있음)
        const user = await getUser(userId);
        const posts = await getPosts(user.id);
        const firstPost = await getPost(posts[0].id);
        const comments = await getComments(firstPost.id);
        
        return {
            user,
            posts,
            firstPost,
            comments
        };
        
    } catch (error) {
        console.error('에러:', error);
        throw error;
    }
}

// 사용
getUserData(1)
    .then(data => console.log(data))
    .catch(error => console.error(error));

async/await 팁

1. forEach에서는 await가 작동 안 함!

// ❌ 작동 안 함
async function processItems(items) {
    items.forEach(async (item) => {
        await processItem(item);  // 대기 안 함!
    });
}

// ✅ for...of 사용
async function processItems(items) {
    for (const item of items) {
        await processItem(item);  // 정상 대기
    }
}

// ✅ Promise.all 사용 (병렬)
async function processItems(items) {
    await Promise.all(
        items.map(item => processItem(item))
    );
}

2. Top-level await (최신)

// ES2022부터 모듈에서 가능
const data = await fetch('https://api.example.com/data');
console.log(data);

📚 6.9-6.10 Fetch API

강의 포인트

기본 GET 요청

fetch('https://api.example.com/users')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error(error));

// async/await 버전
async function getUsers() {
    try {
        const response = await fetch('https://api.example.com/users');
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error(error);
    }
}

POST 요청

async function createUser(userData) {
    try {
        const response = await fetch('https://api.example.com/users', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(userData)
        });
        
        if (!response.ok) {
            throw new Error('HTTP 에러: ' + response.status);
        }
        
        const data = await response.json();
        return data;
        
    } catch (error) {
        console.error('에러:', error);
        throw error;
    }
}

// 사용
createUser({
    name: '홍길동',
    email: 'hong@example.com'
}).then(user => console.log('생성됨:', user));

PUT 요청 (전체 수정)

async function updateUser(userId, userData) {
    const response = await fetch(`https://api.example.com/users/${userId}`, {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(userData)
    });
    
    return response.json();
}

PATCH 요청 (부분 수정)

async function patchUser(userId, updates) {
    const response = await fetch(`https://api.example.com/users/${userId}`, {
        method: 'PATCH',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(updates)
    });
    
    return response.json();
}

DELETE 요청

async function deleteUser(userId) {
    const response = await fetch(`https://api.example.com/users/${userId}`, {
        method: 'DELETE'
    });
    
    if (response.ok) {
        console.log('삭제 완료');
    }
}

Response 객체

const response = await fetch('https://api.example.com/users');

// 상태 확인
console.log(response.ok);        // true (200-299)
console.log(response.status);    // 200
console.log(response.statusText); // "OK"

// 헤더
console.log(response.headers.get('Content-Type'));

// 본문 파싱
const json = await response.json();     // JSON
const text = await response.text();     // 텍스트
const blob = await response.blob();     // Blob (파일)
const buffer = await response.arrayBuffer();  // ArrayBuffer

에러 처리

async function fetchData(url) {
    try {
        const response = await fetch(url);
        
        // fetch는 HTTP 에러에서 reject 안 됨!
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        const data = await response.json();
        return data;
        
    } catch (error) {
        // 네트워크 에러 또는 위에서 throw한 에러
        console.error('Fetch 에러:', error.message);
        throw error;
    }
}

요청 옵션

fetch('https://api.example.com/data', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token123'
    },
    body: JSON.stringify({ data: 'value' }),
    mode: 'cors',           // cors, no-cors, same-origin
    credentials: 'include', // include, same-origin, omit
    cache: 'no-cache',      // default, no-cache, reload, force-cache
    redirect: 'follow',     // follow, error, manual
    referrerPolicy: 'no-referrer'
});

📚 6.11 FormData 전송

파일 업로드

const form = document.querySelector('#uploadForm');

form.addEventListener('submit', async (e) => {
    e.preventDefault();
    
    const formData = new FormData(form);
    
    // 추가 데이터
    formData.append('userId', '123');
    
    try {
        const response = await fetch('/upload', {
            method: 'POST',
            body: formData  // Content-Type 자동 설정
        });
        
        const result = await response.json();
        console.log('업로드 완료:', result);
        
    } catch (error) {
        console.error('업로드 실패:', error);
    }
});

📚 6.12-6.13 AJAX와 axios

XMLHttpRequest (구식)

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');

xhr.onload = function() {
    if (xhr.status === 200) {
        console.log(JSON.parse(xhr.responseText));
    }
};

xhr.onerror = function() {
    console.error('에러');
};

xhr.send();

axios 라이브러리 ⭐

// CDN
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

// GET
axios.get('https://api.example.com/users')
    .then(response => console.log(response.data))
    .catch(error => console.error(error));

// POST
axios.post('https://api.example.com/users', {
    name: '홍길동',
    email: 'hong@example.com'
})
.then(response => console.log(response.data));

// async/await
async function getUsers() {
    try {
        const response = await axios.get('https://api.example.com/users');
        console.log(response.data);
    } catch (error) {
        console.error(error.response.data);
    }
}

axios 장점:


🎯 강의 진행 팁

1-2교시: 동기/비동기, 콜백 (2시간)

3-4교시: Promise (2시간)

5-6교시: async/await (2시간)

7-8교시: Fetch API (2시간)


📝 종합 프로젝트: 게시판 앱

// 사용자 API
class UserAPI {
    static async getUsers() {
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        return response.json();
    }
    
    static async getUser(id) {
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
        return response.json();
    }
}

// 게시글 API
class PostAPI {
    static async getPosts() {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts');
        return response.json();
    }
    
    static async createPost(post) {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(post)
        });
        return response.json();
    }
}

// 사용
async function displayPosts() {
    try {
        const posts = await PostAPI.getPosts();
        posts.slice(0, 10).forEach(post => {
            console.log(`${post.id}. ${post.title}`);
        });
    } catch (error) {
        console.error('에러:', error);
    }
}

6장 비동기 처리를 완료했습니다! ⏱️