C
TypeScript/비동기/Lesson 04

async / await — 반환 타입을 박으면 호출부가 자동 추론된다

30분·theory
이 챕터
4/7
TypeScript

async / await — 반환 타입을 박으면 호출부가 자동 추론된다

💡 왜 배워야 할까요? — async 함수의 시그니처가 곧 계약서

🎯 JS 의 async 함수는 무엇을 돌려주는지 함수 본문을 다 읽어야 알 수 있습니다.
💼 TS 의 async 함수는 시그니처에 `Promise` 같이 박아두면, 호출부는 본문 안 봐도 안전하게 사용 가능합니다.
`await` 의 결과 타입은 자동 추론됩니다 — 한 번 박은 타입이 await 체인 끝까지 흐릅니다.
🔗 try/catch 의 `err` 는 TS 4.4+ 부터 `unknown` 입니다. `instanceof Error` 같이 좁혀야 message 에 접근 가능.
🏢 실무에서는
Next.js Server Component, Server Action, API Route 핸들러 — 전부 async 함수입니다. 반환 타입을 명시하면 그 함수를 import 해서 쓰는 클라이언트·서버 모든 곳에서 IDE 가 매개변수·반환값을 알려줍니다. 명시 안 하면 매번 `as User` 강제 캐스팅이 누적됩니다.

async 함수 = 항상 Promise 반환, await = 풀기

1. async 함수는 반환 타입이 자동 Promise

ts
async function getUser(): Promise<User> {
  return { id: 1, name: '홍길동' };
  //       ↑ User 객체를 그냥 return
  //         실제 반환 타입은 자동으로 Promise<User>
}
  • async 가 붙은 함수는 항상 Promise 를 반환합니다.
  • 반환 타입은 Promise<...> 형태여야 합니다 (그 외 타입을 시그니처에 적으면 컴파일 에러).
  • 본문에서 return value 하면 자동으로 Promise.resolve(value) 로 감싸짐.
  • 본문에서 throw err 하면 자동으로 Promise.reject(err) 가 됨.

2. await 의 결과는 자동 추론

ts
async function loadAll() {
  const user: User = await getUser();          // 추론 OK
  const orders: Order[] = await getOrders(user.id); // 체인 추론
  return { user, orders };
}
// loadAll 의 반환 타입은 자동으로 Promise<{ user: User; orders: Order[] }>

3. catch 의 err 는 unknown (TS 4.4+)

ts
try {
  await dangerousAction();
} catch (err) {
  // err 의 타입은 unknown — 그대로는 .message 접근 불가
  // err.message; ❌ Object is of type 'unknown'

  if (err instanceof Error) {
    console.log(err.message); // ✅ 좁혀짐
  } else {
    console.log('알 수 없는 에러:', err);
  }
}

4. Promise 반환 함수와 async 함수는 같다

ts
// 두 함수의 시그니처는 동일
function a(): Promise<number> {
  return Promise.resolve(42);
}
async function b(): Promise<number> {
  return 42;
}
// a() 와 b() 의 호출부는 완전히 동일하게 동작

취향에 따라 골라 쓰지만, await 을 본문에서 쓰려면 async 가 강제됩니다.

💻 🅰️ JS 방식 — 반환 타입 불투명, err 도 any
// ❌ JS — async 함수 시그니처가 빈약

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('실패');
  return res.json(); // 반환은 Promise<???>
}

async function main() {
  try {
    const user = await fetchUser(42);
    console.log(user.name);     // user 가 무엇인지 IDE 가 모름
    console.log(user.naem);     // ❌ 오타지만 컴파일 통과
  } catch (err) {
    console.log(err.message);   // err 가 Error 라는 보장 없지만 그냥 접근
    // 만약 throw '문자열' 한 거면 err.message 는 undefined
  }
}
main();
💻 🅱️ TS 방식 — 시그니처 + await 추론 + err 좁히기
// ✅ TS — 시그니처에 반환 타입 명시 + err 안전 처리

interface User {
  id: number;
  name: string;
  email: string;
}

class ApiError extends Error {
  constructor(message: string, public status: number) {
    super(message);
    this.name = 'ApiError';
  }
}

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new ApiError('실패', res.status);
  return res.json() as Promise<User>;
  // 또는 zod 등으로 런타임 검증 후 반환
}

async function main(): Promise<void> {
  try {
    const user = await fetchUser(42);
    // user 는 User 로 추론됨
    console.log(user.name);       // ✅ 자동완성
    // console.log(user.naem);    // ❌ 컴파일 거부
    console.log(user.email);      // ✅ User 에 있음
  } catch (err) {
    // err: unknown — 그대로 .message 접근 불가
    if (err instanceof ApiError) {
      console.log(`API 에러 ${err.status}: ${err.message}`);
    } else if (err instanceof Error) {
      console.log('일반 에러:', err.message);
    } else {
      console.log('알 수 없는 에러:', err);
    }
  }
}
main();

💡 💡 async/await TS 핵심 5

1. async 함수의 반환 타입은 시그니처에 명시한다

ts
async function getUser(id: number): Promise<User> { ... }

시그니처만 봐도 호출자가 의도를 안다.

2. await 의 결과는 추론에 맡겨도 OK

ts
const user = await getUser(42); // user: User 로 자동 추론

명시하면 가독성 ↑, 생략해도 안전.

3. err 는 unknown — 좁혀서 써라

ts
catch (err) {
  if (err instanceof Error) console.log(err.message);
}

tsconfiguseUnknownInCatchVariables: true 가 TS 4.4+ strict 의 기본.

4. async 함수 안에서 Promise 를 다시 감싸지 마라

ts
// ❌ 중복 감싸기
async function bad() { return Promise.resolve(42); }
// ✅ 그냥 return
async function good() { return 42; }

5. Promise.all 의 결과는 튜플로 받는다 (이미 promise lesson 에서 다룸)

ts
const [user, orders] = await Promise.all([getUser(1), getOrders(1)]);
// user: User, orders: Order[]

⚡ 직접 실행해보기 — async/await

타입을 뗀 실행 버전. await 으로 순차 처리, try/catch 로 에러 잡기를 확인합니다.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가

확인 퀴즈

TS 4.4+ 에서 `try { ... } catch (err) { console.log(err.message); }` 가 컴파일 에러를 내는 이유는?
💡 TS 4.4 부터 `useUnknownInCatchVariables` 가 strict 모드 기본값이 되면서, `catch (err)` 의 `err` 타입이 `any` → `unknown` 으로 바뀌었습니다. `unknown` 에는 `.message` 가 없으므로 `if (err instanceof Error)` 같은 타입 좁히기를 거쳐야 합니다. 더 안전한 기본값이라 권장됩니다.
async / await — 반환 타입과 err: unknown - TypeScript