Frontend Development

JavaScript의 this 바인딩: 상황별 동작 원리

Kun Woo Kim 2025. 7. 7. 10:08
728x90

JavaScript를 학습하면서 가장 헷갈리는 개념 중 하나가 바로 this입니다. 다른 언어와 달리 JavaScript의 this는 함수가 호출되는 방식에 따라 값이 달라지기 때문에 많은 개발자들이 어려움을 겪습니다. 이 글에서는 다양한 상황에서 this가 어떻게 바인딩되는지 6가지 핵심 상황을 통해 자세히 알아보겠습니다.


this 바인딩의 기본 원리

JavaScript에서 this함수가 호출되는 방식에 따라 결정됩니다. 이는 함수가 정의된 위치가 아닌, 실행되는 순간의 호출 방식이 중요하다는 의미입니다. 마치 전화를 걸 때 상황에 따라 다른 사람과 연결되는 것과 비슷합니다.


1. 전역 호출 (Global Invocation)

전역에서 함수가 호출되면, this는 전역 객체를 참조합니다.

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

globalFunc(); // 브라우저: window, Node.js: global

실행 환경별 차이점

환경 this 값 설명
브라우저 window DOM과 관련된 모든 전역 객체
Node.js global Node.js 런타임 전역 객체
Strict Mode undefined 엄격 모드에서는 전역 객체 대신 undefined
"use strict";
function strictFunc() {
  console.log(this); // undefined
}
strictFunc();

2. 메서드 호출 (Method Invocation)

객체의 메서드로 호출된 함수에서는 this가 해당 객체를 참조합니다. 이는 가장 직관적인 동작 방식입니다.

const user = {
  name: "김철수",
  age: 25,
  greet: function() {
    console.log(`안녕하세요, 저는 ${this.name}입니다.`);
    console.log(`나이는 ${this.age}살입니다.`);
  }
};

user.greet(); 
// 출력: 안녕하세요, 저는 김철수입니다.
// 출력: 나이는 25살입니다.

중첩 객체에서의 this

const company = {
  name: "테크 회사",
  department: {
    name: "개발팀",
    introduce: function() {
      console.log(`${this.name}입니다.`); // "개발팀입니다."
    }
  }
};

company.department.introduce();

3. 생성자 함수와 클래스 (Constructor & Class)

생성자 함수나 클래스에서 this는 새로 생성되는 객체(인스턴스)를 참조합니다.

생성자 함수

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.introduce = function() {
    console.log(`저는 ${this.name}이고, ${this.age}살입니다.`);
  };
}

const person1 = new Person("이영희", 28);
const person2 = new Person("박민수", 32);

person1.introduce(); // 저는 이영희이고, 28살입니다.
person2.introduce(); // 저는 박민수이고, 32살입니다.

ES6 클래스

class Developer {
  constructor(name, language) {
    this.name = name;
    this.language = language;
  }

  coding() {
    console.log(`${this.name}이 ${this.language}로 코딩합니다.`);
  }
}

const developer = new Developer("김개발", "JavaScript");
developer.coding(); // 김개발이 JavaScript로 코딩합니다.

4. 명시적 바인딩 (Explicit Binding)

call(), apply(), bind() 메서드를 사용하면 this를 명시적으로 설정할 수 있습니다.

call() 메서드

function greet() {
  console.log(`안녕하세요, ${this.name}님!`);
}

const user1 = { name: "홍길동" };
const user2 = { name: "김철수" };

greet.call(user1); // 안녕하세요, 홍길동님!
greet.call(user2); // 안녕하세요, 김철수님!

apply() 메서드

function introduce(job, city) {
  console.log(`${this.name}은 ${city}에서 ${job}으로 일합니다.`);
}

const person = { name: "이개발" };

introduce.apply(person, ["개발자", "서울"]); 
// 이개발은 서울에서 개발자로 일합니다.

bind() 메서드

function sayHello() {
  console.log(`Hello, ${this.name}!`);
}

const user = { name: "Alice" };
const boundSayHello = sayHello.bind(user);

boundSayHello(); // Hello, Alice!

메서드 비교표

메서드 즉시 실행 인자 전달 방식 반환값
call() 개별 인자 함수 실행 결과
apply() 배열 형태 함수 실행 결과
bind() 개별 인자 새로운 함수

5. 화살표 함수 (Arrow Function)

화살표 함수는 자체적인 this를 가지지 않고, 상위 스코프의 this를 상속받습니다. 이는 화살표 함수의 가장 중요한 특징입니다.

일반 함수 vs 화살표 함수

const obj = {
  name: "테스트",
  regularFunction: function() {
    console.log("일반 함수:", this.name); // "테스트"
  },
  arrowFunction: () => {
    console.log("화살표 함수:", this.name); // undefined (전역 this)
  }
};

obj.regularFunction(); // 일반 함수: 테스트
obj.arrowFunction();   // 화살표 함수: undefined

실무에서 유용한 활용

class EventHandler {
  constructor() {
    this.message = "클릭되었습니다!";
  }

  setupEventListener() {
    // 화살표 함수를 사용하여 this 바인딩 유지
    document.getElementById("btn").addEventListener("click", () => {
      console.log(this.message); // "클릭되었습니다!"
    });
  }
}

const handler = new EventHandler();
handler.setupEventListener();

6. DOM 이벤트 핸들러 (DOM Event Handler)

DOM 요소의 이벤트 핸들러에서 this는 기본적으로 이벤트를 발생시킨 요소를 참조합니다.

일반 함수 이벤트 핸들러

const button = document.getElementById("myButton");

button.addEventListener("click", function() {
  console.log(this); // 클릭된 button 요소
  console.log(this.textContent); // 버튼의 텍스트
  this.style.backgroundColor = "blue"; // 버튼 색상 변경
});

화살표 함수 이벤트 핸들러

const button = document.getElementById("myButton");

button.addEventListener("click", () => {
  console.log(this); // 상위 스코프의 this (보통 window)
  // 이벤트 객체를 통해 요소에 접근해야 함
});

// 이벤트 객체 활용
button.addEventListener("click", (event) => {
  console.log(event.target); // 클릭된 button 요소
});

실무 팁: 이벤트 핸들러 패턴

class ButtonController {
  constructor() {
    this.clickCount = 0;
  }

  handleClick() {
    this.clickCount++;
    console.log(`클릭 횟수: ${this.clickCount}`);
  }

  init() {
    const button = document.getElementById("counter");

    // bind 사용
    button.addEventListener("click", this.handleClick.bind(this));

    // 또는 화살표 함수 사용
    button.addEventListener("click", () => this.handleClick());
  }
}

const controller = new ButtonController();
controller.init();

this 바인딩 우선순위

JavaScript에서 this 바인딩에는 우선순위가 있습니다.

  1. 명시적 바인딩 (call, apply, bind)
  2. new 바인딩 (생성자 함수)
  3. 암시적 바인딩 (메서드 호출)
  4. 기본 바인딩 (전역 호출)
function test() {
  console.log(this.name);
}

const obj = { name: "객체" };
const boundTest = test.bind({ name: "바인딩" });

// 명시적 바인딩이 암시적 바인딩보다 우선
obj.test = boundTest;
obj.test(); // "바인딩" (명시적 바인딩 우선)

실무에서 자주 발생하는 this 문제와 해결책

문제 1: 콜백 함수에서 this 손실

// 문제 상황
class Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    // 이렇게 하면 this가 전역 객체를 참조
    setInterval(function() {
      this.seconds++; // TypeError: Cannot read property 'seconds' of undefined
      console.log(this.seconds);
    }, 1000);
  }
}

// 해결책 1: bind 사용
class Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    setInterval(function() {
      this.seconds++;
      console.log(this.seconds);
    }.bind(this), 1000);
  }
}

// 해결책 2: 화살표 함수 사용
class Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    setInterval(() => {
      this.seconds++;
      console.log(this.seconds);
    }, 1000);
  }
}

문제 2: 배열 메서드에서 this 손실

// 문제 상황
const calculator = {
  numbers: [1, 2, 3, 4, 5],
  multiplier: 2,

  calculate() {
    return this.numbers.map(function(num) {
      return num * this.multiplier; // this.multiplier is undefined
    });
  }
};

// 해결책 1: thisArg 매개변수 사용
const calculator = {
  numbers: [1, 2, 3, 4, 5],
  multiplier: 2,

  calculate() {
    return this.numbers.map(function(num) {
      return num * this.multiplier;
    }, this); // 두 번째 인자로 this 전달
  }
};

// 해결책 2: 화살표 함수 사용
const calculator = {
  numbers: [1, 2, 3, 4, 5],
  multiplier: 2,

  calculate() {
    return this.numbers.map(num => num * this.multiplier);
  }
};

마무리

JavaScript의 this 바인딩은 함수 호출 방식에 따라 동적으로 결정되는 특별한 개념입니다. 이를 제대로 이해하기 위해서는 다음 핵심 원칙들을 기억해야 합니다.

핵심 정리

  • 호출 방식이 전부: this는 함수가 정의된 곳이 아닌, 호출되는 방식에 따라 결정됩니다.
  • 화살표 함수는 예외: 상위 스코프의 this를 상속받아 바인딩이 고정됩니다.
  • 명시적 바인딩 활용: call, apply, bind를 활용하여 this를 명시적으로 제어할 수 있습니다.
  • 우선순위 이해: 명시적 바인딩 > new 바인딩 > 암시적 바인딩 > 기본 바인딩 순으로 우선순위가 적용됩니다.

this 바인딩을 완전히 이해하면 JavaScript의 객체 지향 프로그래밍과 함수형 프로그래밍을 더욱 효과적으로 활용할 수 있습니다. 실무에서는 화살표 함수와 명시적 바인딩을 적절히 조합하여 this 관련 문제를 예방하는 것이 중요합니다.

728x90