icon
6장 : 자바스크립트 심화 I

This 키워드와 바인딩

자바스크립트에서 this 키워드는 함수가 호출될 때마다 결정되는 특별한 값입니다. 많은 개발자들이 this의 동작 방식 때문에 혼란을 겪곤 하는데, 그 이유는 this가 함수가 선언된 방식이 아니라 함수가 호출되는 방식에 따라 동적으로 결정되기 때문입니다. this는 현재 실행 중인 코드의 '맥락(context)'을 나타내며, 어떤 객체가 현재 함수를 호출했는지(또는 어떤 객체와 연결되어 있는지)를 참조합니다.

이번 장에서는 이 this 키워드가 어떻게 다양한 상황에서 다르게 바인딩(연결)되는지, 그리고 이 특성을 어떻게 이해하고 활용해야 하는지 깊이 있게 탐구해 보겠습니다. this의 동작 원리를 명확히 이해하는 것은 객체 지향 프로그래밍, 이벤트 처리, 그리고 모듈 패턴 등 자바스크립트의 고급 개념들을 자유자재로 다루는 데 필수적인 지식입니다.


this의 기본 개념: 실행 컨텍스트의 일부

앞서 1장에서 우리는 실행 컨텍스트가 Lexical Environment, Variable Environment, 그리고 this 바인딩이라는 세 가지 구성 요소를 가지고 있음을 배웠습니다. 즉, this 키워드는 특정 함수가 실행될 때, 해당 실행 컨텍스트의 한 부분으로서 함께 결정되는 값입니다.

this의 가장 중요한 특징은 "함수를 호출하는 방식"에 따라 this가 참조하는 객체가 달라진다는 것입니다. 이는 다른 언어와 달리 자바스크립트의 this가 매우 유연하다는 것을 의미하며, 동시에 혼란의 원인이 되기도 합니다.

기본적으로 this는 현재 코드를 실행하는 주체(객체)를 가리킵니다. 그럼 이제 다양한 호출 상황별로 this가 어떻게 바인딩되는지 살펴보겠습니다.


this 바인딩 규칙: 상황별 결정 방식

자바스크립트에서 this는 크게 네 가지 일반적인 규칙에 따라 바인딩됩니다.

전역 컨텍스트에서의 this

함수 내부가 아닌 전역 코드에서 this를 사용하면, this는 전역 객체(Global Object)를 참조합니다.

  • 웹 브라우저 환경: window 객체
  • Node.js 환경: global 객체
// 웹 브라우저에서 실행 시
console.log(this === window); // 결과: true

let globalVar = "나는 전역 변수";
console.log(this.globalVar); // 결과: 나는 전역 변수 (window.globalVar와 같음)

function checkGlobalThis() {
    console.log(this === window); // 함수 내부이지만 일반 함수 호출이므로 true (아래 2.2.2. 설명)
}
checkGlobalThis();

엄격 모드(strict mode)에서는 전역 컨텍스트의 thisundefined가 됩니다. (엄격 모드에 대해서는 뒤에서 자세히 다룹니다.)

일반 함수 호출에서의 this

가장 혼란스러운 부분 중 하나입니다. 일반적인 함수(객체의 메서드가 아닌 독립적인 함수)로 호출될 때, this는 전역 객체(window 또는 global)를 참조합니다.

function greet() {
    console.log(`Hello, ${this.name || '세계'}`); // this.name이 없으면 '세계' 사용
}

let name = "전역"; // window.name이 됨 (브라우저 환경)
greet(); // 결과: Hello, 전역 (브라우저에서는 window.name이 '전역'이 됨)
         // Node.js에서는 window가 없으므로 '세계'가 나옴

const personName = "홍길동"; // var가 아니므로 window.personName이 아님

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

showThis(); // 웹 브라우저: window 객체, Node.js: global 객체 (또는 undefined in strict mode)

엄격 모드 ('use strict')의 영향

자바스크립트의 엄격 모드('use strict')에서는 일반 함수 호출 시 thisundefined로 바인딩됩니다. 이는 개발자가 의도치 않게 전역 객체를 수정하는 것을 방지하기 위함입니다.

'use strict'; // 스크립트 최상단 또는 함수 최상단에 선언

function strictGreet() {
    console.log(this); // 결과: undefined
}

strictGreet();

function outer() {
    function inner() {
        console.log(this); // outer가 일반 함수이므로, inner도 일반 함수 호출로 간주되어 undefined
    }
    inner();
}
outer();

따라서, this 문제를 줄이기 위해 항상 엄격 모드 사용을 권장하는 이유 중 하나입니다.

메서드 호출에서의 this

가장 흔하게 사용되는 패턴입니다. 함수가 객체의 속성(메서드)으로서 호출될 때, this는 해당 메서드를 호출한 객체 자체를 참조합니다.

const user = {
    name: "김철수",
    age: 30,
    greet: function() { // greet은 user 객체의 메서드
        console.log(`안녕하세요, 제 이름은 ${this.name}입니다.`); // 여기서 this는 user 객체
    },
    introduce: function() {
        console.log(`저는 ${this.age}살입니다.`); // 여기서 this는 user 객체
    }
};

user.greet();     // 결과: 안녕하세요, 제 이름은 김철수입니다.
user.introduce(); // 결과: 저는 30살입니다.

// 중첩 객체에서의 this
const company = {
    name: "ABC",
    ceo: {
        name: "박영희",
        greetCEO: function() {
            console.log(`CEO 이름: ${this.name}`); // 여기서 this는 ceo 객체
        }
    }
};
company.ceo.greetCEO(); // 결과: CEO 이름: 박영희

this는 '어떤 객체가 점(.) 앞에 왔는가'에 따라 결정됩니다.

주의: 메서드를 별도의 변수에 할당하여 일반 함수처럼 호출하는 경우

메서드를 변수에 할당하여 호출하면, 더 이상 객체의 메서드로서 호출되는 것이 아니라 일반 함수처럼 호출됩니다. 이 경우 this 바인딩 규칙이 변경됩니다.

const myGreet = user.greet; // user.greet 메서드를 myGreet 변수에 할당
myGreet(); // 결과: Hello, 세계 (또는 undefined in strict mode)
           // myGreet은 일반 함수로 호출되었으므로 this는 전역 객체를 가리킵니다.

이러한 특성 때문에 this의 혼란이 발생하며, 이를 해결하기 위한 여러 방법이 있습니다.

new를 사용한 생성자에서의 this

new 키워드와 함께 함수를 호출하면, 해당 함수는 생성자 함수(Constructor Function) 로서 작동합니다. 이 경우 this는 새로 생성되는 인스턴스(Instance) 객체를 참조합니다.

function Person(name, age) {
    // 1. (내부적으로) 새로운 빈 객체가 생성되고 this에 바인딩됩니다. (예: this = {})
    this.name = name; // 새로 생성된 객체의 name 속성에 값 할당
    this.age = age;   // 새로 생성된 객체의 age 속성에 값 할당
    // 2. (내부적으로) this가 반환됩니다.
}

const person1 = new Person("김민준", 25);
console.log(person1.name); // 결과: 김민준
console.log(person1.age);  // 결과: 25

const person2 = new Person("이유리", 30);
console.log(person2.name); // 결과: 이유리

new 연산자는 다음의 과정을 수행합니다:

  1. 빈 객체를 생성하고 this에 할당합니다.
  2. 생성자 함수의 본문을 실행하여 this에 속성들을 추가합니다.
  3. this (새로 생성된 객체)를 반환합니다.

특별한 메서드를 사용한 명시적 바인딩

자바스크립트의 모든 함수는 call(), apply(), bind()라는 특별한 메서드를 가지고 있습니다. 이 메서드들을 사용하면 개발자가 명시적으로 this가 참조할 객체를 지정할 수 있습니다.

  • function.call(thisArg, arg1, arg2, ...): 함수를 즉시 호출하면서, 첫 번째 인자로 전달된 thisArg 객체를 this로 바인딩하고, 이후 인자들은 개별적으로 함수에 전달합니다.

    function introduce(job, city) {
        console.log(`안녕하세요, 저는 ${this.name}입니다. ${job}를 하고 ${city}에 살고 있습니다.`);
    }
    const person = { name: "최지수" };
    introduce.call(person, "개발자", "서울"); // 결과: 안녕하세요, 저는 최지수입니다. 개발자를 하고 서울에 살고 있습니다.
    // introduce 함수 내부의 this가 person 객체를 가리키게 됨
  • function.apply(thisArg, [argsArray]): call()과 동일하게 함수를 즉시 호출하며 thisArgthis로 바인딩합니다. 차이점은 두 번째 인자로 배열 형태의 인자들을 전달해야 한다는 것입니다.

    introduce.apply(person, ["디자이너", "부산"]); // 결과: 안녕하세요, 저는 최지수입니다. 디자이너를 하고 부산에 살고 있습니다.
    // 배열로 인자들을 전달

    callapply는 즉시 함수를 실행한다는 점에서 유사하며, 주로 인자 전달 방식의 차이로 구분됩니다.

  • function.bind(thisArg, arg1, arg2, ...): call()이나 apply()와 달리 함수를 즉시 호출하지 않고, this가 영구적으로 바인딩된 새로운 함수를 반환합니다. 이 반환된 함수는 나중에 호출될 때 지정된 this 값을 가집니다.

    const anotherPerson = { name: "정우진" };
    const boundIntroduce = introduce.bind(anotherPerson, "학생"); // this를 anotherPerson으로, 첫 인자를 "학생"으로 고정
    boundIntroduce("제주"); // 결과: 안녕하세요, 저는 정우진입니다. 학생를 하고 제주에 살고 있습니다.
    // boundIntroduce는 이미 this가 anotherPerson으로 고정되어, job은 "학생", city는 "제주"로 전달됨

    bind()는 이벤트 리스너에서 콜백 함수의 this 컨텍스트를 유지해야 할 때 매우 유용하게 사용됩니다.


화살표 함수에서의 this 바인딩

ES6에서 도입된 화살표 함수(Arrow Function)this 바인딩 방식에서 기존 함수와 중요한 차이점을 가집니다.

화살표 함수는 자체적인 this 바인딩을 가지지 않습니다. 대신, 화살표 함수가 선언될 당시의 상위(Lexical) 스코프의 this를 그대로 상속받습니다. 즉, 화살표 함수는 this를 동적으로 결정하지 않고, 자신이 정의된 렉시컬 환경의 this 값을 '고정'시킵니다.

const user = {
    name: "이아림",
    age: 28,
    // 일반 메서드 (메서드 호출 규칙 적용)
    greet: function() {
        console.log(`안녕하세요, ${this.name}입니다.`); // this는 user 객체
    },
    // 화살표 함수 메서드 (상위 스코프의 this 상속)
    // 이 경우 user 객체는 스코프가 아니므로, 전역 스코프의 this(window)를 상속받음
    arrowGreet: () => {
        console.log(`안녕하세요, ${this.name || '알 수 없는 사람'}입니다.`); // this는 window 객체 (브라우저)
    },
    // 중첩된 함수에서의 화살표 함수 활용 (매우 중요!)
    delayedGreet: function() { // 일반 함수 (여기서의 this는 user 객체)
        console.log(`(1) 일반 함수 내의 this: ${this.name}`); // this는 user

        // 1초 뒤에 실행될 함수 (일반 함수)
        setTimeout(function() {
            console.log(`(2) setTimeout 내부 일반 함수 this: ${this.name}`); // this는 window (일반 함수 호출 규칙)
        }, 1000);

        // 1초 뒤에 실행될 함수 (화살표 함수)
        setTimeout(() => {
            console.log(`(3) setTimeout 내부 화살표 함수 this: ${this.name}`); // this는 outer 스코프(delayedGreet)의 this (user)
        }, 2000);
    }
};

user.greet();          // 결과: 안녕하세요, 이아림입니다.
user.arrowGreet();     // 결과: 안녕하세요, 알 수 없는 사람입니다. (브라우저에서 window.name이 없으면)
user.delayedGreet();
// (1) 일반 함수 내의 this: 이아림
// (2) setTimeout 내부 일반 함수 this: (undefined 또는 window.name 값)
// (3) setTimeout 내부 화살표 함수 this: 이아림

위 예시에서 delayedGreet 메서드 내부의 일반 setTimeout 함수는 this를 전역 객체로 바인딩하지만, 화살표 함수는 delayedGreet가 정의된 렉시컬 스코프의 this(즉, user 객체)를 그대로 유지하는 것을 볼 수 있습니다. 이러한 특성 때문에 이벤트 핸들러나 콜백 함수에서 외부 스코프의 this를 유지하고 싶을 때 화살표 함수를 사용하는 것이 매우 효과적입니다.


마무리하며

이번 장에서는 자바스크립트의 this 키워드와 그 바인딩 원리에 대해 깊이 있게 탐구했습니다. this가 함수가 호출되는 방식에 따라 동적으로 결정되며, 전역, 메서드, 생성자, 일반 함수 호출 시 각각 다른 규칙을 따름을 이해했습니다. 또한, call(), apply(), bind() 메서드를 사용하여 this를 명시적으로 제어하는 방법과, 화살표 함수가 this를 렉시컬하게 상속받는다는 중요한 특성도 학습했습니다.

this는 자바스크립트의 유연성을 보여주는 동시에, 초보 개발자들에게는 가장 큰 혼란을 주는 개념이기도 합니다. 하지만 이제 여러분은 this의 결정 규칙을 명확히 이해하고, 각 상황에 맞는 바인딩 방식을 예측하거나 제어할 수 있는 능력을 갖추게 되었습니다.

이 지식은 객체 지향 프로그래밍 패턴을 이해하고, 비동기 코드에서 컨텍스트를 올바르게 유지하며, 다양한 라이브러리와 프레임워크의 내부 동작을 파악하는 데 필수적인 기반이 될 것입니다. 다양한 상황에서 this 값을 직접 console.log()로 출력해보며 연습하는 것이 this 개념을 완전히 자기 것으로 만드는 데 가장 좋은 방법입니다.