본문 바로가기

화살표 함수와 this

일반 함수의 호출 방식에 따라 달라지는 this와 상위 스코프를 캡처하는 화살표 함수의 바인딩을 타입과 함께 구분합니다.

자바스크립트에서 this 키워드는 개발자가 가장 자주 헷갈리는 지점 중 하나입니다.

this 값이 함수 선언이 아니라 호출 방식에 따라 동적으로 결정되기 때문입니다.

ES2015(ES6)의 화살표 함수(Arrow Functions)는 이 this 바인딩 규칙을 크게 바꿨고, 타입스크립트에서도 이 차이를 정확히 이해하는 것이 중요합니다.

이번 절에서는 화살표 함수의 기본적인 사용법을 다시 한번 살펴보고, 특히 this 바인딩 규칙이 어떻게 달라지는지, 그리고 이것이 타입스크립트에서 어떤 의미를 가지는지 자세히 알아보겠습니다.


화살표 함수 기본 문법 복습

화살표 함수는 기존의 function 키워드 함수보다 더 간결한 문법으로 함수를 정의할 수 있게 해줍니다.

// 1. 기본 화살표 함수 (매개변수 2개, 반환 값 존재)
const add = (a: number, b: number): number => {
  return a + b;
};
console.log(add(5, 3)); // 8

// 2. 단일 매개변수 (타입 표기가 있으므로 괄호 필요)
const square = (num: number): number => num * num;
console.log(square(4)); // 16

// 3. 매개변수 없음 (빈 괄호 필요)
const sayHello = (): string => "안녕하세요!";
console.log(sayHello()); // 안녕하세요!

// 4. 객체 리터럴 반환 (괄호로 감싸야 함)
const createPoint = (x: number, y: number) => ({ x: x, y: y });
console.log(createPoint(1, 2)); // { x: 1, y: 2 }

화살표 함수는 특히 콜백 함수나 짧은 함수를 정의할 때 유용하며, 코드를 훨씬 간결하고 읽기 쉽게 만듭니다.


this 바인딩의 차이점

this 키워드는 함수가 실행될 때 함수의 실행 컨텍스트(Execution Context)에 따라 결정됩니다.

이 this 바인딩 방식에서 화살표 함수와 일반 함수는 결정적인 차이를 보입니다.

  • 일반 함수 (function 키워드 함수): this는 함수가 어떻게 호출되었는지(호출 방식)에 따라 동적으로 결정됩니다.

    • 객체의 메서드로 호출될 때: this는 해당 객체를 가리킵니다.
    • 단독으로 호출될 때 (엄격 모드가 아닐 경우): this는 전역 객체(브라우저에서는 window, Node.js에서는 global)를 가리킵니다. 엄격 모드('use strict')에서는 undefined를 가리킵니다.
    • new 키워드와 함께 생성자 함수로 호출될 때: this는 새로 생성된 인스턴스를 가리킵니다.
    • call(), apply(), bind() 메서드를 사용할 때: this는 명시적으로 바인딩된 객체를 가리킵니다.
    class TraditionalGreeter {
      greeting: string = "Hello";
    
      sayGreeting(name: string) {
        console.log(`${this.greeting}, ${name}!`);
      }
    
      // 메서드를 콜백으로 전달할 때 문제가 발생할 수 있습니다.
      sayGreetingLater() {
        setTimeout(function() {
          // 이 일반 함수의 this는 타이머 호스트의 호출 규칙을 따르며 tg가 아닙니다.
          // 브라우저와 Node.js의 receiver는 다를 수 있고 strict/noImplicitThis 설정도 확인해야 합니다.
          console.log(`${this.greeting}, world!`);
        }, 100);
      }
    }
    
    const tg = new TraditionalGreeter();
    tg.sayGreeting("TypeScript"); // Hello, TypeScript!
    // tg.sayGreetingLater(); // tg의 greeting을 유지하지 않는 예시이며 결과·진단은 호스트와 검사 설정에 따릅니다.

    이 예시는 일반 함수 콜백이 tg의 this를 캡처하지 않는 문제를 보여 줍니다. 타이머가 전달하는 receiver는 실행 호스트에 따라 다르며, noImplicitThis 설정에서는 명시되지 않은 this 타입도 진단 대상입니다.

  • 화살표 함수: 화살표 함수는 자신만의 this 바인딩을 가지지 않습니다.

    대신, 함수가 선언된 스코프(Lexical Scope)의 this 값을 상속(Lexical this)받습니다.

    화살표 함수의 this 참조는 바깥 렉시컬 환경의 this 바인딩으로 이어집니다. 중간에 다른 화살표 함수가 있어도 그 함수가 새로운 this를 만들지는 않습니다. 아래 메서드 안의 화살표 콜백은 그 메서드가 호출될 때 받은 this를 사용합니다.

    class ArrowGreeter {
      greeting: string = "Hello";
    
      sayGreetingLater() {
        setTimeout(() => {
          // 화살표 함수이므로, sayGreetingLater 메서드의 this를 상속받습니다.
          // 따라서 여기서 this는 ArrowGreeter 인스턴스를 가리킵니다.
          console.log(`${this.greeting}, world!`);
        }, 100);
      }
    }
    
    const ag = new ArrowGreeter();
    ag.sayGreetingLater(); // 약 100ms 후: Hello, world!

    ArrowGreeter 클래스의 sayGreetingLater 메서드 안에서 사용된 화살표 함수는 sayGreetingLater 메서드의 this를 그대로 상속받습니다. 덕분에 this.greeting이 올바르게 Hello 값을 참조할 수 있습니다. 이런 this의 자동 바인딩 특성 때문에 화살표 함수는 이벤트 핸들러나 콜백 함수에서 특히 편리합니다.


아래 다이어그램은 콜백과 메서드에서 this를 어떻게 다룰지 선택하는 흐름을 보여줍니다.

일반 함수의 this는 호출 receiver에 따릅니다. 실제 화살표 함수는 바깥 this를 캡처하고 bind는 지정 receiver를 고정합니다. this 타입 매개변수는 검사만 수행합니다.

TYPE / RUNTIME / CONTRACT

일반 함수는 호출 방식, 화살표 함수는 바깥 this

본문의 일반 타이머 콜백은 Greeter 인스턴스를 캡처하지 않습니다. 메서드 안의 실제 화살표 함수는 메서드가 받은 this를 유지합니다.

구현 형태에 따른 this 결정실제화살표구현인가라는선택에서예→바깥렉시컬this,아니오→일반호출receiver의3노드2간선이다. this타입표기는런타임분기나바인딩을추가하지않는다. bind는일반호출receiver를고정하는별도방법이다.실제 화살표함수인가?아니오예일반 함수호출 receiver화살표 함수바깥 렉시컬 this
구현 형태에 따른 this 결정 · 모바일실제화살표구현인가라는선택에서예→바깥렉시컬this,아니오→일반호출receiver의3노드2간선이다. this타입표기는런타임분기나바인딩을추가하지않는다. bind는일반호출receiver를고정하는별도방법이다.실제 화살표함수인가?아니오예 · 별도 경로일반 함수호출 receiver화살표 함수바깥 렉시컬 this

REGULAR FUNCTION

obj.method()의 receiver는 obj

메서드를 값으로 꺼내 전달하면 원래 객체가 자동 유지되지 않습니다. 타이머 receiver는 호스트마다 달라 TraditionalGreeter의 지연 호출 결과를 하나로 단정할 수 없습니다.

ACTUAL ARROW

메서드 안에서 바깥 this를 캡처

ArrowGreeter의 setTimeout(() => …)는 인스턴스 greeting을 사용해 Hello, world!을 출력합니다. call·bind로 화살표 함수의 this를 바꿀 수 없습니다.

BIND / IDENTITY

함수 참조를 한 번 만들어 보관

handler.handleClick.bind(handler)는 handler를 receiver로 고정합니다. 이벤트 제거 등에 같은 참조가 필요하면 만들어 둔 bound 함수를 보관합니다. 이 참조 관리 설명은 설계 확장입니다.

STATIC THIS

타입 표기가 자동 바인딩은 아닙니다

this: ButtonHandler는 JavaScript 매개변수에서 제거됩니다. 함수 타입 (message: string) => void도 실제 구현을 화살표 함수로 바꾸지 않습니다. MyLogger는 생성자에서 실제 화살표 함수를 할당합니다.

화살표 함수도 자신을 감싼 메서드가 잘못된 receiver로 호출되면 그 this를 캡처합니다. 필요한 receiver와 실제 호출 방식을 함께 확인합니다.


타입스크립트에서 this 타입 명시하기

타입스크립트는 함수의 this가 어떤 타입을 가리킬지 명시적으로 지정할 수 있는 기능을 제공합니다.

이는 특히 라이브러리를 만들거나 복잡한 콜백 패턴에서 this의 타입을 명확히 하고 싶을 때 유용합니다.

this 매개변수는 함수 매개변수 목록의 가장 첫 번째에 위치하며, 실제 자바스크립트 코드로는 컴파일되지 않습니다.

interface MyButton {
  label: string;
  onClick: (this: MyButton, event: Event) => void; // this가 MyButton 타입임을 명시
}

class ButtonHandler {
  buttonLabel: string = "클릭 미";

  // 이벤트 핸들러 함수
  // 첫 번째 매개변수로 this: ButtonHandler를 명시하여
  // 이 함수의 this가 ButtonHandler 인스턴스임을 알려줍니다.
  handleClick(this: ButtonHandler, event: Event) {
    console.log(`버튼 '${this.buttonLabel}'이 클릭되었습니다.`);
    console.log(`이벤트 타입: ${event.type}`);
  }
}

const handler = new ButtonHandler();

// 실제로 HTML 버튼에 연결되는 상황을 가정
// document.getElementById('myButton').addEventListener('click', handler.handleClick);

// 가상 호출:
// 만약 handler.handleClick이 버튼의 컨텍스트에서 호출되었다면 (DOM 이벤트)
// handleClick은 ButtonHandler의 this를 요구하므로 DOM 요소 receiver와 계약이 다릅니다.

// 올바르게 this를 바인딩하여 호출하는 방법
const boundHandleClick = handler.handleClick.bind(handler);
// boundHandleClick(new Event('click')); // 이 경우 this는 handler 인스턴스

// ---
// 다른 예시: 객체 리터럴 내에서 this 타입 명시
interface Calculator {
  value: number;
  add(this: Calculator, num: number): void;
}

const calc: Calculator = {
  value: 0,
  add(num) { // 여기서 this는 Calculator 타입으로 추론됩니다.
    this.value += num;
  },
};

calc.add(10);
console.log(calc.value); // 10

this: MyButton 또는 this: ButtonHandler와 같이 this 매개변수를 선언하면, 타입스크립트 컴파일러는 해당 함수가 호출될 때 this의 타입이 일치하는지 검사해줍니다.

이는 this의 컨텍스트가 중요한 호출을 정적으로 확인하는 데 도움이 됩니다. 타입 표기가 함수를 자동 bind하거나 DOM이 전달하는 receiver를 바꾸는 것은 아닙니다. 위의 주석 처리된 DOM 연결은 receiver 계약을 별도로 맞춰야 하며, 타이머 예제도 호스트의 호출 규칙을 유지합니다.


화살표 함수와 this 타입 명시의 조합

화살표 함수는 렉시컬 this를 사용하며 별도의 this 매개변수를 선언할 수 없습니다.

인터페이스의 (message: string) => void는 함수 타입 표기일 뿐 구현을 화살표 함수로 바꾸지 않습니다. 아래 클래스는 생성자에서 실제 화살표 함수를 할당하므로 인스턴스의 this를 캡처합니다.

interface Logger {
  logLevel: string;
  // 화살표 함수 문법으로 콜백 시그니처 정의
  logMessage: (message: string) => void;
}

class MyLogger implements Logger {
  logLevel: string = "INFO";
  logMessage: (message: string) => void;

  constructor() {
    // 화살표 함수를 사용하여 this를 MyLogger 인스턴스에 바인딩
    this.logMessage = (message: string) => {
      console.log(`[${this.logLevel}] ${message}`);
    };
  }
}

const logger = new MyLogger();
logger.logMessage("데이터 로드 완료."); // [INFO] 데이터 로드 완료.

// 일반 함수도 logger.logMessage(message)로 호출하면 logger가 this입니다.
// 함수를 분리해 콜백으로 전달하면서도 인스턴스의 this를 유지하려면 bind를 사용할 수 있습니다.
/*
class MyLoggerWithTraditionalFunction implements Logger {
  logLevel: string = "INFO";
  logMessage: (message: string) => void;

  constructor() {
    this.logMessage = function(message: string) {
      console.log(`[${this.logLevel}] ${message}`); // 아래 bind가 인스턴스의 this를 유지합니다.
    }.bind(this); // 분리해 전달해도 이 인스턴스의 this를 사용하도록 고정합니다.
  }
}
*/

화살표 함수 덕분에 this 바인딩에 대한 고민을 줄이고 더 간결하고 직관적인 코드를 작성할 수 있게 됩니다.


화살표 함수와 this의 관계는 자바스크립트 및 타입스크립트에서 매우 중요한 개념입니다.

화살표 함수가 렉시컬 this를 가진다는 점을 명확히 이해하면 콜백 함수나 비동기 코드에서 this 컨텍스트를 유지하는 데 큰 도움이 됩니다.

필요한 경우에는 타입스크립트의 this 타입 명시 기능을 활용하여 코드의 안정성을 더욱 높일 수 있습니다.

타이머 없이 호출 방식 비교하기

다음 예제는 브라우저·Node 타이머가 제공하는 receiver 차이를 제외하고, 객체 호출·call·bind와 실제 화살표 함수를 직접 비교합니다.

class Counter {
  value = 10;
  read(this: { value: number }): number { return this.value; }
  readArrow = (): number => this.value;
}
const counter = new Counter();
const read = counter.read;
const readArrow = counter.readArrow;
const boundRead = counter.read.bind(counter);
console.log(counter.read()); // 10
console.log(read.call({ value: 20 })); // 20
console.log(readArrow.call({ value: 20 })); // 10
console.log(boundRead.call({ value: 20 })); // 10
// read(); // this 계약에 맞는 receiver가 없어 정적 오류

메서드는 호출 때의 receiver를 읽고, readArrow는 인스턴스에서 만들어질 때 바깥 this를 사용합니다. boundRead는 일반 호출에서 고정한 counter를 사용합니다. this: { value: number }는 출력 JavaScript에서 제거되므로 런타임 receiver를 자동 생성하지 않습니다. 이 비교는 ECMAScript 화살표 함수의 렉시컬 this 규칙과 연결됩니다.

혼자 receiver를 추적하기

위 counter.value를 30으로 바꾼 뒤, read.call({ value: 7 }), readArrow(), boundRead()의 값을 예측하고 실행합니다. this 타입을 명시했다는 이유로 read를 단독 호출해도 원 counter가 자동 연결되는지도 판단하세요.

호출과 캡처 해설

각 결과는 7, 30, 30입니다. 일반 함수 read는 call로 넘긴 객체를 읽습니다. 화살표 함수와 boundRead는 같은 counter 인스턴스를 가리키므로 그 객체의 현재 value인 30을 읽습니다. 캡처·바인딩이 숫자 10을 영구 복사하는 것은 아닙니다. this 타입은 검사 계약이어서 단독 read() 호출에 receiver를 보충하지 않습니다.

이로써 3장 함수와 타입에 대한 모든 내용을 마쳤습니다.