📚 CS/JavaScript

[JavaScript] this 바인딩

dev.daisy 2025. 8. 30. 21:06
이번에 this 바인딩 규칙을 정리하면서, 단순히 ‘전역에서는 window', '메서드에서는 객체’ 정도로만 이해했던 개념이 훨씬 복잡하고 상황에 따라 다르게 동작한다는 것을 깨달았습니다. 특히 함수 참조 분리 시 this 소실, ES 모듈에서의 this = undefined, 그리고 이벤트 핸들러에서의 this 차이(일반 함수 vs 화살표 함수) 같은 부분은 실제 코드에서 자주 마주칠 수 있는 상황이라고 느껴 실제로 개발하면서도 적절하게 사용해보려고 합니다.

 

JavaScript에서 this함수 또는 메서드 내에서 동작하며, 호출되는 방식에 따라 값이 동적으로 결정되는 실행 컨텍스트의 참조 객체입니다. 따라서 함수가 어디서 정의되었는지가 아니라 어떻게 호출되었는지에 따라 값이 달라집니다.

  • 전역에서는 전역 객체(window / global, useStrict  undefined)
  • 객체의 메서드 호출 시에는 호출 시점에서 가장 가까운 객체
  • 생성자 호출 시에는 새로 만들어진 인스턴스
  • 화살표 함수에서는 상위 스코프의 this

이처럼 상황마다 값이 바뀌는 것이 JavaScript this의 가장 큰 특징입니다.


this 바인딩 규칙

JavaScript의 this함수가 어떻게 호출되었는지에 따라 값이 정해지는 실행 컨텍스트의 수신자(receiver) 입니다. 어디서 정의됐느냐가 아니라 누가 그 함수를 불렀는가가 중요합니다. 이 규칙은 한 가지가 아니라 여러개가 중첩되어 사용할 수 있기 때문에 호출 형태에 따라 우선순위를 갖습니다. 보통 new 바인딩 > 명시적 바인딩(call/apply/bind) > 암묵적 바인딩(객체.메서드()) > 기본 바인딩(그 외 일반 호출) 순으로 결정되고, 화살표 함수는 예외적으로 자신만의 this를 갖지 않고 정의된 시점의 상위 스코프 this(lexical this)를 캡처합니다.

1) 전역 컨텍스트

전역 공간에서의 this는 실행 환경에 따라 다르게 적용됩니다.

  • 브라우저에서는 window, Node.js에서는 global
  • ES 모듈 최상단에서는 엄격 모드가 기본 적용 → undefined

가장 바깥에 존재하는 전역 컨텍스트에서의 this는 실행 환경에 따라 달라집니다. 브라우저의 스크립트 태그 최상단에서는 전역 객체(window)를 가리키지만, ES 모듈 최상단과 엄격 모드("use strict")에서는 undefined가 됩니다. 

console.log(this); // 브라우저: window, Node.js: global

"use strict";
function foo() {
  console.log(this); // undefined
}
foo();

2) 기본 바인딩

함수를 독립적으로 호출하면 this전역 객체를 가리키며, 

 

기본 바인딩은 함수가 아무 대상에도 속하지 않은 일반 호출일 때 적용됩니다. 예를 들어 func()처럼 독립적으로 호출하면, 비엄격 모드에서는 전역 객체, strict mode에서는 undefinedthis가 됩니다. 그래서 엄격 모드에서는 일반 함수가 this에 의존하지 않도록 주의해야합니다.

function func() {
  console.log(this);
}
func(); // window 또는 undefined

3) 메서드로서 호출

메서드 내부의 this는 해당 메서드를 호출하는 시점에서 가장 가까운 객체를 가리킵니다.

특정 객체와 연계되어 동작할 때 this는 호출한 객체가 됩니다.

var obj = {
  value: 10,
  getValue: function() {
    console.log(this.value);
  }
};
obj.getValue(); // 10

4) 메서드 내부 함수

메서드 내부에서 선언된 일반 함수는 독립적으로 호출되므로 this는 전역 객체를 가리킵니다. 이를 해결하려면 bind를 사용하거나 화살표 함수를 사용할 수 있습니다.

  • bind → 명시적으로 this를 강제로 고정
  • 화살표 함수 → 선언 시점의 상위 스코프 this를 암묵적으로 따라감
var obj = {
  outer: function() {
    function inner() {
      console.log(this);
    }
    inner(); // window 또는 undefined
  }
};
obj.outer();

 

1) bind 사용 예시

bind(this)를 해주면 inner는 항상 obj를 가리키게 됩니다.

var obj = {
  outer: function () {
    function inner() {
      console.log(this);
    }
    const boundInner = inner.bind(this); // outer의 this(obj)를 강제로 바인딩
    boundInner(); // obj
  },
};

obj.outer();

 

2) 화살표 함수 사용 예시

화살표 함수는 자기만의 this를 갖지 않고 정의될 당시의 상위 스코프(코드의 outer 메서드)의 this를 그대로 사용합니다.

var obj = {
  outer: function () {
    const inner = () => {
      console.log(this);
    };
    inner(); // obj
  },
};

obj.outer();

5) new 바인딩 - 생성자 호출

생성자 함수에서 this는 new 키워드와 함께 호출될 때 새로 생성된 인스턴스를 가리킵니다. 이 과정에서 this에 바인딩된 속성과 메서드가 인스턴스에 할당됩니다.

 

new 바인딩은 생성자를 호출할 때 작동합니다. new Foo()처럼 부르면, 엔진이 새로운 빈 객체를 만들고 그 객체를 this로 설정한 뒤 함수 본문을 실행합니다. 결과적으로 인스턴스 필드가 그 새 객체에 붙습니다. 다른 규칙보다 우선순위가 제일 높아서 new foo.call(x)처럼 섞어서 사용해도 도 최종 this는 새 인스턴스가 됩니다.

 

* 화살표 함수는 생성자 역할을 하지 못하기 때문에 new로 호출할 수 없습니다.

function Person(name) {
  this.name = name;
}
const p1 = new Person("Daeun");
console.log(p1.name); // "Daeun"

6) 명시적 바인딩 - 화살표 함수

화살표 함수는 자신만의 this를 가지지 않고, 선언될 당시 상위 스코프의 this를 그대로 사용합니다. 따라서 call이나 bindthis를 바꿀 수 없습니다.

const obj = {
  value: 42,
  normal: function() {
    const arrow = () => console.log(this.value);
    arrow();
  }
};
obj.normal(); // 42

 

call과 bind로 화살표 함수 값 변경을 시도한다면?

const obj = {
  value: 42,
  normal: function() {
    const arrow = () => console.log(this.value);
    arrow.call({ value: 100 });   // (1) call 시도
    arrow.bind({ value: 200 })(); // (2) bind 시도
  }
};

obj.normal();

 

실행 결과

(1) 42
(2) 42

 

→ 화살표 함수는 자신만의 this를 갖지 않아서 call이나 bind로 this를 사용해도 값이 변경되지 않습니다. 코드에서 arrow 함수는 선언된 순간 상위 스코프(normal 함수)의 this(obj)를 캡쳐하기 때문에 arrow.call(...), arrow.bind(...)로 바꾸려고 해도 무시되고, 계속 obj.value를 참조합니다.

 

하지만 반대로 normal 자체를 call이나 bind 하면 동작이 달라집니다.

obj.normal.call({ value: 400 });
// 출력: 400

 

이때는 normal의 this가 변경되고, 그 안에서 정의된 화살표 함수는 바뀐 this인 value:400을 캡처하기 때문입니다.

 

헷갈리기 쉬운 부분

위 코드에서 call이나 bind로 this를 사용해도 무시된다고 했는데 console.log가 2번 찍힌다는 것 자체가 call / bind가 뭔가 영향을 주는게 아닌가? 라는 생각이 들어 실제 동작 원리를 찾아보았습니다.

 

(1) arrow.call({ value: 100 });   // call 시도

→ call은 this를 바꾸려고 하지만, 화살표 함수는 이미 상위 스코프(this=obj)를 캡처해버려서 바뀌지 않습니다. 그래도 함수가 실행되기 때문에 console.log가 찍혀 값은 42가 출력됩니다.


(2) arrow.bind({ value: 200 })(); // bind 시도

bind도 마찬가지로 새로운 함수가 반환되지만 원본 함수의 this가 이미 고정되어 있어서 여전히 obj.value = 42가 출력됩니다.

 

무시된다 = '함수 실행 자체가 안 된다'가 아니라 'this 변경 효과가 없다' 라는 의미입니다.


7) 명시적 바인딩

JavaScript는 call, apply, bind 메서드를 사용해 this를 명시적으로 바인딩할 수 있습니다. call과 apply함수를 즉시 실행시키면서 this를 바꾸고, bind새로운 함수를 반환하여 this를 영구적으로 묶습니다.

function greet(msg) {
  console.log(msg, this.name);
}
greet.call({ name: "Daeun" }, "Hi");   // Hi Daeun
greet.apply({ name: "Daeun" }, ["Hey"]); // Hey Daeun
const bound = greet.bind({ name: "Daeun" });
bound("Hello"); // Hello Daeun

8) 콜백 함수에서의 this

배열 메서드의 콜백 함수는 기본적으로 전역 객체를 가리키지만, 두 번째 인자로 this를 직접 지정할 수 있습니다. 또는 bind를 사용하여 this를 고정할 수도 있습니다.

const obj = {
  count: 0,
  increase() {
    [1, 2, 3].forEach(function() {
      this.count++;
    }, this);
  }
};
obj.increase();
console.log(obj.count); // 3

 

콜백 함수의 특징

1. 자신이 직접 inner()처럼 호출하지 않습니다. 대신 다른 함수가 호출해줍니다.

2. 호출되는 맥락(this)는 누가 호출해주냐에 따라 달라집니다.

  • 일반적인 함수 전달 → 기본적으로 전역 / undefined
  • forEach(..., this) 두 번째 인자로 전달 → 우리가 원하는 객체에 묶임
  • bind 사용 → 우리가 원하는 객체에 강제로 묶임

 

콜백 함수란?
말 그대로 다른 함수에 인자로 전달되어, 나중에 그 함수 안에서 호출되는 함수를 의미합니다,
'내가 직접 지금 호출하는 함수'가 아니라, '다른 함수가 필요할 때 대신 호출해주는 함수'입니다. 대표적으로는 forEach, map, filter, setTimeout, addEventListener 등에 전달하는 함수들이 전부 콜백 함수의 예시입니다. 위 코드의 function() { this.count++; } 이 바로 콜백 함수인데, forEach가 내부적으로 반복하면서 이 함수를 대신 실행해주는 구조이기 때문입니다.

 


this 바인딩 문제를 관리하고 명시적으로 제어하는 방법

1) call() 메서드

Function.prototype.call(thisArg[, arg1[, arg2[...]]]) 형태로 사용되며, 첫 번째 매개변수로 전달된 thisArg를 this에 바인딩함과 동시에 함수를 즉시 실행시킵니다.

2) apply() 메서드

Function.prototype.apply(thisArg[, argsArray]) 형태로 사용되며, call() 메서드와 동일하게 thisArg를 this에 바인딩하고 함수를 즉시 실행시킵니다. 차이점은 함수 자체의 매개변수를 Array 형태로 입력받는다는 점입니다.

3) bind() 메서드

Function.prototype.bind(thisArg[, arg1[, arg2[...]]]) 형태로 사용되며, 함수의 this를 thisArg로 영구적으로 변경하고, 함수를 즉시 실행하지 않고 새로운 함수를 반환합니다.

4) 콜백 함수에서의 this 바인딩

ES6에서 추가된 배열 메서드(Array.map, filter, reduce)는 콜백 함수 내부에서 this를 어떤 것으로 할지를 두 번째 매개변수로 받을 수 있습니다. bind() 함수를 사용해서도 콜백 함수의 this를 바인딩할 수 있습니다.


실제 사용 예시

1) React 클래스 컴포넌트에서의 this 바인딩

클래스 컴포넌트 메서드는 자동으로 this가 바인딩되지 않습니다. 이벤트 핸들러에서 this를 사용하려면 생성자에서 bind를 하거나, 화살표 함수로 정의해야 합니다.

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this); // 바인딩 필수
  }

  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return <button onClick={this.handleClick}>+</button>;
  }
}

2) 브라우저 이벤트 핸들러

DOM 이벤트 리스너에서 일반 함수는 this가 이벤트 타깃을 가리키지만, 화살표 함수상위 스코프의 this를 따릅니다.

const btn = document.querySelector("button");

btn.addEventListener("click", function() {
  console.log(this); // <button> 엘리먼트
});

btn.addEventListener("click", () => {
  console.log(this); // window (상위 스코프의 this)
});

3) 콜백 함수 내부의 this (배열 메서드)

forEach 같은 고차 함수에서 this를 명시적으로 지정하지 않으면 전역 객체를 가리키게 됩니다. 두 번째 인자나 bind로 제어할 수 있습니다.

const counter = {
  num: 0,
  inc() {
    [1, 2, 3].forEach(function() {
      this.num++;
    }, this); // 두 번째 인자로 this 전달
  }
};
counter.inc();
console.log(counter.num); // 3

보조 개념 정리

  • 엄격 모드 (Strict Mode): "use strict" 선언 시 일반 함수 호출에서 this가 전역 객체가 아닌 undefined로 바인딩
  • 바인딩 소실 (Lost Binding): 객체의 메서드를 변수에 할당 후 독립 실행하면 this 연결이 끊어져 전역 객체(또는 undefined)를 가리키는 현상
  • 암묵적 바인딩 (Implicit Binding): 메서드를 객체를 통해 호출할 때 자동으로 this가 그 객체에 바인딩되는 규칙
  • 명시적 바인딩 (Explicit Binding): call, apply, bind와 같은 내장 메서드를 사용하여 this를 강제로 지정하는 방식