동기/비동기/Promise 완전 정복 가이드

oversleep·2025년 8월 14일

개발하다 보면 계속 헷갈리는 동기/비동기/Promise를 실생활 예시로 쉽게 정리해보겠습니다!

📚 목차

  1. 동기 vs 비동기 기본 개념
  2. Promise가 뭔가요?
  3. 실제 코드로 보는 차이점
  4. React Query에서의 실전 적용
  5. 자주 하는 실수와 해결법

동기 vs 비동기 기본 개념

🏪 편의점에서 물건 사기 (동기)

// 동기 함수 - 즉석에서 바로 처리
function 편의점에서물건사기(돈) {
  if (돈 >= 1000) {
    return "🍫 초콜릿";
  } else {
    return "💸 돈이 부족해요";
  }
}

const 결과 = 편의점에서물건사기(1500);
console.log(결과); // "🍫 초콜릿" - 즉시 나옴!
console.log("다음 일 진행"); // 바로 다음 줄 실행

특징:

  • 즉시 실행, 즉시 결과
  • 기다릴 필요 없음
  • 순서대로 실행됨

🍕 피자 배달 주문 (비동기)

// 비동기 함수 - 시간이 걸림
function 피자주문하기(메뉴) {
  console.log("주문 접수!");
  
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (메뉴 === "페퍼로니") {
        resolve("🍕 페퍼로니 피자 도착!");
      } else {
        reject("😭 재료가 떨어졌어요");
      }
    }, 3000); // 3초 후 결과
  });
}

// ❌ 잘못된 사용법
const 피자 = 피자주문하기("페퍼로니");
console.log(피자); // Promise 객체만 나옴 (피자가 아님!)

// ✅ 올바른 사용법
피자주문하기("페퍼로니")
  .then(피자 => console.log(피자)) // "🍕 페퍼로니 피자 도착!"
  .catch(에러 => console.log(에러));

// 또는 async/await 사용
async function 피자먹기() {
  try {
    const 피자 = await 피자주문하기("페퍼로니");
    console.log(피자); // "🍕 페퍼로니 피자 도착!"
  } catch (에러) {
    console.log(에러); // "😭 재료가 떨어졌어요"
  }
}

특징:

  • 시간이 걸림
  • Promise 객체를 반환
  • then/catch 또는 async/await로 처리

Promise가 뭔가요?

🎫 영화표 예매 시스템으로 이해하기

// Promise는 "약속표"같은 것
function 영화표예매(영화명) {
  return new Promise((resolve, reject) => {
    console.log("예매 진행 중...");
    
    setTimeout(() => {
      const 예매성공 = Math.random() > 0.3; // 70% 확률로 성공
      
      if (예매성공) {
        resolve(`🎬 ${영화명} 예매 완료!`);
      } else {
        reject(`😞 ${영화명} 매진되었습니다`);
      }
    }, 2000);
  });
}

Promise의 3가지 상태

const 예매약속 = 영화표예매("아바타");

// 1. Pending (대기): 아직 결과 모름
console.log(예매약속); // Promise { <pending> }

// 2. Fulfilled (성공): resolve 호출됨
예매약속.then(결과 => {
  console.log(결과); // "🎬 아바타 예매 완료!"
});

// 3. Rejected (실패): reject 호출됨
예매약속.catch(에러 => {
  console.log(에러); // "😞 아바타 매진되었습니다"
});

체이닝으로 연속 작업

영화표예매("어벤져스")
  .then(예매결과 => {
    console.log(예매결과);
    return 팝콘사기(); // 다음 Promise 반환
  })
  .then(팝콘결과 => {
    console.log(팝콘결과);
    return 영화관가기();
  })
  .then(도착결과 => {
    console.log("영화 관람 준비 완료!");
  })
  .catch(에러 => {
    console.log("어딘가에서 문제 발생:", 에러);
  });

실제 코드로 보는 차이점

API 호출 예시

// 동기식 (실제로는 불가능, 예시용)
function getUserSync(userId) {
  // 만약 동기식이라면...
  const user = fetch(`/api/users/${userId}`); // 즉시 완료
  return user;
}

// 비동기식 (실제 방식)
function getUserAsync(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json());
}

// 사용법 비교
// ❌ 동기식처럼 사용하면 안됨
const user = getUserAsync(123);
console.log(user.name); // Error! user는 Promise

// ✅ 비동기식 올바른 사용
getUserAsync(123)
  .then(user => {
    console.log(user.name); // 정상 동작
  });

// ✅ async/await 사용
async function 사용자정보가져오기() {
  const user = await getUserAsync(123);
  console.log(user.name); // 정상 동작
}

여러 API 동시 호출

// 순차 실행 (느림)
async function 순차실행() {
  const user = await getUserAsync(1);
  const posts = await getPostsAsync(1);
  const comments = await getCommentsAsync(1);
  
  return { user, posts, comments };
}

// 병렬 실행 (빠름)
async function 병렬실행() {
  const [user, posts, comments] = await Promise.all([
    getUserAsync(1),
    getPostsAsync(1),
    getCommentsAsync(1)
  ]);
  
  return { user, posts, comments };
}

React Query에서의 실전 적용

mutate vs mutateAsync 차이

// mutate (동기): 즉시 반환, 결과를 기다리지 않음
const { mutate: createUser } = useCreateUser({
  onSuccess: (data) => {
    console.log("성공!", data);
  },
  onError: (error) => {
    console.log("실패!", error);
  }
});

function 버튼클릭() {
  createUser({ name: "홍길동" }); // 즉시 완료
  console.log("다음 코드 실행"); // 바로 실행됨
  // API 결과는 onSuccess/onError에서 처리
}

// mutateAsync (비동기): Promise 반환, 결과를 기다림
const { mutateAsync: createUserAsync } = useCreateUser();

async function 버튼클릭() {
  try {
    const result = await createUserAsync({ name: "홍길동" });
    console.log("성공!", result); // API 완료 후 실행
    // 성공 후 처리 로직
  } catch (error) {
    console.log("실패!", error);
    // 실패 후 처리 로직
  }
}

실제 모달 컴포넌트 예시

// ❌ 문제가 있는 코드
function Modal({ onSubmit }) {
  const handleSubmit = async () => {
    await onSubmit(formData); // onSubmit이 동기함수면 즉시 완료
    showSuccessToast(); // API 결과와 상관없이 성공 토스트
    closeModal();
  };
}

// ✅ 해결책 1: 모달에서는 검증만, 결과 처리는 React Query에서
function Modal({ onSubmit }) {
  const handleSubmit = () => {
    if (validateForm()) {
      onSubmit(formData); // 그냥 호출하고 끝
      closeModal();
    }
  };
}

// ✅ 해결책 2: mutateAsync 사용하여 실제로 기다리기
const { mutateAsync: submitData } = useSubmitData();

function Modal() {
  const handleSubmit = async () => {
    try {
      await submitData(formData); // 실제 API 완료까지 기다림
      showSuccessToast();
      closeModal();
    } catch (error) {
      showErrorToast(error.message);
    }
  };
}

자주 하는 실수와 해결법

실수 1: Promise를 동기처럼 사용

// ❌ 잘못된 방법
function 데이터가져오기() {
  const data = fetch('/api/data'); // Promise 객체
  return data.name; // Error! data는 Promise
}

// ✅ 올바른 방법
async function 데이터가져오기() {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data.name;
}

실수 2: async/await와 .then() 혼용

// ❌ 혼란스러운 코드
async function 혼란스러운함수() {
  const result = await fetch('/api/data')
    .then(res => res.json())
    .then(data => data.name);
  return result;
}

// ✅ 일관성 있는 코드 (async/await)
async function 깔끔한함수() {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data.name;
}

// ✅ 일관성 있는 코드 (.then())
function 깔끔한함수2() {
  return fetch('/api/data')
    .then(res => res.json())
    .then(data => data.name);
}

실수 3: 에러 처리 빼먹기

// ❌ 에러 처리 없음
async function 위험한함수() {
  const data = await fetch('/api/data'); // 실패하면 에러 발생
  return data.json();
}

// ✅ 에러 처리 포함
async function 안전한함수() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error('API 호출 실패');
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('데이터 가져오기 실패:', error);
    throw error; // 필요에 따라 다시 던지기
  }
}

정리하면...

🎯 핵심 포인트

  1. 동기함수: 즉시 실행, 즉시 결과 → return 값
  2. 비동기함수: 나중에 결과 → return Promise
  3. Promise: "나중에 결과 줄게"라는 약속표
  4. await: Promise의 결과가 나올 때까지 기다리기
  5. mutate: 호출하고 잊기 (fire-and-forget)
  6. mutateAsync: 호출하고 결과 기다리기

🤔 언제 뭘 써야 할까?

  • 즉시 처리 가능한 것: 동기함수 (계산, 변환 등)
  • 시간이 걸리는 것: 비동기함수 (API, 파일 읽기 등)
  • 결과를 기다려야 하는 경우: async/await 또는 .then()
  • 결과를 기다리지 않아도 되는 경우: 그냥 호출

💡 실무 팁

  1. 일관성 유지: 한 함수에서는 async/await나 .then() 중 하나만 사용
  2. 에러 처리: 항상 try/catch나 .catch() 사용
  3. 타입 명시: TypeScript에서 Promise 타입 명시
  4. React Query: 대부분은 mutate 사용, 특별한 경우만 mutateAsync

이제 동기/비동기/Promise가 조금 더 명확해졌나요? 실무에서 막힐 때마다 이 가이드를 참고해보세요! 🚀

profile
궁금한 것, 했던 것, 시행착오 그리고 기억하고 싶은 것들을 기록합니다.

0개의 댓글