Skip to content

Latest commit

 

History

History
848 lines (691 loc) · 19.1 KB

File metadata and controls

848 lines (691 loc) · 19.1 KB

03. 비동기 프로그래밍 (Async Programming)

1. 동기 vs 비동기

동기 (Synchronous)

코드가 순차적으로 실행되며, 이전 작업이 완료될 때까지 다음 작업이 대기합니다.

console.log("1번 작업");
console.log("2번 작업");
console.log("3번 작업");
// 출력: 1번 작업 → 2번 작업 → 3번 작업 (순서대로)

비동기 (Asynchronous)

시간이 걸리는 작업을 백그라운드에서 처리하고, 다른 코드를 계속 실행합니다.

console.log("1번 작업");

setTimeout(() => {
  console.log("2번 작업 (1초 후)");
}, 1000);

console.log("3번 작업");

// 출력: 1번 작업 → 3번 작업 → 2번 작업 (1초 후)

왜 비동기가 필요한가?

// ❌ 동기 방식 (가상 코드 - 실제로는 fetch가 비동기)
const data = fetchDataFromServer(); // 3초 걸림
console.log("데이터를 받았습니다."); // 3초 동안 대기

// ✅ 비동기 방식
fetchDataFromServer().then(data => {
  console.log("데이터를 받았습니다.");
});
console.log("다른 작업 계속 진행..."); // 즉시 실행

2. 콜백 함수 (Callback)

비동기 작업의 가장 기본적인 패턴입니다.

// setTimeout - 일정 시간 후 실행
setTimeout(() => {
  console.log("1초 후 실행");
}, 1000);

// 콜백 함수 예제
function fetchUser(id, callback) {
  setTimeout(() => {
    const user = { id, name: "김철수" };
    callback(user);
  }, 1000);
}

fetchUser(1, (user) => {
  console.log(user); // { id: 1, name: "김철수" }
});

콜백 지옥 (Callback Hell) ❌

콜백이 중첩되면 코드가 복잡해집니다.

// 사용자 정보 → 게시글 정보 → 댓글 정보 순차 조회
fetchUser(1, (user) => {
  console.log("사용자:", user);
  fetchPosts(user.id, (posts) => {
    console.log("게시글:", posts);
    fetchComments(posts[0].id, (comments) => {
      console.log("댓글:", comments);
      // 너무 깊게 중첩됨... 😱
    });
  });
});

3. Promise ⭐

콜백 지옥을 해결하기 위한 패턴입니다.

Promise 기본 개념

Promise는 3가지 상태를 가집니다:

  • Pending (대기): 아직 완료되지 않은 상태
  • Fulfilled (이행): 성공적으로 완료된 상태
  • Rejected (거부): 실패한 상태
// 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);
  });

Promise 체이닝

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

function fetchPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, title: "첫 번째 글" },
        { id: 2, title: "두 번째 글" }
      ]);
    }, 1000);
  });
}

// ✅ Promise 체이닝으로 깔끔하게 처리
fetchUser(1)
  .then((user) => {
    console.log("사용자:", user);
    return fetchPosts(user.id);
  })
  .then((posts) => {
    console.log("게시글:", posts);
    return posts[0];
  })
  .then((firstPost) => {
    console.log("첫 번째 게시글:", firstPost);
  })
  .catch((error) => {
    console.error("에러 발생:", error);
  });

Promise 정적 메서드

// Promise.resolve - 즉시 성공한 Promise 생성
const resolved = Promise.resolve(42);
resolved.then(value => console.log(value)); // 42

// Promise.reject - 즉시 실패한 Promise 생성
const rejected = Promise.reject("에러");
rejected.catch(error => console.log(error)); // "에러"

// Promise.all - 모든 Promise가 성공해야 성공
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log(results); // [1, 2, 3]
  });

// 하나라도 실패하면 실패
Promise.all([
  Promise.resolve(1),
  Promise.reject("에러"),
  Promise.resolve(3)
])
  .then(results => console.log(results))
  .catch(error => console.log("실패:", error)); // "실패: 에러"

// Promise.race - 가장 먼저 완료된 것을 반환
Promise.race([
  new Promise(resolve => setTimeout(() => resolve("느림"), 1000)),
  new Promise(resolve => setTimeout(() => resolve("빠름"), 500))
])
  .then(result => console.log(result)); // "빠름"

// Promise.allSettled - 모든 Promise 결과 반환 (실패해도)
Promise.allSettled([
  Promise.resolve(1),
  Promise.reject("에러"),
  Promise.resolve(3)
])
  .then(results => console.log(results));
// [
//   { status: "fulfilled", value: 1 },
//   { status: "rejected", reason: "에러" },
//   { status: "fulfilled", value: 3 }
// ]

4. async/await ⭐⭐ 가장 많이 사용

Promise를 더 간결하고 읽기 쉽게 만들어줍니다.

기본 사용법

// Promise 방식
function getUserPromise() {
  return fetchUser(1)
    .then(user => {
      console.log(user);
      return user;
    })
    .catch(error => {
      console.error(error);
    });
}

// async/await 방식 (훨씬 간결!)
async function getUserAsync() {
  try {
    const user = await fetchUser(1);
    console.log(user);
    return user;
  } catch (error) {
    console.error(error);
  }
}

// async 함수는 항상 Promise를 반환
getUserAsync().then(user => {
  console.log("완료:", user);
});

순차 실행

async function fetchAllData() {
  try {
    // 순차적으로 실행 (하나씩)
    const user = await fetchUser(1);
    console.log("사용자:", user);

    const posts = await fetchPosts(user.id);
    console.log("게시글:", posts);

    const comments = await fetchComments(posts[0].id);
    console.log("댓글:", comments);

    return { user, posts, comments };
  } catch (error) {
    console.error("에러 발생:", error);
    throw error; // 에러 전파
  }
}

fetchAllData();

병렬 실행

// ❌ 비효율적 (순차 실행 - 총 3초)
async function fetchSequential() {
  const user1 = await fetchUser(1);  // 1초
  const user2 = await fetchUser(2);  // 1초
  const user3 = await fetchUser(3);  // 1초
  return [user1, user2, user3];
}

// ✅ 효율적 (병렬 실행 - 총 1초)
async function fetchParallel() {
  const [user1, user2, user3] = await Promise.all([
    fetchUser(1),
    fetchUser(2),
    fetchUser(3)
  ]);
  return [user1, user2, user3];
}

// 또는
async function fetchParallel2() {
  const promise1 = fetchUser(1);
  const promise2 = fetchUser(2);
  const promise3 = fetchUser(3);

  const user1 = await promise1;
  const user2 = await promise2;
  const user3 = await promise3;

  return [user1, user2, user3];
}

5. Fetch API ⭐⭐ 실전 필수

브라우저에서 HTTP 요청을 보내는 표준 방법입니다.

GET 요청

// 기본 GET 요청
async function getUsers() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");

    // 응답 상태 확인
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const users = await response.json(); // JSON 파싱
    console.log(users);
    return users;
  } catch (error) {
    console.error("데이터 조회 실패:", error);
  }
}

getUsers();

// 특정 사용자 조회
async function getUser(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);

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

    const user = await response.json();
    return user;
  } catch (error) {
    console.error("에러:", error.message);
  }
}

getUser(1);

POST 요청 (데이터 생성)

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

    if (!response.ok) {
      throw new Error("사용자 생성 실패");
    }

    const newUser = await response.json();
    console.log("생성된 사용자:", newUser);
    return newUser;
  } catch (error) {
    console.error("에러:", error);
  }
}

// 사용 예제
createUser({
  name: "김철수",
  email: "kim@example.com",
  age: 25
});

PUT 요청 (데이터 수정)

async function updateUser(id, userData) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(userData)
    });

    if (!response.ok) {
      throw new Error("사용자 수정 실패");
    }

    const updatedUser = await response.json();
    console.log("수정된 사용자:", updatedUser);
    return updatedUser;
  } catch (error) {
    console.error("에러:", error);
  }
}

// 사용 예제
updateUser(1, {
  name: "박영희",
  email: "park@example.com",
  age: 30
});

PATCH 요청 (일부 데이터 수정)

async function patchUser(id, partialData) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(partialData)
    });

    if (!response.ok) {
      throw new Error("사용자 수정 실패");
    }

    const updatedUser = await response.json();
    console.log("수정된 사용자:", updatedUser);
    return updatedUser;
  } catch (error) {
    console.error("에러:", error);
  }
}

// 사용 예제 (이름만 수정)
patchUser(1, { name: "이민수" });

DELETE 요청 (데이터 삭제)

async function deleteUser(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
      method: "DELETE"
    });

    if (!response.ok) {
      throw new Error("사용자 삭제 실패");
    }

    console.log(`사용자 ${id} 삭제 완료`);
    return true;
  } catch (error) {
    console.error("에러:", error);
    return false;
  }
}

// 사용 예제
deleteUser(1);

6. JSON 다루기

JavaScript Object Notation - 데이터 교환 형식입니다.

// JavaScript 객체
const user = {
  name: "김철수",
  age: 25,
  hobbies: ["독서", "영화감상"]
};

// 객체 → JSON 문자열 (JSON.stringify)
const jsonString = JSON.stringify(user);
console.log(jsonString);
// {"name":"김철수","age":25,"hobbies":["독서","영화감상"]}

// 예쁘게 포맷팅 (들여쓰기 2칸)
const prettyJson = JSON.stringify(user, null, 2);
console.log(prettyJson);
// {
//   "name": "김철수",
//   "age": 25,
//   "hobbies": ["독서", "영화감상"]
// }

// JSON 문자열 → JavaScript 객체 (JSON.parse)
const parsed = JSON.parse(jsonString);
console.log(parsed.name); // "김철수"

// 에러 처리
try {
  const invalid = JSON.parse("잘못된 JSON");
} catch (error) {
  console.error("JSON 파싱 에러:", error.message);
}

// localStorage와 함께 사용
const data = { theme: "dark", fontSize: 16 };
localStorage.setItem("settings", JSON.stringify(data));
const loaded = JSON.parse(localStorage.getItem("settings"));
console.log(loaded); // { theme: "dark", fontSize: 16 }

7. 실전 예제: API 통합

완전한 CRUD 예제

const API_BASE = "https://jsonplaceholder.typicode.com";

// API 헬퍼 함수
async function apiRequest(endpoint, options = {}) {
  try {
    const response = await fetch(`${API_BASE}${endpoint}`, {
      headers: {
        "Content-Type": "application/json",
        ...options.headers
      },
      ...options
    });

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

    // 204 No Content인 경우 (DELETE 등)
    if (response.status === 204) {
      return null;
    }

    return await response.json();
  } catch (error) {
    console.error("API 요청 실패:", error);
    throw error;
  }
}

// CRUD 함수들
const userAPI = {
  // Create
  create: async (userData) => {
    return apiRequest("/users", {
      method: "POST",
      body: JSON.stringify(userData)
    });
  },

  // Read (전체)
  getAll: async () => {
    return apiRequest("/users");
  },

  // Read (단일)
  getById: async (id) => {
    return apiRequest(`/users/${id}`);
  },

  // Update
  update: async (id, userData) => {
    return apiRequest(`/users/${id}`, {
      method: "PUT",
      body: JSON.stringify(userData)
    });
  },

  // Patch
  patch: async (id, partialData) => {
    return apiRequest(`/users/${id}`, {
      method: "PATCH",
      body: JSON.stringify(partialData)
    });
  },

  // Delete
  delete: async (id) => {
    return apiRequest(`/users/${id}`, {
      method: "DELETE"
    });
  }
};

// 사용 예제
async function example() {
  try {
    // 전체 사용자 조회
    const users = await userAPI.getAll();
    console.log("전체 사용자:", users.length);

    // 특정 사용자 조회
    const user = await userAPI.getById(1);
    console.log("사용자 1:", user.name);

    // 새 사용자 생성
    const newUser = await userAPI.create({
      name: "김철수",
      email: "kim@example.com"
    });
    console.log("생성된 사용자:", newUser);

    // 사용자 수정
    const updated = await userAPI.update(1, {
      name: "박영희",
      email: "park@example.com"
    });
    console.log("수정된 사용자:", updated);

    // 사용자 삭제
    await userAPI.delete(1);
    console.log("삭제 완료");

  } catch (error) {
    console.error("작업 실패:", error);
  }
}

example();

여러 API 요청 병렬 처리

async function fetchDashboardData() {
  try {
    // 병렬로 여러 API 호출
    const [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.length}명`);
    console.log(`게시글: ${posts.length}개`);
    console.log(`댓글: ${comments.length}개`);

    return { users, posts, comments };
  } catch (error) {
    console.error("대시보드 데이터 로딩 실패:", error);
  }
}

fetchDashboardData();

재시도 로직 (Retry Logic)

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      console.log(`시도 ${i + 1}/${maxRetries} 실패:`, error.message);

      if (i === maxRetries - 1) {
        throw new Error(`${maxRetries}번 시도 후 실패`);
      }

      // 지수 백오프 (exponential backoff)
      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
    }
  }
}

// 사용 예제
fetchWithRetry("https://jsonplaceholder.typicode.com/users")
  .then(data => console.log("성공:", data))
  .catch(error => console.error("최종 실패:", error));

타임아웃 추가

async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    clearTimeout(timeoutId);

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

    return await response.json();
  } catch (error) {
    if (error.name === "AbortError") {
      throw new Error("요청 시간 초과");
    }
    throw error;
  }
}

// 사용 예제
fetchWithTimeout("https://jsonplaceholder.typicode.com/users", 3000)
  .then(data => console.log("성공:", data))
  .catch(error => console.error("에러:", error.message));

8. 에러 처리 패턴

try-catch with async/await

async function safeFetch() {
  try {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

// 사용
const result = await safeFetch();
if (result.success) {
  console.log("데이터:", result.data);
} else {
  console.error("에러:", result.error);
}

finally 블록

async function fetchWithLoading() {
  let isLoading = true;
  console.log("로딩 시작...");

  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    const data = await response.json();
    console.log("데이터 로드 완료");
    return data;
  } catch (error) {
    console.error("에러 발생:", error);
  } finally {
    // 성공하든 실패하든 항상 실행
    isLoading = false;
    console.log("로딩 종료");
  }
}

fetchWithLoading();

실습 과제

exercises/03-async-practice.js 파일을 생성하고 다음을 구현해보세요:

1. Promise 연습

// TODO: 1초 후에 랜덤 숫자(1~100)를 반환하는 Promise 함수 작성
// TODO: 숫자가 50 이상이면 resolve, 미만이면 reject

function getRandomNumber() {
  // 여기에 코드 작성
}

// TODO: then-catch로 처리하기

2. async/await 연습

// TODO: JSONPlaceholder API에서 사용자 목록 가져오기
// URL: https://jsonplaceholder.typicode.com/users

async function fetchUsers() {
  // 여기에 코드 작성
}

// TODO: 사용자 이름만 추출하여 배열로 반환
// TODO: try-catch로 에러 처리

3. 병렬 요청 연습

// TODO: 여러 사용자의 정보를 동시에 가져오기
// URL: https://jsonplaceholder.typicode.com/users/[id]

async function fetchMultipleUsers(ids) {
  // Promise.all 사용
  // 예: ids = [1, 2, 3]
}

// TODO: 모든 사용자의 이름을 배열로 반환

4. CRUD 실습

// TODO: 게시글 API 함수 작성
// Base URL: https://jsonplaceholder.typicode.com/posts

const postAPI = {
  // TODO: 모든 게시글 조회
  getAll: async () => {
    // 여기에 코드 작성
  },

  // TODO: 특정 게시글 조회
  getById: async (id) => {
    // 여기에 코드 작성
  },

  // TODO: 새 게시글 생성
  create: async (postData) => {
    // 여기에 코드 작성
  },

  // TODO: 게시글 삭제
  delete: async (id) => {
    // 여기에 코드 작성
  }
};

5. 종합 문제

// TODO: 사용자 ID를 받아서 해당 사용자의 정보와 게시글을 함께 반환하는 함수
// 1. 사용자 정보 가져오기: /users/{id}
// 2. 사용자의 게시글 가져오기: /posts?userId={id}
// 3. 두 정보를 합쳐서 반환

async function getUserWithPosts(userId) {
  // 여기에 코드 작성
  // 반환 형식: { user: {...}, posts: [...] }
}