eatthefrog

비동기 프로그래밍 입문: async, Promise, await 쉽게 이해하기 본문

프론트엔드 노트

비동기 프로그래밍 입문: async, Promise, await 쉽게 이해하기

eater_forg 2025. 11. 12. 15:06

 

 

 

웹이나 앱을 개발하다 보면 "지금 당장은 결과를 알 수 없지만, 나중에 알 수 있는" 작업들이 많이 있습니다. 서버에서 데이터를 가져오거나, 파일을 읽거나, 사용자의 응답을 기다리는 것들이죠. 이런 작어을 처리하는 방법이 바로 비동기 프로그래밍입니다.

 

본문에서는 JS/TS에서 비동기 작업을 다루는 핵심 키워드 세 가지를 알아보겠습니다.


핵심 개념

1. async: " 이 함수는 나중에 결과를 줄게요!" 라고 선언

2. Prmoise: 미래에 올 값을 담는 상자

3. await: 상자가 열릴 때까지 기다렸다가, 안의 실제 값을 꺼냄

 


1. async 함수란?

async는 함수 앞에 붙이는 키워드로, "이 함수는 비동기 작업을 할 거예요"라고 선언하는 것입니다.

핵심 특징

async 함수는 항상 Promise를 반환합니다.

무슨 말이냐고요? 일반 함수는 결과를 바로 돌려주지만, async 함수는 "나중에 줄게요"라는 약속(Promise)을 담아서 돌려줍니다.

// 일반 함수
function sayHello() {
  return '안녕';  // 바로 '안녕' 반환
}

// async 함수
async function sayHelloLater() {
  return '안녕';  // Promise로 감싸진 '안녕' 반환
}

반환 타입은 자동으로 결정됩니다

  • 아무것도 반환하지 않으면 → Promise<void>
  • 문자열을 반환하면 → Promise<string>
  • 숫자를 반환하면 → Promise<number>

2. Promise: 미래의 값을 담는 상자

Promise를 가장 쉽게 이해하는 방법은 "택배 송장"에 비유하는 것입니다.

택배 송장으로 이해하기

온라인 쇼핑을 하면 물건이 바로 오지 않죠? 대신 송장 번호를 받습니다. 이 송장이 바로 Promise입니다.

  • 지금은 물건이 없지만, 나중에 배송이 완료되면 물건을 받을 수 있어요
  • 송장(Promise) 안에는 어떤 물건(타입)이 올지 정보가 담겨 있어요
  • 배송이 완료되면 실제 물건(값)을 받을 수 있어요
// 서버에서 사용자 정보를 가져오는 함수
// 지금 당장은 없지만, 나중에 문자열이 올 거예요
async function getUserName() {
  // ... 서버 통신 로직
  return 'kyoka';  // Promise<string> 형태로 반환됨
}

// 숫자를 계산하는 비동기 함수
async function calculateTotal() {
  // ... 복잡한 계산
  return 29;  // Promise<number> 형태로 반환됨
}

Promise는 다양한 타입을 담을 수 있습니다:

  • Promise<string> - 나중에 문자열이 올 거예요
  • Promise<number> - 나중에 숫자가 올 거예요
  • Promise<boolean> - 나중에 참/거짓이 올 거예요
  • Promise<string | null> - 나중에 문자열이 오거나 없을 수도 있어요

3. await: 택배가 도착할 때까지 기다리기

택배 송장(Promise)을 받았다면, 실제 물건이 도착할 때까지 기다려야겠죠? 그게 바로 await입니다.

await의 역할

"여기서 잠깐 멈추고, Promise 안의 실제 값이 준비될 때까지 기다릴게요"

async function showUserInfo() {
  // getUserName()은 Promise<string>을 반환
  // await을 붙이면 실제 string 값을 얻을 때까지 기다림
  const name = await getUserName();  // Kyoka'
  
  console.log(name);  // 이제 실제 값을 사용할 수 있어요!
}

중요한 규칙들

  1. await은 async 함수 안에서만 사용 가능
    • async가 없는 일반 함수에서는 사용할 수 없어요
  2. await 뒤에는 Promise를 반환하는 것만 올 수 있어요
    • 다른 async 함수 호출
    • Promise 객체
    • Promise를 반환하는 라이브러리 함수
// ✅ 올바른 사용
await fetchDataFromServer();  // async 함수
await AsyncStorage.getItem('key');  // Promise 반환 함수

// ❌ 잘못된 사용
await 123;  // 숫자는 Promise가 아니에요
await '안녕';  // 문자열도 Promise가 아니에요

그렇다면 언제 async/await를 사용해야 할까?

지금까지 async/await가 무엇인지 알아봤습니다. 그렇다면 실제로 언제 사용하면 좋을까요?

한 문장으로 요약하면: async/await는 복잡한 비동기 처리를 마치 일반 코드처럼 읽기 쉽게 만들어주는 도구입니다.

 

비동기 작업은 "시간이 걸리는 작업"이에요. 서버 통신, 파일 읽기, 데이터베이스 조회 등은 무조건 비동기 로 처리돼야 합니다. 우리가 선택할 수 있는 게 아니에요. , fetchUser(),,,,etc
 

 

 

이런 상황에 사용하세요

1. 작업을 순서대로 진행해야 할 때

: 만약 async/await 없이 구현한다면? 콜백 지옥이나 복잡한 Promise 체이닝으로 코드가 매우 읽기 어려워집니다.

// 예: 회원가입 프로세스
async function signUp(email, password) {
  // 1단계: 이메일 중복 확인 (완료되어야 다음으로)
  const isAvailable = await checkEmailAvailable(email);
  
  // 2단계: 계정 생성 (1단계 완료 후)
  const user = await createAccount(email, password);
  
  // 3단계: 환영 이메일 발송 (2단계 완료 후)
  await sendWelcomeEmail(user);
  
  return user;
}

 

 

2. 서버 통신이나 데이터 로딩이 필요할 때

: fetch 같은 함수들은 Promise를 반환하기 때문에 await과 찰떡궁합입니다

async function loadUserProfile() {
  // 서버에서 데이터 가져오기
  const profile = await fetch('/api/user/profile');
  const data = await profile.json();
  
  return data;
}

 

3. 코드의 가독성을 높이고 싶을 때

: 비동기 코드를 일반 코드처럼 위에서 아래로 읽을 수 있어서, 다른 개발자가 봐도 이해하기 쉽습니다.

 


⚠️ 주의: async/await를 너무 남용하면 안 돼요

async/await는 편리하지만, 무분별하게 사용하면 오히려 앱이 느려질 수 있습니다@.@

 

 

 

문제 상황: 불필요하게 기다리기

// ❌ 나쁜 예: 서로 독립적인 작업인데 순차 실행
async function loadDashboard() {
  const user = await fetchUser();        // 1초 대기
  const notifications = await fetchNotifications();  // 1초 대기
  const settings = await fetchSettings();       // 1초 대기
  
  // 총 3초 소요 😱
}

 

문제점: 사용자 정보, 알림, 설정은 서로 관련이 없는데도 순서대로 기다리고 있어요. 마치 식당에서 음료, 메인 요리, 디저트를 따로따로 주문하고 하나씩 먹는 것과 같죠.

 

 

해결 방법: 병렬 처리

: Promise.all을 사용하면 여러 작업을 동시에 실행할 수 있습니다. 식당에서 한 번에 주문하고 준비되는 대로 받는 것과 같아요.

// ✅ 좋은 예: 동시에 실행
async function loadDashboard() {
  // 세 작업을 동시에 시작!
  const [user, notifications, settings] = await Promise.all([
    fetchUser(),
    fetchNotifications(),
    fetchSettings()
  ]);
  
  // 총 1초 소요 (가장 오래 걸리는 작업 기준) 🚀
}

 

실제로 발생할 수 있는 문제들

  • 성능 저하: 동시에 처리해도 되는 작업을 순차 처리하면 사용자 대기 시간 증가
  • 메모리 낭비: 불필요한 대기 시간 동안 함수가 멈춰있으면서 메모리 점유
  • 사용자 경험 악화: 로딩이 길어져서 답답함

언제 순차, 언제 병렬?

순차 실행 (await 각각 사용)

  • 다음 작업이 이전 결과에 의존할 때
  • 예: 로그인 → 프로필 로드 → 대시보드 표시

병렬 실행 (Promise.all 사용)

  • 서로 독립적인 작업들
  • 예: 사용자 정보, 알림 목록, 설정 동시 로드