C
TypeScript/비동기/Lesson 01

동기 vs 비동기 — TS 는 콜백 시그니처까지 보호한다

25분·theory
이 챕터
1/7
TypeScript

동기 vs 비동기 — TS 는 콜백 시그니처까지 보호한다

💡 왜 배워야 할까요? — 동기·비동기는 같지만, 안전망이 다르다

🎯 동기·비동기의 런타임 동작은 JS 와 TS 가 동일합니다 — 차이는 '콜백·결과의 타입을 누가 보장하느냐' 입니다.
💼 JS 는 `setTimeout(cb, 1000)` 의 `cb` 가 어떤 인자를 받는지 모릅니다. TS 는 콜백 시그니처를 타입으로 박을 수 있습니다.
비동기 함수가 무엇을 돌려주는지 (`Promise` 인지 `Promise` 인지) 시그니처에 박아두면, 호출부 코드가 자동완성 됩니다.
🔗 Node.js 와 브라우저의 `setTimeout` 반환값 타입이 다릅니다 (`NodeJS.Timeout` vs `number`) — TS 는 이걸 환경별로 정확히 추론합니다.
🏢 실무에서는
이벤트 핸들러·타이머·API 호출은 전부 콜백을 받습니다. JS 에서는 콜백 안에서 `e.target.value` 같은 코드를 쓰다 오타로 `e.target.valeu` 가 되어도 런타임에서야 발견됩니다. TS 는 `(e: React.ChangeEvent) => void` 처럼 타입을 박아서 IDE 자동완성·오타 차단·구조 변경 자동 추적을 다 제공합니다.

동기·비동기는 같지만, 콜백 타입이 다르다

1. 런타임 동작은 동일

ts
// 동기 — 위에서 아래로 즉시 실행
console.log('1');
console.log('2');
console.log('3');

// 비동기 — setTimeout 의 콜백은 Task Queue 로
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 출력: 1 → 3 → 2

TS 든 JS 든 이 동작은 같습니다. TS 는 컴파일 후 타입을 떼고 JS 로 실행되니까요.

2. 차이는 '콜백·반환값 타입' 명시

ts
// JS
function onChange(e) {
  console.log(e.target.value); // e 는 any — 오타 잡아주지 않음
}

// TS
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value); // ✅ 자동완성, 오타 시 빨간 줄
}

3. 비동기 함수의 반환 타입을 박는다

ts
// JS — 호출자는 fetchUser 가 무엇을 돌려주는지 시그니처만 봐서 모름
function fetchUser(id) {
  return fetch(`/api/${id}`).then(r => r.json());
}

// TS — 시그니처만 봐도 'Promise<User> 가 온다' 가 명확
async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/${id}`);
  return res.json();
}

4. setTimeout 의 반환값은 환경별로 다르다 — 한 코드는 한 환경에서만 안전

ts
// ❌ 브라우저 전용 — Node.js 에서는 NodeJS.Timeout 객체가 반환되어 타입 에러
const id1: number = setTimeout(() => {}, 1000);

// ❌ Node.js 전용 — 브라우저 환경 (@types/node 없음) 에서는 NodeJS 네임스페이스 자체가 없어 컴파일 에러
const id2: NodeJS.Timeout = setTimeout(() => {}, 1000);

// ✅ 양쪽 환경에서 안전 — TS 가 lib/@types/node 설정에 따라 자동 추론
const id3: ReturnType<typeof setTimeout> = setTimeout(() => {}, 1000);
clearTimeout(id3);

> 💡 라이브러리·범용 모듈을 만들 때는 무조건 ReturnType<typeof setTimeout>. 앱 코드처럼 환경이 확정된 곳에서만 numberNodeJS.Timeout 을 직접 적어도 됩니다.

💻 🅰️ JS 방식 — 콜백 인자가 무엇인지 모름
// ❌ JS — 콜백·반환값의 모양을 시그니처로 표현 못함

function fetchUser(id, callback) {
  setTimeout(() => {
    callback({ id, name: '홍길동' }); // 이 콜백이 무엇을 받는지 호출자가 모름
  }, 500);
}

fetchUser(42, (user) => {
  console.log(user.name);    // ✅ 운 좋으면 동작
  console.log(user.naem);    // ❌ 오타 — 런타임 undefined, 발견 늦음
  console.log(user.email);   // ❌ 존재하지 않는 필드 — 런타임 undefined
});

// setTimeout 의 반환값도 타입 없음
const timerId = setTimeout(() => console.log('tick'), 1000);
clearTimeout(timerId); // 브라우저면 number, Node 면 객체 — 둘 다 그냥 '값' 으로 취급

// 비동기 함수의 시그니처도 빈약
async function loadOrders(userId) {
  // 반환 타입이 Promise<무엇> 인지 함수 본문을 다 읽어야 알 수 있음
  const res = await fetch(`/api/orders?u=${userId}`);
  return res.json();
}
💻 🅱️ TS 방식 — 콜백 시그니처·반환 타입을 박는다
// ✅ TS — 콜백·반환값의 모양을 타입으로 박는다

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

// 콜백 시그니처 명시 — (user: User) => void
function fetchUser(id: number, callback: (user: User) => void): void {
  setTimeout(() => {
    callback({ id, name: '홍길동' });
  }, 500);
}

fetchUser(42, (user) => {
  // user 의 타입이 User 로 추론됨
  console.log(user.name);  // ✅ 자동완성
  // console.log(user.naem); // ❌ 컴파일 거부 — 'naem' 은 User 에 없음
  // console.log(user.email); // ❌ 컴파일 거부 — User 에 email 없음
});

// 타이머 ID — 환경 무관하게 안전한 추론
const timerId: ReturnType<typeof setTimeout> = setTimeout(
  () => console.log('tick'),
  1000,
);
clearTimeout(timerId);

// 비동기 함수 시그니처에 반환 타입 명시
async function loadOrders(userId: number): Promise<Order[]> {
  const res = await fetch(`/api/orders?u=${userId}`);
  return res.json() as Promise<Order[]>;
}

interface Order { id: number; item: string; }

// 호출부 — 반환이 Order[] 임을 IDE 가 알고 있음
loadOrders(42).then((orders) => {
  orders.forEach((o) => console.log(o.item)); // ✅ 자동완성
});

💡 💡 JS ↔ TS 핵심 차이 (동기/비동기)

1. 콜백을 받는 함수는 콜백 시그니처를 인자 타입에 박아라

ts
function onLoad(cb: (data: User[]) => void): void { ... }

2. 비동기 함수는 반환 타입을 시그니처에 박아라

ts
async function getUser(id: number): Promise<User> { ... }
// 시그니처만 봐도 'Promise<User> 가 온다' 가 보임

3. 타이머 ID 는 ReturnType<typeof setTimeout> 으로 받아라
브라우저는 number, Node 는 NodeJS.Timeout — 환경 무관하게 안전.

4. 비동기 결과를 그냥 any 로 흘리지 마라
res.json() 의 기본 반환은 Promise<any>. 캐스트하든 zod 같은 검증으로 좁히든, 경계에서 타입을 박는 게 핵심.

5. 콜백 헬은 JS·TS 둘 다 같은 답: async/await
TS 는 async/await 의 추론을 더 잘 받쳐줍니다. 이미 promise lesson 에서 다룬 내용.

⚡ 직접 실행해보기 — 동기 vs 비동기

위 🅱️ TS 코드에서 타입을 뗀 실행 버전. 동작 자체는 JS·TS 가 동일하다는 걸 확인합니다.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가

확인 퀴즈

TS 에서 브라우저·Node 양쪽에서 안전하게 타이머 ID 를 받으려면?
💡 브라우저는 `setTimeout` 이 `number` 를, Node.js 는 `NodeJS.Timeout` 객체를 반환합니다. `ReturnType<typeof setTimeout>` 으로 받으면 컴파일 환경(lib 설정·@types/node 유무)에 따라 정확히 추론되어, 같은 코드가 양쪽에서 안전하게 컴파일됩니다.
동기 vs 비동기 — 콜백 시그니처까지 보호 - TypeScript