화살표 함수의 렉시컬 this란?

화살표 함수가 자신만의 this를 갖지 않고 정의된 시점의 외부 스코프 this를 캡처하는 렉시컬 바인딩 원리를 학습합니다

중급 15분 화살표 함수 렉시컬 this 클로저 캡처

JavaScript에서 this는 함수가 호출되는 방식에 따라 동적으로 결정되는 것이 기본 원칙입니다. 그러나 화살표 함수(Arrow Function)는 이 원칙의 유일한 예외로, 자신만의 this를 생성하지 않고 정의된 시점의 외부 스코프에서 this를 캡처합니다. 이것을 렉시컬 this 바인딩이라고 부릅니다. 기존의 일반 함수에서 발생하던 this 유실 문제를 근본적으로 해결하기 위해 ES6에서 도입된 이 메커니즘은, 콜백 함수와 이벤트 핸들러에서 의도치 않게 this가 바뀌는 상황을 원천적으로 방지합니다. 화살표 함수의 렉시컬 this를 정확히 이해하면, bind, call, apply 같은 명시적 바인딩 없이도 안정적인 this 참조를 유지할 수 있습니다.

🎯 핵심 특징

  • 자체 this 미생성: 화살표 함수는 자신만의 this 바인딩을 갖지 않으며, 선언된 위치의 상위 스코프 this를 그대로 사용합니다
  • 정적 바인딩: 일반 함수와 달리 호출 방식(메서드 호출, 콜백, 직접 호출 등)에 관계없이 this가 변하지 않습니다
  • 클로저와 유사한 캡처 메커니즘: 변수가 클로저를 통해 외부 스코프의 값을 기억하듯, 화살표 함수도 외부 스코프의 this를 기억합니다
  • bind/call/apply 무효화: 화살표 함수에 bind, call, apply를 사용해도 this를 변경할 수 없으며, 항상 렉시컬 스코프의 this를 유지합니다
  • 프로토타입 부재: 화살표 함수는 prototype 속성이 없어 생성자 함수로 사용할 수 없고, new 키워드와 함께 호출하면 에러가 발생합니다

💡 실무에서의 영향

화살표 함수의 렉시컬 this는 현대 JavaScript 개발에서 가장 빈번하게 활용되는 패턴 중 하나입니다. React 클래스 컴포넌트에서 이벤트 핸들러를 작성할 때, 과거에는 생성자에서 this.handleClick = this.handleClick.bind(this)와 같은 바인딩 코드를 반복적으로 작성해야 했지만, 화살표 함수를 사용하면 이런 보일러플레이트 코드가 완전히 사라집니다. 또한 setTimeout, setInterval, Array.prototype.map 등의 콜백 함수에서 외부 객체의 메서드나 속성에 접근해야 할 때, 화살표 함수는 별도의 self = this 패턴이나 bind 호출 없이 자연스럽게 상위 컨텍스트의 this를 참조할 수 있게 해줍니다. 그러나 이 특성을 제대로 이해하지 못하면 객체의 메서드를 화살표 함수로 정의하거나, 프로토타입 메서드에 화살표 함수를 사용하는 등의 실수를 범할 수 있으며, 이는 this가 객체가 아닌 전역 스코프를 가리키는 예상치 못한 버그로 이어집니다. 렉시컬 this의 원리를 정확히 파악하는 것은 화살표 함수를 적재적소에 활용하고, 사용해서는 안 되는 상황을 판별하는 데 핵심적인 역량입니다.


핵심 개념

렉시컬 this 바인딩의 원리

입문

화살표 함수는 자기만의 this를 만들지 않고, 바깥에서 쓰던 this를 그대로 가져다 써요. 이걸 ‘렉시컬 this’라고 불러요.

📷 사진 속 배경처럼 고정되는 this 일반 함수의 this는 마치 거울 같아요. 누가 거울을 들고 있느냐에 따라 비치는 사람이 바뀌죠. 하지만 화살표 함수의 this는 사진과 같아요. 사진을 찍는 순간의 배경이 영원히 고정되듯, 화살표 함수가 만들어지는 순간의 this가 영원히 고정돼요.

🏠 집 주소를 기억하는 편지 여러분이 친구에게 편지를 쓸 때 “우리 집”이라고 적으면, 그 편지가 어디로 배달되든 “우리 집”은 항상 여러분의 집을 의미하죠? 화살표 함수도 마찬가지예요. 어디에서 실행되든 “this”는 항상 화살표 함수가 태어난 곳의 this를 가리켜요.

🎯 왜 ‘렉시컬’이라고 부르나요? ‘렉시컬(lexical)‘은 ‘글로 쓰여진 위치’라는 뜻이에요. 코드가 쓰여진 위치를 보면 this가 무엇인지 알 수 있다는 거예요. 일반 함수처럼 “누가 호출했는지”를 추적할 필요 없이, 그냥 코드를 읽기만 하면 되니까 훨씬 예측하기 쉬워요.

💡 한 줄 정리 화살표 함수의 this는 “어디서 호출되느냐”가 아니라 “어디서 만들어졌느냐”로 결정돼요. 마치 태어난 고향이 바뀌지 않는 것처럼요!

중급

화살표 함수는 자체적인 this 바인딩을 생성하지 않습니다. 대신, 화살표 함수가 정의된 시점에 자신을 감싸고 있는 외부 스코프의 this를 캡처하여 사용합니다. 이것을 **렉시컬 this 바인딩(Lexical this Binding)**이라고 합니다.

일반 함수의 this는 호출 시점에 동적으로 결정되지만, 화살표 함수의 this는 정의 시점에 정적으로 결정됩니다. 이 차이가 화살표 함수의 가장 핵심적인 특성입니다.

const obj = {
  name: 'Alice',
  // 일반 함수: this는 호출 방식에 따라 결정
  regular: function() {
    console.log(this.name); // 'Alice' (obj가 호출)
  },
  // 화살표 함수: this는 정의 위치의 외부 스코프 this
  arrow: () => {
    console.log(this.name); // undefined (외부 스코프 = 전역)
  }
};

obj.regular(); // 'Alice'
obj.arrow();   // undefined

위 예시에서 arrow는 객체 리터럴 내부에서 정의되었지만, 객체 리터럴은 자체 스코프를 생성하지 않습니다. 따라서 화살표 함수의 외부 스코프는 전역 스코프가 되고, this는 전역 객체(또는 strict mode에서 undefined)를 가리킵니다.

심화

화살표 함수의 렉시컬 this는 ECMAScript 명세에서 일반 함수와 근본적으로 다른 내부 슬롯 구조를 통해 구현됩니다.

ECMAScript 명세의 [[ThisMode]] 내부 슬롯 ECMAScript 2023 명세 10.2.1절(ECMAScript Function Objects)에 따르면, 모든 함수 객체는 [[ThisMode]] 내부 슬롯(Internal Slot, 엔진 내부에서만 접근 가능한 숨겨진 속성)을 가집니다. 이 슬롯은 세 가지 값 중 하나를 가집니다:

  • lexical: 화살표 함수에 설정되며, 자체 this 바인딩을 생성하지 않음
  • strict: strict mode 함수에 설정
  • global: non-strict mode 일반 함수에 설정

화살표 함수가 [[ThisMode]]: lexical로 설정되면, 명세 10.2.1.1절(PrepareForOrdinaryCall)에서 OrdinaryCallBindThis 단계를 건너뜁니다. 즉, 함수 실행 컨텍스트(Execution Context)의 환경 레코드(Environment Record)에 this 바인딩 자체가 생성되지 않습니다.

ResolveThisBinding의 스코프 체인 탐색 화살표 함수 내부에서 this를 참조하면 ResolveThisBinding 추상 연산(Abstract Operation, 명세에서 정의한 의사 함수)이 호출됩니다. 이 연산은 현재 실행 컨텍스트의 환경 레코드에서 HasThisBinding()을 호출하는데, 화살표 함수의 환경 레코드는 항상 false를 반환합니다. 그러면 외부 환경 참조(Outer Environment Reference)를 따라 스코프 체인을 올라가며 HasThisBinding()이 true를 반환하는 환경 레코드를 찾습니다. 이것이 바로 렉시컬 this의 실제 해결 메커니즘입니다.

V8 엔진의 구현 최적화 V8에서 화살표 함수는 파싱 단계에서 is_arrow 플래그가 설정됩니다. 바이트코드 생성 시 Ldar 명령어(Load Accumulator Register)가 현재 컨텍스트가 아닌 외부 컨텍스트의 this 슬롯을 직접 참조하도록 컴파일됩니다. TurboFan 최적화 컴파일러는 이를 더 최적화하여, 중첩 깊이가 정적으로 결정 가능한 경우 스코프 체인 탐색 없이 고정 오프셋(Fixed Offset)으로 직접 접근합니다.

콜백에서의 this 유실과 화살표 함수 해결

입문

일반 함수를 콜백으로 넘기면 this가 사라지는 문제가 있는데, 화살표 함수를 쓰면 이 문제가 깔끔하게 해결돼요.

📞 전화 돌려막기 문제 여러분이 친구에게 전화를 걸어서 “나 대신 선생님한테 전화해줘”라고 부탁한다고 상상해보세요. 친구가 선생님에게 전화할 때 “나(여러분)“가 아니라 “나(친구 자신)“로 소개하는 거예요. 이게 바로 일반 함수의 this 유실 문제예요!

🎒 이름표를 붙인 가방 화살표 함수는 마치 이름표가 단단히 붙어있는 가방 같아요. 누가 이 가방을 들고 다니든, 가방에 붙어있는 이름표(this)는 절대 바뀌지 않아요. 그래서 콜백으로 함수를 넘겨도 원래 주인이 누구인지 항상 알 수 있죠.

🔄 예전에는 어떻게 해결했나요? 화살표 함수가 없던 시절에는 var self = this처럼 this를 다른 변수에 미리 저장해두는 방법을 썼어요. 마치 “내 이름을 메모지에 적어서 가방에 넣어두는” 것과 같았죠. 화살표 함수는 이런 번거로운 과정을 없애줬어요.

💡 언제 화살표 함수를 쓰면 좋을까요? 타이머(setTimeout), 배열 처리(map, filter), 이벤트 처리 같은 곳에서 바깥의 this를 그대로 쓰고 싶을 때 화살표 함수를 쓰면 돼요. “바깥의 this가 필요하다” 싶으면 화살표 함수를 떠올리세요!

중급

일반 함수를 콜백으로 전달하면 호출 주체가 바뀌면서 this가 유실되는 문제가 발생합니다. ES6 이전에는 var self = this, .bind(this) 등의 패턴으로 우회했지만, 화살표 함수는 이 문제를 근본적으로 해결합니다.

class Timer {
  constructor() {
    this.seconds = 0;
  }

  // 문제: 일반 함수 콜백에서 this 유실
  startBroken() {
    setInterval(function() {
      this.seconds++; // TypeError: this는 전역 객체
    }, 1000);
  }

  // 해결: 화살표 함수로 this 유지
  startFixed() {
    setInterval(() => {
      this.seconds++; // 정상: this는 Timer 인스턴스
    }, 1000);
  }
}

ES6 이전의 우회 패턴들

화살표 함수 이전에는 다음과 같은 방법으로 this 유실을 해결했습니다:

  1. var self = this — 외부 this를 변수에 저장
  2. .bind(this) — 명시적으로 this를 고정
  3. 세 번째 인자 전달 — forEach(callback, thisArg)

화살표 함수는 이 모든 패턴을 대체하며, 코드의 의도를 더 명확하게 표현합니다.

function Counter() {
  this.count = 0;

  // 패턴 1: self = this
  var self = this;
  setTimeout(function() { self.count++; }, 100);

  // 패턴 2: bind
  setTimeout(function() { this.count++; }.bind(this), 100);

  // 패턴 3: 화살표 함수 (권장)
  setTimeout(() => { this.count++; }, 100);
}

심화

콜백에서의 this 유실은 ECMAScript의 함수 호출 규약과 실행 컨텍스트 생성 메커니즘에서 비롯되는 구조적 문제입니다.

OrdinaryCallBindThis와 콜백 호출의 관계 일반 함수가 콜백으로 전달되면 함수 객체 자체만 전달되며, 호출 시 참조(Reference Record, 속성 접근의 base 객체 정보를 담는 레코드)가 소실됩니다. ECMAScript 명세 13.3.7.1절(Runtime Semantics: Evaluation of CallExpression)에서, 일반적인 메서드 호출 obj.method()는 Reference Record의 base value를 thisValue로 사용하지만, 변수에 할당된 함수 const fn = obj.method; fn()은 Reference Record가 없으므로 thisValue가 undefined(strict mode) 또는 전역 객체(sloppy mode)로 설정됩니다.

setTimeout이나 Array.prototype.forEach 같은 호스트 함수가 콜백을 호출할 때도 동일한 원리가 적용됩니다. 콜백 함수의 Reference Record가 존재하지 않으므로 OrdinaryCallBindThis에서 thisValue 결정 시 원래 객체 컨텍스트가 사라집니다.

화살표 함수의 구조적 해결 메커니즘 화살표 함수가 이 문제를 해결하는 방식은 단순한 “this 고정”이 아니라, 아예 this 바인딩 단계를 건너뛰는 것입니다. OrdinaryCallBindThis 자체가 호출되지 않으므로, Reference Record의 유무와 관계없이 항상 외부 스코프의 this를 참조합니다. 이는 클로저(Closure)가 외부 변수를 캡처하는 것과 동일한 메커니즘으로, this를 일반 변수처럼 스코프 체인을 통해 해결합니다.

self = this 패턴과의 내부 동작 비교 var self = this 패턴은 this를 일반 변수로 변환하여 클로저로 캡처합니다. 화살표 함수도 내부적으로 동일하게 스코프 체인을 통해 this를 해결하므로, 두 방식의 런타임 동작은 사실상 동일합니다. 그러나 화살표 함수는 언어 수준에서 이를 보장하므로 개발자의 실수 가능성을 제거합니다.

bind/call/apply와 화살표 함수

입문

화살표 함수의 this는 한 번 정해지면 어떤 방법을 써도 바꿀 수 없어요. bind, call, apply라는 도구를 써도 소용없답니다!

🔒 자물쇠가 잠긴 금고 일반 함수의 this는 열쇠(bind, call, apply)로 바꿀 수 있는 금고 같아요. 하지만 화살표 함수의 this는 비밀번호가 영구적으로 설정된 금고예요. 아무리 다른 열쇠를 가져와도 비밀번호는 절대 바뀌지 않죠.

🎨 지워지지 않는 문신 일반 함수의 this가 옷을 갈아입을 수 있는 것이라면, 화살표 함수의 this는 마치 문신처럼 한 번 새겨지면 지울 수 없어요. bind로 새 옷을 입히려 해도, call로 강제로 바꾸려 해도 원래 그대로예요.

❓ 그러면 bind를 쓰면 에러가 나나요? 에러가 나지는 않아요! bind, call, apply를 화살표 함수에 사용할 수는 있지만, this를 바꾸는 기능만 무시될 뿐이에요. 다른 인자(arguments)는 정상적으로 전달돼요.

💡 왜 이렇게 만들었을까요? this가 예상치 못하게 바뀌는 게 많은 버그의 원인이었거든요. 그래서 화살표 함수는 아예 this를 바꿀 수 없게 만들어서, 코드를 읽기만 해도 this가 무엇인지 확신할 수 있도록 설계된 거예요.

중급

화살표 함수에 bind, call, apply를 사용하면 this를 변경하는 효과가 무시됩니다. 이들 메서드는 내부적으로 함수의 this를 교체하는 방식으로 동작하는데, 화살표 함수는 자체 this 바인딩이 없으므로 교체할 대상 자체가 존재하지 않습니다.

const obj = {
  name: 'Original',
  getNameArrow: null,
  init() {
    this.getNameArrow = () => this.name;
  }
};
obj.init();

const other = { name: 'Other' };

// 화살표 함수: this 변경 불가
console.log(obj.getNameArrow.call(other));  // 'Original'
console.log(obj.getNameArrow.apply(other)); // 'Original'
console.log(obj.getNameArrow.bind(other)()); // 'Original'

단, bindthis 외에 인자를 부분 적용(partial application)하는 용도로는 화살표 함수에서도 유효합니다.

const add = (a, b) => a + b;
const add5 = add.bind(null, 5); // this는 무시, 첫 번째 인자 고정
console.log(add5(3)); // 8

심화

bind/call/apply가 화살표 함수에서 this를 변경하지 못하는 것은 명세 수준에서 보장되는 동작입니다.

Function.prototype.bind의 내부 동작 ECMAScript 명세 20.2.3.2절(Function.prototype.bind)에서 bind는 BoundFunctionCreate 추상 연산을 호출하여 바운드 함수 이국 객체(Bound Function Exotic Object)를 생성합니다. 이 이국 객체는 [[BoundTargetFunction]](원본 함수), [[BoundThis]](바인딩할 this), [[BoundArguments]](바인딩할 인자)를 내부 슬롯으로 가집니다.

바운드 함수가 호출되면 [[Call]] 내부 메서드(명세 10.4.1.1절)가 실행되어, [[BoundThis]]를 thisArgument로 사용하여 원본 함수를 호출합니다. 그러나 원본 함수가 화살표 함수인 경우, OrdinaryCallBindThis에서 [[ThisMode]]: lexical 체크에 의해 thisArgument가 완전히 무시됩니다.

call/apply의 동일한 무효화 경로 Function.prototype.call(명세 20.2.3.3절)과 apply(명세 20.2.3.1절)도 동일한 경로를 따릅니다. 두 메서드 모두 최종적으로 [[Call]](thisArgument, argumentsList) 내부 메서드를 호출하며, 화살표 함수의 [[ThisMode]]: lexical에 의해 thisArgument 설정이 건너뛰어집니다.

V8의 최적화 관점 V8의 TurboFan 컴파일러는 화살표 함수에 대한 bind/call/apply 호출을 인라인 최적화할 때, this 바인딩 관련 코드를 완전히 제거(Dead Code Elimination)합니다. 이는 [[ThisMode]]: lexical이 정적 분석 시점에 확정되므로 가능한 최적화입니다. 결과적으로 화살표 함수에 bind를 호출해도 일반 함수 대비 추가적인 런타임 오버헤드가 발생하지 않습니다.

화살표 함수를 사용하면 안 되는 경우

입문

화살표 함수가 편리하지만, 모든 곳에서 쓰면 안 돼요. 사용하면 문제가 되는 상황들이 있답니다.

🏷️ 이름표가 필요 없는 곳에 이름표를 붙이면? 화살표 함수의 this는 바깥의 것을 가져온다고 했죠? 그런데 만약 “지금 이 물건의 주인이 누구야?”라고 물어봐야 하는 상황에서 화살표 함수를 쓰면, 엉뚱한 주인을 알려주게 돼요.

🏪 가게 주인이 바뀌어야 하는 상황 객체의 메서드(기능)를 만들 때는, 그 객체가 “나”가 되어야 해요. 예를 들어 가게의 “문 열기” 기능은 그 가게가 주체가 되어야 하죠. 화살표 함수를 쓰면 가게가 아니라 바깥 세상이 주체가 되어버려서 이상한 일이 벌어져요.

🏗️ 새로운 물건을 만드는 공장에서는 사용 불가 화살표 함수는 생성자(새로운 물건을 찍어내는 틀)로 사용할 수 없어요. 공장에서 새 제품을 만들려면 제품 설계도(prototype)가 필요한데, 화살표 함수에는 설계도가 없거든요. 그래서 new라는 키워드로 새 물건을 만들려고 하면 에러가 나요.

💡 간단한 판단 기준 “이 함수가 호출될 때 this가 바뀌어야 하나?”라고 자문해보세요. 바뀌어야 한다면 일반 함수, 바뀌면 안 된다면 화살표 함수를 쓰면 돼요!

중급

화살표 함수의 렉시컬 this는 강력하지만, 다음 상황에서는 일반 함수를 사용해야 합니다. 이 구분을 명확히 이해하는 것이 실무에서 버그를 예방하는 핵심입니다.

const user = {
  name: 'Alice',
  // 잘못된 사용: 화살표 함수
  greet: () => {
    return `Hello, ${this.name}`; // undefined (전역 this)
  },
  // 올바른 사용: 일반 함수 (또는 축약 메서드)
  greetCorrect() {
    return `Hello, ${this.name}`; // 'Alice'
  }
};
const Person = (name) => {
  this.name = name;
};

// TypeError: Person is not a constructor
const p = new Person('Alice');

화살표 함수를 피해야 하는 주요 상황

  1. 객체 리터럴 메서드: 객체 리터럴은 스코프를 생성하지 않으므로 this가 전역을 가리킴
  2. 생성자 함수: prototype이 없어 new 연산자 사용 불가
  3. 프로토타입 메서드: this가 인스턴스를 가리키지 못함
  4. DOM 이벤트 핸들러 (this가 요소를 가리켜야 할 때): addEventListener의 this는 이벤트 대상 요소여야 하는 경우
  5. arguments 객체 필요 시: 화살표 함수는 자체 arguments를 갖지 않음

심화

화살표 함수의 사용 제한은 ECMAScript 명세에서 의도적으로 설계된 것이며, 단순한 부작용이 아닌 언어 설계 철학의 반영입니다.

생성자 사용 불가의 명세적 근거 ECMAScript 명세 15.3절(Arrow Function Definitions)의 Static Semantics에서 화살표 함수는 [[Construct]] 내부 메서드(객체 생성을 담당하는 내부 메서드)를 갖지 않도록 정의됩니다. 명세 10.2.2절에서 함수 객체 생성 시 functionKindArrow이면 MakeConstructor 추상 연산이 호출되지 않습니다. 이로 인해:

  • prototype 속성이 생성되지 않음 (메모리 절약 효과)
  • new 연산자 사용 시 [[Construct]]가 없으므로 TypeError 발생
  • new.target 메타 속성도 화살표 함수 내에서 외부 스코프의 값을 참조

객체 리터럴 메서드에서의 this 해결 경로 분석 객체 리터럴 { method: () => this }에서 화살표 함수의 this가 객체를 가리키지 않는 이유는 명세의 환경 레코드 구조에 있습니다. 객체 리터럴의 평가(명세 13.2.6절, Runtime Semantics: PropertyDefinitionEvaluation)는 새로운 환경 레코드를 생성하지 않습니다. 따라서 화살표 함수가 스코프 체인을 탐색할 때 객체 리터럴을 건너뛰고, 객체 리터럴이 위치한 외부 환경의 this를 찾게 됩니다.

반면 축약 메서드(Concise Method) { method() {} }는 일반 함수로 생성되므로 [[ThisMode]]: strict가 설정되고, OrdinaryCallBindThis에서 호출 시 Reference Record의 base value(객체)가 this로 바인딩됩니다.

arguments와 super의 렉시컬 해결 화살표 함수가 갖지 않는 것은 this만이 아닙니다. arguments, super, new.target 모두 자체 바인딩 없이 외부 스코프에서 렉시컬하게 해결됩니다. 이들은 모두 환경 레코드의 특수 바인딩(Special Binding)으로, 화살표 함수의 환경 레코드는 이 바인딩들을 생성하지 않도록 설계되어 있습니다. 이는 화살표 함수를 “경량 함수(Lightweight Function)“로 만들어 메모리 풋프린트를 줄이는 설계 의도를 반영합니다.