Published on

this는 어떤 규칙으로 결정될까?

Authors
  • avatar
    Name
    GW

개요

JavaScript를 공부하다 보면 this만큼 헷갈리는 개념도 드뭅니다.

처음에는 대부분 이렇게 생각합니다.

this는 현재 함수를 실행하고 있는 자기 자신을 가리키는 거 아닌가?

또는

this는 함수가 선언된 객체를 가리키는 거겠지?

하지만 실제로는 둘 다 정확한 설명이 아닙니다.

예를 들어 다음 코드를 보겠습니다.

const person = {
  name: '철수',

  sayHello() {
    console.log(this.name);
  },
};

person.sayHello();

실행 결과는 다음과 같습니다.

철수

여기까지는 자연스럽습니다.

그런데 이번에는 함수를 변수에 저장한 뒤 실행해 보겠습니다.

const person = {
  name: '철수',

  sayHello() {
    console.log(this.name);
  },
};

const hello = person.sayHello;

hello();

같은 함수인데 실행 결과가 달라집니다.

strict mode에서는 TypeError 또는 undefined가 출력됩니다.

non-strict mode에서는 undefined 또는 전역 객체의 값이 출력될 수도 있습니다.

왜 이런 일이 발생하는 걸까요?

this가 함수가 만들어질 때 결정되는 것이 아니라, 함수를 어떻게 호출했는지에 따라 결정되기 때문입니다.

이번 글에서는 다음 내용을 순서대로 살펴보겠습니다.

  • this란 무엇인지
  • this는 언제 결정되는지
  • 일반 함수와 메서드에서 어떻게 달라지는지
  • 왜 호출 방식이 중요한지

this란 무엇일까?

this는 함수를 호출한 객체를 참조하기 위해 JavaScript가 실행 시점에 결정하는 값입니다.

많은 사람들이 "this는 함수 자신이다." 또는 "this는 함수가 선언된 객체다."라고 생각하지만 그렇지 않습니다.

JavaScript에서는 함수가 어떻게 호출되었는지에 따라 this가 달라집니다.

함수가 어디에서 만들어졌는가
❌ 중요하지 않음

함수가 어떻게 호출되었는가
✅ 매우 중요

즉, 이것이 this를 이해하는 가장 중요한 원칙입니다.

this는 언제 결정될까?

this를 이해할 때 가장 많이 하는 오해가 있습니다.

함수를 만들 때 this가 정해지는 것 아닌가?

그렇지 않습니다. this는 함수를 선언할 때가 아니라 호출할 때 결정됩니다.

예를 들어 다음 코드를 보겠습니다.

const person = {
  name: '철수',

  sayHello() {
    console.log(this.name);
  },
};

여기까지는 아무 일도 일어나지 않았습니다.

JavaScript는 객체를 만들고 함수를 만들었을 뿐입니다.

아직 this는 결정되지 않았습니다.

그 이후에 person.sayHello()처럼 함수를 호출하면,

JavaScript는 "이 함수를 person 객체가 호출했네."라고 판단하고 this === person으로 결정합니다.

즉, 이런 순서로 동작합니다.

함수 생성
↓
this 미정
↓
함수 호출
↓
this 결정

일반 함수에서 this는 어떻게 결정될까?

먼저 가장 단순한 일반 함수부터 살펴보겠습니다.

const sayHello = () => {
  console.log(this);
};

sayHello();

잠깐. 여기서 한 가지 알아둘 점이 있습니다.

화살표 함수는 자신만의 this를 가지지 않습니다.

그래서 일반 함수의 this를 설명할 때는 일반 함수 문법을 사용해야 합니다.

function sayHello() {
  console.log(this);
}

sayHello();

strict mode에서는 undefined가 출력되고, non-strict mode(브라우저 기준)에서는 window가 출력됩니다.

왜 이런 차이가 생길까요?

일반 함수는 누가 호출했는지를 기준으로 this를 결정합니다.

그런데 sayHello()처럼 단독으로 호출하면 이 함수를 호출한 객체가 없습니다.

따라서 strict mode에서는 this가 undefined가 되고, non-strict mode에서는 전역 객체 window로 자동 변환됩니다.

그래서 일반 함수에서는 "객체 없이 호출하면 this도 객체를 가리킬 수 없습니다."

메서드에서 this는 어떻게 결정될까?

이번에는 객체 안에서 호출해 보겠습니다.

const person = {
  name: '철수',

  sayHello() {
    console.log(this.name);
  },
};

person.sayHello();

이번에는 철수가 출력됩니다.

왜일까요?

JavaScript는 person.sayHello()를 보는 순간 "sayHello를 호출한 객체는 person이네" 라고 판단합니다.

그래서 this === person이 됩니다.

여기서 중요한 점은 많은 사람들이 "객체 안에 있으니까 person을 가리킨다." 라고 생각한다는 것입니다.

하지만 정확한 이유는 아래와 같습니다.

person 객체가 이 함수를 호출했기 때문입니다.

함수가 객체 안에 있기 때문이 아니라, 호출한 주체가 person이기 때문입니다.

이 차이를 이해해야 다음 예제가 자연스럽게 이해됩니다.

다음 코드에서는 같은 함수를 변수에 저장한 뒤 실행해 보겠습니다.

const person = {
  name: '철수',

  sayHello() {
    console.log(this.name);
  },
};

const hello = person.sayHello;

hello();

분명 같은 함수인데 이번에는 철수가 출력되지 않습니다.

왜 이런 일이 발생할까요?

다음으로는 같은 함수인데도 호출 방식만 바뀌면 this가 달라지는 이유를 살펴보겠습니다.

this는 호출한 방식이 중요하다

앞에서 같은 함수인데도 호출 방식이 바뀌면 this가 달라질 수 있다고 했습니다.

왜 그럴까요?

JavaScript는 함수가 호출되는 순간 . 앞에 있는 객체가 누구인지를 기준으로 this를 결정합니다.

예를 들어 다음 코드를 보겠습니다.

const person = {
  name: '철수',

  sayHello() {
    console.log(this.name);
  },
};

person.sayHello();

JavaScript는 이 코드를 다음처럼 이해합니다.

person.sayHello()
↓
`.` 앞의 객체 확인
↓
person
↓
this === person

하지만 이번에는 함수를 변수에 저장해 보겠습니다.

const person = {
  name: '철수',

  sayHello() {
    console.log(this.name);
  },
};

const hello = person.sayHello;

hello();

실행 결과는 non-strict mode에서는 undefined가 출력되고, strict mode에서는 TypeError가 발생합니다.

많은 사람들이 "같은 함수인데 왜 결과가 달라지지?" 라고 생각합니다.

사실 함수는 그대로입니다. 바뀐 것은 호출 방식뿐입니다.

첫 번째는 person.sayHello()처럼 객체가 호출했고, 두 번째는 hello()처럼 일반 함수로 호출했습니다.

따라서 JavaScript도 서로 다른 규칙을 적용하는 것입니다.

여기서 기억해야 하는 가장 중요한 문장은 하나입니다.

this는 함수를 어디서 선언했는 지가 아니라, 함수를 호출한 방식에 따라 결정됩니다.

call, apply, bind는 왜 존재할까?

그렇다면 "this를 내가 직접 정할 수는 없을까?" 라는 생각이 들 수도 있습니다.

이럴 때 사용하는 것이 call, apply, bind입니다.

call

function sayHello() {
  console.log(this.name);
}

const person = {
  name: '철수',
};

sayHello.call(person);

실행 결과는 철수가 출력됩니다.

call()의 첫 번째 인수는 this로 사용할 객체입니다.

즉, this === person으로 강제로 지정해서 실행한 것입니다.

apply

function introduce(age) {
  console.log(`${this.name} ${age}`);
}

const person = {
  name: '철수',
};

introduce.apply(person, [20]);

apply도 call과 거의 동일합니다.

차이점은 인수를 배열로 전달한다는 점입니다.

bind

bind는 조금 다릅니다.

function sayHello() {
  console.log(this.name);
}

const person = {
  name: '철수',
};

const hello = sayHello.bind(person);

hello();

실행 결과는 철수가 출력됩니다.

bind는 함수를 바로 실행하지 않습니다.

대신 "this가 person으로 고정된 새로운 함수를 만들어 반환합니다."

그래서 이벤트 핸들러나 콜백에서 많이 사용됩니다.

화살표 함수의 this

일반 함수와 달리 화살표 함수는 자신만의 this를 가지지 않습니다.

예를 들어 다음 코드를 살펴보겠습니다.

const person = {
  name: '철수',

  sayHello: () => {
    console.log(this.name);
  },
};

person.sayHello();

person.sayHello()로 호출했기 때문에 this가 person을 가리킬 것 같지만 그렇지 않습니다.

화살표 함수는 일반 함수처럼 호출 방식에 따라 자신의 this를 결정하지 않습니다.

대신 자신이 선언된 바깥 스코프의 this를 그대로 사용합니다.

따라서 객체의 메서드에서 해당 객체를 this로 사용해야 한다면 일반 함수를 사용하는 것이 적절합니다.

const person = {
  name: '철수',

  sayHello() {
    console.log(this.name);
  },
};

person.sayHello();

이 경우 sayHello는 일반 함수이고 person의 메서드로 호출되었기 때문에 this는 person을 가리킵니다.

person.sayHello()
↓
person의 메서드로 호출
↓
this === person

그렇다면 화살표 함수의 this는 어디에서 가져오는 걸까요?

다음 예제를 살펴보겠습니다.

const person = {
  name: '철수',

  sayHello() {
    const printName = () => {
      console.log(this.name);
    };

    printName();
  },
};

person.sayHello();

실행 결과는 다음과 같습니다.

철수

person.sayHello()가 호출되면 일반 함수인 sayHello의 this는 person이 됩니다.

하지만 그 안에 선언된 printName은 화살표 함수이므로 자신의 this를 만들지 않고 바깥의 this를 그대로 사용합니다.

즉, 위 예제에서 printName은 sayHello 함수 내부에 선언되어 있으므로, sayHello에서 사용되는 this를 그대로 사용합니다.

person.sayHello()
↓
sayHello의 this === person
↓
printName은 자신의 this를 가지지 않음
↓
바깥 sayHello의 this 사용
↓
this === person
↓
this.name === '철수'

따라서 printName 안에서 this.name에 접근하면 person.name인 '철수'가 출력됩니다.

즉, 일반 함수와 화살표 함수의 this는 다음과 같은 차이가 있습니다.

일반 함수
└─ 호출 방식에 따라 this 결정

화살표 함수
└─ 자신의 this를 가지지 않음
   └─ 바깥 스코프의 this 사용

화살표 함수의 this를 이해할 때는 "누가 이 함수를 호출했는가?"가 아닌 "이 화살표 함수를 감싸고 있는 바깥의 this는 무엇인가?" 를 확인하는 것이 중요합니다.

정리

처음에는 많은 사람들이 "this는 현재 객체를 가리킨다." 라고 외웁니다.

하지만 이것만으로는 대부분의 this 관련 코드를 설명할 수 없습니다.

this를 이해하는 가장 중요한 원칙은 하나입니다.

this는 함수를 호출한 방식에 따라 결정된다.

핵심을 정리하면 다음과 같습니다.

  • this는 실행 중인 함수 자신을 의미하지 않습니다.
  • this는 함수를 호출한 객체를 참조하기 위해 사용됩니다.
  • this는 함수를 만들 때가 아니라 호출할 때 결정됩니다.
  • 일반 함수와 메서드는 서로 다른 방식으로 this를 결정합니다.
  • call, apply, bind를 사용하면 this를 직접 지정할 수 있습니다.
  • 화살표 함수는 자신의 this를 가지지 않고 바깥 스코프의 this를 사용합니다.

앞으로 JavaScript에서 this가 헷갈린다면 복잡한 규칙을 떠올리기보다 먼저 다음 질문을 해보면 됩니다.

이 함수는 어떤 방식으로 호출되었을까?

대부분의 this는 이 질문 하나만으로도 자연스럽게 이해할 수 있습니다.

참고 자료