화살표 함수와 메서드 타이핑
JavaScript를 공부하다 보면 function 키워드와 화살표 함수(=>)가 모두 등장합니다. 대부분의 경우 비슷하게 동작하지만, this를 다룰 때 완전히 다릅니다. TypeScript는 이 미묘한 차이를 타입 시스템으로 잡아냅니다.
이 장에서는 화살표 함수에 타입을 붙이는 방법과, 객체 메서드를 타이핑하는 방법을 살펴봅니다. 그리고 많은 JavaScript 개발자를 괴롭혀온 this의 타입도 함께 정리합니다.
화살표 함수 타이핑
화살표 함수에 타입을 붙이는 방법은 일반 함수와 거의 같습니다.
// 새 파일: arrow-function.ts// 화살표 함수에 직접 타입 붙이기const add = (a: number, b: number): number => a + b;const greet = (name: string): string => `안녕하세요, ${name}님!`;const logMessage = (message: string): void => { console.log(`[LOG] ${message}`);};console.log(add(3, 7)); // 10console.log(greet("이민준")); // 안녕하세요, 이민준님!logMessage("화살표 함수"); // [LOG] 화살표 함수
변수에 함수 타입을 먼저 선언하는 방식도 화살표 함수와 잘 어울립니다.
// 함수 타입 먼저 선언 후 화살표 함수로 구현type BinaryOp = (a: number, b: number) => number;const multiply: BinaryOp = (a, b) => a * b;const divide: BinaryOp = (a, b) => { if (b === 0) throw new Error("0으로 나눌 수 없습니다."); return a / b;};console.log(multiply(4, 5)); // 20console.log(divide(10, 2)); // 5
BinaryOp로 타입을 선언했으므로 화살표 함수 안에서 a와 b의 타입을 다시 쓰지 않아도 됩니다.
객체 메서드 타이핑
객체 안의 함수, 즉 메서드에도 타입을 붙일 수 있습니다.
// 새 파일: object-methods.ts// 인터페이스로 메서드 타입 정의interface Calculator { add(a: number, b: number): number; subtract(a: number, b: number): number; multiply(a: number, b: number): number; // 화살표 함수 스타일로도 정의할 수 있습니다 divide: (a: number, b: number) => number;}const calc: Calculator = { add(a, b) { return a + b; }, subtract(a, b) { return a - b; }, multiply(a, b) { return a * b; }, divide: (a, b) => { if (b === 0) throw new Error("0으로 나눌 수 없습니다."); return a / b; }};console.log(calc.add(10, 5)); // 15console.log(calc.subtract(10, 5)); // 5console.log(calc.multiply(3, 4)); // 12console.log(calc.divide(20, 4)); // 5
인터페이스의 메서드 선언 방식은 두 가지입니다. add(a: number, b: number): number처럼 메서드 스타일로 쓰거나, divide: (a: number, b: number) => number처럼 함수 타입 표현식으로 쓸 수 있습니다. 기능은 같지만 this 타이핑에서 차이가 생깁니다.
this의 혼란
this는 JavaScript에서 가장 헷갈리는 개념 중 하나입니다.
// 새 파일: this-problem.tsconst counter = { count: 0, increment() { this.count += 1; // 여기서 this는 counter 객체 console.log(this.count); }};counter.increment(); // 1counter.increment(); // 2// 문제 발생const fn = counter.increment;fn(); // TypeError! this가 counter가 아닙니다
counter.increment()로 호출하면 this는 counter입니다. 하지만 fn()으로 독립 호출하면 this는 undefined(strict mode) 또는 전역 객체가 됩니다.
화살표 함수가 this를 고정하는 방법
화살표 함수는 자신만의 this를 만들지 않습니다. 선언된 위치의 this를 그대로 씁니다. 이것을 "렉시컬 this(lexical this)"라고 합니다.
// 새 파일: arrow-this.tsclass Timer { private seconds = 0; // 일반 함수 메서드 — this가 바뀔 수 있습니다 startRegular() { setInterval(function() { this.seconds += 1; // TypeError: this가 Timer가 아닙니다 console.log(this.seconds); }, 1000); } // 화살표 함수 메서드 — this가 항상 Timer 인스턴스입니다 startArrow() { setInterval(() => { this.seconds += 1; // 정상: this는 Timer 인스턴스 console.log(this.seconds); }, 1000); }}const timer = new Timer();timer.startArrow(); // 1, 2, 3, ... (정상 동작)
setInterval의 콜백이 화살표 함수이므로, 콜백 안의 this는 startArrow가 선언된 클래스 인스턴스를 가리킵니다.
TypeScript의 this 타입 매개변수
TypeScript는 함수의 첫 번째 매개변수 자리에 this를 선언하는 특수한 문법을 지원합니다. 실제 매개변수는 아니지만 컴파일러에게 this의 타입을 알려줍니다.
// 새 파일: this-type.tsinterface UserInfo { name: string; age: number; greet(this: UserInfo): string; // this의 타입을 명시}function greetUser(this: UserInfo): string { return `안녕하세요, ${this.name}님! (${this.age}세)`;}const user: UserInfo = { name: "이민준", age: 22, greet: greetUser};console.log(user.greet()); // 안녕하세요, 이민준님! (22세)// this가 잘못된 컨텍스트에서 호출되면 오류const standalone = greetUser;standalone(); // 오류: void 형식의 'this' 컨텍스트를 'UserInfo' 형식의 메서드 'this'에 할당할 수 없습니다
this: UserInfo를 선언하면 greetUser를 올바른 컨텍스트 없이 단독으로 호출할 때 컴파일 오류가 납니다.
클래스에서의 메서드 타이핑
클래스는 메서드의 타입을 자연스럽게 표현합니다. 클래스에 대한 자세한 내용은 이후 파트에서 다루지만, 메서드 타이핑 관점에서 미리 살펴봅니다.
// 새 파일: class-methods.tsclass TodoList { private items: string[] = []; add(item: string): void { this.items.push(item); console.log(`"${item}" 추가됨`); } remove(item: string): boolean { const index = this.items.indexOf(item); if (index === -1) return false; this.items.splice(index, 1); console.log(`"${item}" 제거됨`); return true; } getAll(): string[] { return [...this.items]; // 복사본을 반환합니다 } count(): number { return this.items.length; }}const todo = new TodoList();todo.add("TypeScript 공부");todo.add("운동하기");todo.add("독서");console.log(todo.count()); // 3console.log(todo.getAll()); // ["TypeScript 공부", "운동하기", "독서"]todo.remove("운동하기");console.log(todo.count()); // 2
클래스의 this는 항상 해당 클래스의 인스턴스를 가리킵니다. TypeScript가 자동으로 처리하므로 별도로 선언할 필요가 없습니다.
화살표 함수를 클래스 필드로 정의하기
클래스에서 메서드를 이벤트 리스너로 쓸 때 this 문제가 자주 발생합니다. 화살표 함수를 클래스 필드로 정의하면 this가 항상 고정됩니다.
// 새 파일: class-arrow.tsclass ButtonHandler { private clickCount = 0; // 일반 메서드 — 이벤트 리스너로 쓰면 this가 바뀝니다 handleClickRegular() { this.clickCount += 1; console.log(`클릭 횟수: ${this.clickCount}`); } // 화살표 함수 필드 — this가 항상 인스턴스입니다 handleClickArrow = () => { this.clickCount += 1; console.log(`클릭 횟수: ${this.clickCount}`); };}const handler = new ButtonHandler();// 일반 메서드는 참조 전달 시 this가 끊깁니다const regularFn = handler.handleClickRegular;// regularFn(); // TypeScript가 오류를 낼 수 있습니다// 화살표 함수 필드는 this가 유지됩니다const arrowFn = handler.handleClickArrow;arrowFn(); // 클릭 횟수: 1arrowFn(); // 클릭 횟수: 2// 실제 이벤트 리스너 등록// document.getElementById("btn")?.addEventListener("click", handler.handleClickArrow);
메서드 체이닝을 위한 this 반환
메서드가 this를 반환하면 연속 호출이 가능해집니다.
// 새 파일: method-chaining.tsclass QueryBuilder { private tableName = ""; private conditions: string[] = []; private limitValue: number | null = null; from(table: string): this { this.tableName = table; return this; } where(condition: string): this { this.conditions.push(condition); return this; } limit(n: number): this { this.limitValue = n; return this; } build(): string { let query = `SELECT * FROM ${this.tableName}`; if (this.conditions.length > 0) { query += ` WHERE ${this.conditions.join(" AND ")}`; } if (this.limitValue !== null) { query += ` LIMIT ${this.limitValue}`; } return query; }}const query = new QueryBuilder() .from("users") .where("age >= 18") .where("active = true") .limit(10) .build();console.log(query);// SELECT * FROM users WHERE age >= 18 AND active = true LIMIT 10
반환 타입을 this로 선언하면 서브클래스에서 상속받았을 때도 올바르게 동작합니다. 서브클래스의 메서드가 this를 반환하면 서브클래스 타입이 유지됩니다.
이것으로 PART 04를 마칩니다. 매개변수와 반환값 타이핑에서 시작해 선택적 매개변수, 콜백, 오버로드, 그리고 화살표 함수와 this까지 함수의 모든 면에 타입을 입혔습니다.
다음 PART 05에서는 타입 좁히기를 다룹니다. 유니온 타입처럼 여러 가능성이 있을 때 TypeScript가 특정 경우를 정확히 알아낼 수 있도록 코드를 작성하는 방법입니다. typeof, instanceof, 그리고 사용자 정의 타입 가드까지 타입 추론을 더 정밀하게 제어하는 기법들을 배웁니다.