JavaScript의 this란 무엇이고 왜 이렇게 헷갈릴까

Frontend

JavaScript를 공부하다 보면 꽤 빠른 시점에 this를 만나게 됩니다.

그런데 대부분 여기서부터 한 번쯤은 멈칫하게 됩니다.

  • 왜 어떤 함수 안의 this는 객체를 가리키고
  • 왜 어떤 함수 안의 thisundefined가 되고
  • arrow function은 또 다르게 동작하고
  • bind, call, apply가 필요한지
  • 왜 React나 이벤트 핸들러에서 this가 자꾸 헷갈리는지

이 주제는 초반에는 "현재 객체를 가리킨다"처럼 외우기 쉽습니다. 하지만 실무에서는 이 설명만으로 거의 항상 막히게 됩니다. 중요한 것은 this함수가 어디에 선언됐는가보다, 어떻게 호출됐는가에 더 크게 좌우된다는 점입니다.

이 글에서는 JavaScriptthis를 아래 흐름으로 정리해보겠습니다.

  1. this는 무엇인지
  2. 왜 그렇게 헷갈리는지
  3. 일반 함수, 메서드, arrow function은 어떻게 다른지
  4. bind, call, apply는 왜 필요한지
  5. class, 이벤트 핸들러, 콜백에서는 무엇을 조심해야 하는지

한눈에 보면

먼저 짧게 정리하면 이렇습니다.

  • this는 함수가 실행될 때 결정되는 현재 컨텍스트 참조값에 가깝습니다.
  • this는 변수처럼 lexical scope로 결정되지 않습니다.
  • 일반 함수의 this호출 방식에 따라 달라집니다.
  • 메서드 호출에서는 보통 점 앞의 객체가 중요하고, arrow function은 바깥 this를 그대로 따릅니다.
  • bind, call, apply는 함수의 this를 명시적으로 제어할 때 씁니다.

즉, this의 핵심은 "이 함수가 누구 안에 있나?"보다 이 함수가 지금 어떻게 호출됐나? 입니다.

this는 정확히 무엇일까?

가장 단순하게 말하면 this함수가 실행되는 현재 컨텍스트를 가리키는 특별한 참조값입니다.

예를 들어:

const user = {
  name: 'Marco',
  printName() {
    console.log(this.name);
  },
};
 
user.printName(); // Marco

여기서 thisuser처럼 보입니다.

그래서 처음에는 "this는 현재 객체"라고 이해하기 쉽습니다. 하지만 이 설명은 일부 상황에서만 맞습니다. 조금만 형태가 바뀌어도 결과가 달라집니다.

const user = {
  name: 'Marco',
  printName() {
    console.log(this.name);
  },
};
 
const fn = user.printName;
fn();

이제 this는 더 이상 직관적으로 user가 아닐 수 있습니다.

즉, this는 함수에 고정된 값이라기보다 호출 시점에 어떤 방식으로 실행됐는지에 따라 결정되는 값이라고 보는 편이 맞습니다.

this가 특히 헷갈릴까?

가장 큰 이유는 this가 일반 변수와 다르게 동작하기 때문입니다.

예를 들어 scope에서 본 변수들은 보통:

  • 어디서 선언됐는가
  • 어떤 바깥 스코프를 갖는가

가 중요합니다.

하지만 this는 그런 방식으로만 움직이지 않습니다.

즉:

  • scope는 선언 위치의 영향을 크게 받고
  • this는 호출 방식의 영향을 크게 받습니다

이 차이를 놓치면 기본기 전체가 섞여버립니다.

즉, this가 헷갈리는 이유는 단순히 복잡해서가 아니라, 다른 식별자 해석 규칙과 전혀 다른 축으로 움직이기 때문입니다.

일반 함수에서의 this

먼저 가장 기본적인 감각부터 보는 것이 좋습니다.

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

이 함수의 this는 함수가 어떻게 호출되느냐에 따라 달라집니다.

중요한 것은:

  • 함수 선언 위치보다
  • 호출 방식이 우선이라는 점입니다

예를 들어 같은 함수라도:

  • 그냥 호출할 때
  • 객체의 메서드처럼 호출할 때
  • call로 호출할 때

this가 달라질 수 있습니다.

즉, 일반 함수의 this는 함수 정의문만 봐서는 확정되지 않는 경우가 많습니다.

메서드 호출에서는 어떻게 될까?

가장 익숙한 형태입니다.

const user = {
  name: 'Marco',
  printName() {
    console.log(this.name);
  },
};
 
user.printName();

이 경우 보통 thisuser를 가리킵니다.

그래서 메서드 호출에서는 흔히:

  • obj.method()

형태에서 점 앞 객체가 중요하다고 설명합니다.

즉, this는 "함수가 어디에 들어 있나"보다 어떤 객체를 기준으로 호출됐나에 더 가깝습니다.

하지만 여기서도 한 가지를 더 봐야 합니다. 함수가 객체 안에 "들어 있다"는 사실만으로 this가 고정되는 것은 아닙니다.

const user = {
  name: 'Marco',
  printName() {
    console.log(this.name);
  },
};
 
const print = user.printName;
print();

이제 호출 방식이 달라졌기 때문에 this도 달라질 수 있습니다.

즉, 메서드 안의 함수라고 해서 항상 그 객체를 this로 갖는 것이 아니라, 그 함수가 실제로 메서드 호출 문맥에서 실행됐는가가 중요합니다.

함수 분리와 콜백에서 왜 자주 깨질까?

이 부분이 실무에서 정말 자주 문제를 만듭니다.

예를 들어:

const user = {
  name: 'Marco',
  printName() {
    console.log(this.name);
  },
};
 
setTimeout(user.printName, 100);

많은 사람이 처음엔 Marco를 기대합니다. 하지만 실제로는 기대와 다른 결과가 나올 수 있습니다.

왜냐하면 이 코드는 더 이상 user.printName()처럼 메서드 호출을 하고 있는 것이 아니기 때문입니다.

즉, 함수 참조만 전달되는 순간:

  • 원래 객체 문맥이 분리되고
  • 호출 주체가 바뀌며
  • this도 달라질 수 있습니다

이 감각이 없으면 콜백, 이벤트 핸들러, 비동기 코드에서 this가 자꾸 헷갈리게 됩니다.

this는 언제 결정될까?

이 질문도 중요합니다.

많은 경우 this함수가 호출될 때 결정된다고 보는 편이 맞습니다.

즉:

  • scope는 선언 위치 기준
  • this는 호출 시점 기준

이라는 구분을 반드시 잡아야 합니다.

예를 들어:

const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
 
function printName() {
  console.log(this.name);
}

이 함수는 어디에 선언됐는지가 핵심이 아니라, 나중에 어떤 방식으로 호출되느냐가 핵심입니다.

즉, this는 함수에 붙어 있는 고정 속성처럼 보기보다, 호출 문맥이 실행 시점에 주입하는 값처럼 이해하는 편이 더 정확합니다.

arrow function은 왜 다르게 느껴질까?

여기서 많은 혼란이 생깁니다.

arrow function은 일반 함수와 다르게 자기만의 this를 만들지 않습니다.

예를 들어:

const user = {
  name: 'Marco',
  printName: () => {
    console.log(this.name);
  },
};

이 코드는 메서드처럼 보여도 우리가 흔히 기대하는 메서드 this와 다르게 동작할 수 있습니다.

이유는 arrow function이:

  • 호출 방식에 따라 this를 새로 받는 것이 아니라
  • 바깥 lexical 환경의 this를 그대로 사용하기 때문입니다

즉, arrow functionthis는 일반 함수처럼 "누가 호출했는가"를 따라가지 않습니다.

arrow function은 이렇게 설계됐을까?

실무적으로 보면 목적이 꽤 분명합니다.

콜백 안에서 바깥 this를 그대로 유지하고 싶을 때가 많기 때문입니다.

예를 들어 예전에는 이런 코드가 많았습니다.

function Timer() {
  this.seconds = 0;
 
  setInterval(function () {
    this.seconds += 1;
  }, 1000);
}

여기서는 내부 함수의 this가 기대와 다를 수 있습니다.

그래서 과거에는:

  • const self = this
  • .bind(this)

같은 패턴을 많이 썼습니다.

반면 arrow function을 쓰면:

function Timer() {
  this.seconds = 0;
 
  setInterval(() => {
    this.seconds += 1;
  }, 1000);
}

바깥 this를 그대로 쓸 수 있습니다.

즉, arrow function은 메서드 대체용이라기보다, 콜백 안에서 바깥 문맥을 유지하는 데 강한 함수 형태에 더 가깝습니다.

그럼 메서드에는 arrow function을 쓰면 안 될까?

항상 안 되는 것은 아니지만, 보통 객체 메서드를 정의할 때는 일반 메서드 문법이나 일반 함수가 더 자연스럽습니다.

이유는 객체 메서드에서 기대하는 것은 보통:

  • 호출한 객체를 this로 받는 것

이기 때문입니다.

반면 arrow function은 자기 this를 만들지 않으므로, 객체 메서드처럼 쓰면 기대와 어긋날 수 있습니다.

즉, arrow function은 "더 최신 함수"가 아니라, this를 다르게 다루는 별도 도구라고 이해하는 편이 맞습니다.

bind, call, apply는 왜 필요할까?

이 세 가지는 함수의 this를 명시적으로 제어할 때 씁니다.

call

function printName() {
  console.log(this.name);
}
 
printName.call({ name: 'Marco' });

즉시 호출하면서 this를 지정합니다.

apply

function introduce(city: string, job: string) {
  console.log(this.name, city, job);
}
 
introduce.apply({ name: 'Marco' }, ['Seoul', 'Frontend Developer']);

call과 비슷하지만 인자를 배열 형태로 넘기는 감각입니다.

bind

function printName() {
  console.log(this.name);
}
 
const boundPrint = printName.bind({ name: 'Marco' });
boundPrint();

즉시 실행하지 않고, this가 고정된 새 함수를 만듭니다.

즉, 이 세 가지는 "함수 호출을 더 강하게 통제하고 싶을 때" 쓰는 도구입니다.

bind는 왜 실무에서 특히 중요할까?

콜백으로 함수를 넘길 때 문맥이 분리되는 문제가 자주 생기기 때문입니다.

예를 들어:

class Counter {
  count = 0;
 
  increment() {
    this.count += 1;
  }
}
 
const counter = new Counter();
const fn = counter.increment;

이 상태에서 fn()을 그냥 실행하면 기대와 다를 수 있습니다.

그래서:

const fn = counter.increment.bind(counter);

처럼 bind로 문맥을 고정하는 패턴이 나옵니다.

즉, bind는 단순 고급 기능이 아니라 함수 참조 분리로 인한 this 손실을 막는 실무 도구입니다.

class에서는 this를 어떻게 봐야 할까?

class에서도 결국 핵심은 같습니다. this는 인스턴스 메서드가 어떤 문맥으로 호출되느냐에 영향을 받습니다.

class User {
  name = 'Marco';
 
  printName() {
    console.log(this.name);
  }
}
 
const user = new User();
user.printName();

이 경우에는 보통 this가 인스턴스를 가리킵니다.

하지만 메서드를 분리하면 또 상황이 달라질 수 있습니다.

즉, class를 쓴다고 해서 this가 자동으로 영원히 안전해지는 것은 아닙니다.

이 때문에 실무에서는 아래 패턴이 자주 나옵니다.

  • 생성자에서 bind
  • 클래스 필드에 arrow function

예를 들어:

class User {
  name = 'Marco';
 
  printName = () => {
    console.log(this.name);
  };
}

이 방식은 바깥 인스턴스 문맥을 유지하기 쉬운 편입니다.

즉, 클래스에서도 결국 중요한 것은 문법보다 메서드를 분리해서 넘길 일이 있는가입니다.

이벤트 핸들러에서는 어떻게 될까?

브라우저 환경에서는 이벤트 핸들러의 this도 자주 헷갈립니다.

전통적인 함수 형태에서는:

  • 어떤 핸들러가 어떤 대상에 연결됐는지
  • 브라우저가 어떤 방식으로 호출하는지

에 따라 this가 달라질 수 있습니다.

반면 arrow function은 바깥 this를 그대로 따릅니다.

즉, 이벤트 코드에서 중요한 것은:

  • "이 함수의 this가 DOM 요소여야 하는가"
  • 아니면 "바깥 객체/인스턴스의 this를 유지해야 하는가"

를 먼저 구분하는 것입니다.

이 질문 없이 arrow function이나 일반 함수를 섞어 쓰면 의도와 어긋나기 쉽습니다.

thisscope는 어떻게 다를까?

이 부분은 기본기에서 반드시 분리해두는 편이 좋습니다.

scope

  • 선언 위치 기준
  • 어떤 식별자를 볼 수 있는가

this

  • 호출 방식 기준
  • 함수가 어떤 컨텍스트로 실행되는가

즉, 둘 다 "현재 문맥"처럼 느껴질 수 있지만, 사실은 완전히 다른 축입니다.

이 구분이 서 있지 않으면:

  • closure
  • arrow function
  • 메서드 호출
  • 콜백 버그

를 전부 한 덩어리로 헷갈리게 됩니다.

실무에서 자주 하는 실수

정리하면 아래 실수가 정말 자주 나옵니다.

  • this를 무조건 "현재 객체"라고 이해한다
  • 함수가 어디에 선언됐는지로 this가 결정된다고 생각한다
  • 메서드를 분리해 전달해도 같은 this가 유지될 거라 생각한다
  • arrow function을 메서드에 아무 생각 없이 사용한다
  • bind가 왜 필요한지 모르고 우회 코드만 늘린다

즉, this는 어렵다기보다 변수 해석 규칙과 다른 방식으로 움직인다는 점을 놓칠 때 헷갈립니다.

실무 체크리스트

실제로 코드를 볼 때는 아래 질문으로 빠르게 점검하면 도움이 됩니다.

  1. 이 함수는 지금 어떻게 호출되고 있는가?
  2. 이 코드에서 필요한 것은 호출 객체의 this인가, 바깥 lexical this인가?
  3. 메서드 참조를 분리해서 전달하고 있지는 않은가?
  4. arrow function을 써야 하는 이유가 분명한가?
  5. bind, call, apply로 문맥을 명시하는 편이 더 안전하지 않은가?

이 기준으로 보면 this는 신기한 예외 규칙이 아니라, 함수 호출 문맥을 읽는 도구로 보이기 시작합니다.

같이 보면 좋은 글

결론

JavaScriptthis는 함수 안에 자동으로 들어 있는 고정 값이라기보다, 함수가 어떤 방식으로 호출됐는지에 따라 결정되는 실행 문맥 참조에 가깝습니다.

짧게 정리하면:

  • 일반 함수의 this는 호출 방식이 중요하고
  • 메서드 호출에서는 호출 객체 문맥이 중요하며
  • arrow function은 자기 this를 만들지 않고
  • bind, call, applythis를 명시적으로 제어하는 도구입니다

결국 this를 이해하면 메서드 분리, 콜백 버그, 클래스 메서드, 이벤트 핸들러에서 왜 값이 달라지는지를 훨씬 자연스럽게 설명할 수 있게 됩니다.