iBetter Books
수정

콜백 함수 타이핑

택배 기사에게 "문 앞에 놓고 문자 주세요"라고 부탁한 적이 있나요? 그 문자, 즉 "배달 완료 후 해야 할 일"이 콜백입니다. 당장 실행하는 게 아니라, 어떤 일이 끝난 다음 실행될 함수를 미리 전달하는 것입니다.

JavaScript는 콜백 패턴을 아주 많이 씁니다. 배열의 forEach, map, filter도 콜백을 받습니다. 버튼 클릭 이벤트도 콜백입니다. TypeScript는 이 콜백 함수에도 정확한 타입을 요구합니다.

콜백의 기본 패턴

함수를 매개변수로 받는 함수를 만들어봅시다.

// 새 파일: callback-basic.tsfunction processNumber(value: number, callback: (result: number) => void): void {  const doubled = value * 2;  callback(doubled);}processNumber(5, (result) => {  console.log(`결과: ${result}`);   // 결과: 10});processNumber(10, (result) => {  console.log(`결과의 제곱: ${result * result}`);   // 결과의 제곱: 400});

callback: (result: number) => void가 핵심입니다. "숫자 하나를 받고 아무것도 반환하지 않는 함수"라는 타입입니다. 이 타입에 맞지 않는 함수를 넘기면 오류가 납니다.

함수 타입 표현식

콜백 타입이 길어지면 읽기 불편합니다. type 키워드로 함수 타입에 이름을 붙일 수 있습니다.

// 새 파일: function-type.ts// 함수 타입에 이름 붙이기type NumberCallback = (value: number) => void;type StringTransformer = (text: string) => string;type Validator = (input: unknown) => boolean;function applyToAll(items: number[], callback: NumberCallback): void {  items.forEach(item => callback(item));}function transformText(text: string, transformer: StringTransformer): string {  return transformer(text);}// 사용applyToAll([1, 2, 3], (n) => console.log(n * n));  // 1, 4, 9const upper = transformText("hello typescript", (s) => s.toUpperCase());console.log(upper);   // HELLO TYPESCRIPTconst shout = transformText("안녕하세요", (s) => s + "!!!");console.log(shout);   // 안녕하세요!!!

타입 이름이 생기면 코드의 의도가 더 명확해집니다. NumberCallback이라고 쓰는 순간, "숫자를 다루는 콜백"이라는 사실이 바로 보입니다.

반환값이 있는 콜백

콜백이 값을 반환하는 경우도 많습니다. 배열의 mapfilter가 대표적입니다.

// 새 파일: callback-return.tstype Mapper<T, U> = (item: T) => U;type Predicate<T> = (item: T) => boolean;function myMap<T, U>(items: T[], mapper: Mapper<T, U>): U[] {  const result: U[] = [];  for (const item of items) {    result.push(mapper(item));  }  return result;}function myFilter<T>(items: T[], predicate: Predicate<T>): T[] {  const result: T[] = [];  for (const item of items) {    if (predicate(item)) {      result.push(item);    }  }  return result;}const numbers = [1, 2, 3, 4, 5];// number[] → string[]const texts = myMap(numbers, (n) => `${n}번`);console.log(texts);   // ["1번", "2번", "3번", "4번", "5번"]// 짝수만 필터링const evens = myFilter(numbers, (n) => n % 2 === 0);console.log(evens);   // [2, 4]

TU는 제네릭 타입 매개변수입니다. 제네릭은 PART 06에서 자세히 다루지만, 여기서는 "어떤 타입이든 받겠다"는 표현으로 이해하면 됩니다.

배열 메서드의 콜백 타입

실제로 많이 쓰는 배열 메서드들의 콜백 타입을 살펴봅시다.

// 새 파일: array-callbacks.tsinterface Product {  name: string;  price: number;  inStock: boolean;}const products: Product[] = [  { name: "TypeScript 교재", price: 28000, inStock: true },  { name: "JavaScript 입문서", price: 22000, inStock: false },  { name: "알고리즘 문제집", price: 35000, inStock: true },  { name: "데이터베이스 가이드", price: 30000, inStock: true }];// forEach: (value: T) => voidproducts.forEach((product) => {  if (product.inStock) {    console.log(`${product.name}: ${product.price.toLocaleString()}원`);  }});// map: (value: T) => Uconst names: string[] = products.map((product) => product.name);console.log(names);// filter: (value: T) => booleanconst available: Product[] = products.filter((product) => product.inStock);console.log(available.length);   // 3// reduce: (acc: U, value: T) => Uconst totalPrice: number = products.reduce((sum, product) => {  return sum + product.price;}, 0);console.log(`총 가격: ${totalPrice.toLocaleString()}원`);   // 총 가격: 115,000원

TypeScript는 배열 메서드의 콜백 타입을 배열의 원소 타입으로부터 자동으로 추론합니다. productsProduct[]이므로 forEach의 콜백 인수는 자동으로 Product가 됩니다.

이벤트 리스너 타이핑

브라우저 환경에서 이벤트 리스너도 콜백입니다. TypeScript는 DOM 이벤트에 대한 타입 정보를 내장하고 있습니다.

// 새 파일: event-listener.ts (브라우저 환경)const button = document.getElementById("submit-btn");// addEventListener의 두 번째 인수는 EventListener 타입// event 매개변수는 MouseEvent로 추론됩니다button?.addEventListener("click", (event: MouseEvent) => {  console.log(`클릭 위치: (${event.clientX}, ${event.clientY})`);  event.preventDefault();});// input 이벤트const input = document.getElementById("search-input") as HTMLInputElement;input?.addEventListener("input", (event: Event) => {  const target = event.target as HTMLInputElement;  console.log(`입력값: ${target.value}`);});// keydown 이벤트document.addEventListener("keydown", (event: KeyboardEvent) => {  if (event.key === "Enter") {    console.log("Enter 키가 눌렸습니다.");  }});

이벤트 종류에 따라 타입이 다릅니다. 클릭은 MouseEvent, 키보드는 KeyboardEvent, 일반적인 이벤트는 Event입니다. 올바른 타입을 써야 event.clientX 같은 속성에 접근할 수 있습니다.

비동기 콜백 타이핑

setTimeout, setInterval 같은 비동기 함수의 콜백에도 타입이 있습니다.

// 새 파일: async-callback.ts// setTimeout의 콜백은 () => voidconst timeoutId: ReturnType<typeof setTimeout> = setTimeout(() => {  console.log("1초 후 실행");}, 1000);// setInterval의 콜백도 () => voidconst intervalId: ReturnType<typeof setInterval> = setInterval(() => {  console.log("1초마다 실행");}, 1000);// 5초 후 정지setTimeout(() => {  clearInterval(intervalId);  console.log("인터벌 정지");}, 5000);

실전 예제 — 이벤트 에미터 만들기

콜백 패턴을 활용해 간단한 이벤트 시스템을 만들어봅시다.

// 새 파일: event-emitter.tstype EventHandler<T> = (data: T) => void;class EventEmitter<Events extends Record<string, unknown>> {  private handlers: Partial<{    [K in keyof Events]: EventHandler<Events[K]>[];  }> = {};  on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void {    if (!this.handlers[event]) {      this.handlers[event] = [];    }    this.handlers[event]!.push(handler);  }  emit<K extends keyof Events>(event: K, data: Events[K]): void {    const eventHandlers = this.handlers[event];    if (eventHandlers) {      eventHandlers.forEach(handler => handler(data));    }  }}// 이벤트 타입 정의interface AppEvents {  login: { userId: string; timestamp: Date };  logout: { userId: string };  error: { code: number; message: string };}const emitter = new EventEmitter<AppEvents>();emitter.on("login", ({ userId, timestamp }) => {  console.log(`${userId}님이 ${timestamp.toLocaleTimeString()}에 로그인했습니다.`);});emitter.on("error", ({ code, message }) => {  console.error(`오류 ${code}: ${message}`);});emitter.emit("login", { userId: "minjun", timestamp: new Date() });emitter.emit("error", { code: 404, message: "페이지를 찾을 수 없습니다." });

이벤트 이름과 데이터 타입을 AppEvents로 정의했습니다. onemit 호출 시 이벤트 이름에 맞는 데이터 타입을 자동으로 추론합니다. 잘못된 이벤트 이름이나 데이터를 넘기면 오류가 납니다.


콜백에 타입을 붙이면 "어떤 함수를 넣어야 하는지"가 명확해집니다. API 문서를 찾아볼 필요 없이 IDE가 올바른 콜백 형태를 알려줍니다.

다음 장에서는 함수 오버로드를 다룹니다. 같은 이름의 함수가 서로 다른 인수를 받을 때, 각 경우에 맞는 정확한 타입을 정의하는 방법입니다.