안동민 개발노트

본문 시작

This 키워드와 바인딩

일반 호출·메서드·생성자·명시적 호출에서 this가 정해지는 규칙과 화살표 함수의 렉시컬 바인딩을 구분합니다.

일반 함수의 this는 호출 방식에 따라 결정됩니다. 화살표 함수는 자체 this 없이 바깥 렉시컬 환경의 값을 사용합니다.

많은 개발자가 헷갈리는 이유는 this가 함수 선언 방식이 아니라 호출 방식에 따라 동적으로 바뀌기 때문입니다.

this는 현재 실행 코드의 맥락(context)을 나타내며, 어떤 객체가 함수를 호출했는지와 연결됩니다.

이번 장에서는 this가 상황별로 어떻게 바인딩되는지와, 이를 안전하게 활용하는 방법을 정리합니다.

일반 함수 this 바인딩 우선순위

DECISION TREE · ECMASCRIPT CALL SITE

일반 함수 this 바인딩 우선순위

new → call/apply/bind → receiver.method → plain call 순으로 호출 형태를 판별하고 환경별 plain-call 결과를 분리

일반 함수 this 바인딩 우선순위 new → call/apply/bind → receiver.method → plain call 순으로 호출 형태를 판별하고 환경별 plain-call 결과를 분리 화살표 함수인가?호출 형태로 this를 다시 정하지 않는다lexical thissurrounding this를 그대로 사용YESNOnew로 호출했는가?new Fn()새 인스턴스bound this보다 constructor 호출이 우선YESNO명시적으로 지정했는가?call · apply · bindthisArg / bound value호출자가 지정한 receiverYESNOreceiver.method()인가?호출 표현식에 base object가 있는가receiverobj.method() → this === objYESNO · STRICT / MODULENO · NON-STRICTBROWSER SCRIPTundefinedplain function call in strict mode / moduleglobalThisplain call in non-strict browser script
  1. 화살표 함수인지 먼저 구분

    화살표 함수면 호출 형태와 무관하게 surrounding this를 lexical하게 사용한다.

  2. new 호출

    일반 함수를 constructor로 호출했다면 새 인스턴스가 this다.

  3. 명시 바인딩

    call / apply / bind의 지정값이 receiver보다 우선한다.

  4. 메서드 호출

    receiver.method()의 base object가 this다.

  5. plain call

    strict mode와 ES module은 undefined, non-strict browser script는 globalThis다.

this는 함수가 선언된 위치보다 호출 표현식이 결정한다. 단, 화살표 함수는 이 우선순위에 참여하지 않고 바깥 this를 사용한다.


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

앞서 1장에서 우리는 실행 컨텍스트가 Lexical Environment, Variable Environment, 그리고 this 바인딩이라는 세 가지 구성 요소를 가지고 있음을 배웠습니다.

즉, this 키워드는 특정 함수가 실행될 때, 해당 실행 컨텍스트의 한 부분으로서 함께 결정되는 값입니다.

this의 가장 중요한 특징은 함수를 호출하는 방식에 따라 this가 참조하는 객체가 달라진다는 것입니다.

이는 다른 언어와 달리 자바스크립트의 this가 매우 유연하다는 것을 의미하며, 동시에 혼란의 원인이 되기도 합니다.

기본적으로 this는 현재 코드를 실행하는 주체(객체)를 가리킵니다.

그럼 이제 다양한 호출 상황별로 this가 어떻게 바인딩되는지 살펴보겠습니다.


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

This 키워드와 바인딩에서는 값의 변화, 실행 순서, 화면 반응을 함께 봅니다.

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

전역 컨텍스트에서의 this

최상위 this는 실행 형식에 따라 다릅니다.

  • 브라우저 classic script: 엄격 모드 여부와 관계없이 window를 가리킵니다.
  • ES module: 브라우저와 Node.js 모두 최상위 this는 undefined입니다.
  • Node.js CommonJS 파일: 모듈 wrapper 안에서 실행되므로 최상위 this는 초기 module.exports입니다.
// 브라우저의 비엄격 classic script에서 각 예시를 독립적으로 실행
console.log(this === window); // 결과: true

let globalVar = "나는 전역 변수";
console.log(this.globalVar); // 결과: undefined (같은 이름의 기존 window 속성이 없을 때)

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

전역 let·const는 전역 lexical binding을 만들며 window 속성을 만들지 않습니다. 엄격 모드 classic script의 최상위 this도 window이고, undefined가 되는 것은 엄격한 일반 함수의 독립 호출입니다.

일반 함수 호출에서의 this

가장 혼란스러운 부분 중 하나입니다.

일반 함수를 독립적으로 호출할 때, 그 함수가 비엄격 모드이면 this는 전역 객체(window 또는 global)가 됩니다. 엄격한 함수에서는 undefined입니다.

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

let name = "전역"; // 전역 lexical binding이며 window.name 속성을 바꾸지 않음
greet(); // 비엄격 classic script에서 window.name이 빈 문자열이면 Hello, 세계
         // 비엄격 Node.js 일반 호출도 global.name이 없으면 Hello, 세계

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

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

showThis(); // 웹 브라우저: window 객체, Node.js: global 객체 (또는 undefined in strict mode)
엄격 모드 ('use strict')의 영향

자바스크립트의 엄격 모드('use strict')에서는 일반 함수 호출 시 this가 undefined로 바인딩됩니다.

이는 개발자가 의도치 않게 전역 객체를 수정하는 것을 방지하기 위함입니다.

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

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

strictGreet();

function outer() {
    function inner() {
        console.log(this); // inner를 독립 호출했고 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(); // 비엄격 classic script에서 window.name이 빈 문자열이면: 안녕하세요, 제 이름은 입니다.
           // user.greet 자체가 엄격한 함수라면 this.name 접근에서 TypeError

이러한 특성 때문에 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 연산자는 다음의 과정을 수행합니다.

빈 객체를 생성하고 this에 할당합니다.

생성자 함수의 본문을 실행하여 this에 속성들을 추가합니다.

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()과 동일하게 함수를 즉시 호출하며 thisArg를 this로 바인딩합니다. 차이점은 두 번째 인자로 배열 형태의 인자들을 전달해야 한다는 것입니다.

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

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

  • 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를 유지할 때 유용합니다. 단, 생성 가능한 bound 함수를 new로 호출하면 bound this 대신 새 인스턴스를 사용합니다. 비엄격 일반 함수에 call·apply·bind로 null/undefined를 주면 전역 객체로, 다른 원시값은 wrapper 객체로 변환됩니다.


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

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

화살표 함수는 자체적인 this 바인딩을 가지지 않습니다.

대신, 화살표 함수가 선언될 당시의 상위(Lexical) 스코프의 this를 그대로 상속받습니다.

즉, 화살표 함수는 this를 동적으로 결정하지 않고, 자신이 정의된 렉시컬 환경의 this 값을 고정시킵니다.

const user = {
    name: "이아림",
    age: 28,
    // 일반 메서드 (메서드 호출 규칙 적용)
    greet: function() {
        console.log(`안녕하세요, ${this.name}입니다.`); // this는 user 객체
    },
    // 화살표 함수 메서드 (상위 스코프의 this 상속)
    // 브라우저 classic script 기준: 객체 리터럴은 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}`); // 브라우저 timer API가 this로 window를 전달
        }, 1000);

        // 최소 지연 2초를 요청한 함수 (화살표 함수)
        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: (브라우저 window.name 값)
// (3) setTimeout 내부 화살표 함수 this: 이아림

브라우저 timer API는 일반 콜백에 window를 this로 전달합니다. 화살표 콜백은 전달된 값 대신 자신을 생성한 delayedGreet 호출의 this(이 예시에서는 user)를 사용합니다. 지정한 지연은 정확한 실행 시각을 보장하지 않으며, Node.js timer의 일반 콜백 this는 별도 API 계약입니다.

이러한 특성 때문에 이벤트 핸들러나 콜백 함수에서 외부 스코프의 this를 유지하고 싶을 때 화살표 함수를 사용하는 것이 매우 효과적입니다.


객체 구조 정리

이번 장에서는 자바스크립트의 this 키워드와 그 바인딩 원리에 대해 깊이 있게 탐구했습니다.

this가 함수가 호출되는 방식에 따라 동적으로 결정되며, 전역, 메서드, 생성자, 일반 함수 호출 시 각각 다른 규칙을 따름을 이해했습니다.

또한, call(), apply(), bind() 메서드를 사용하여 this를 명시적으로 제어하는 방법과, 화살표 함수가 this를 렉시컬하게 상속받는다는 중요한 특성도 학습했습니다.

this는 호출 방식에 따라 값이 달라지는 자바스크립트의 실행 규칙입니다.

전역 호출, 메서드 호출, 생성자 호출, call/apply/bind, 화살표 함수의 렉시컬 바인딩을 구분하면 각 상황의 바인딩 결과를 예측할 수 있습니다.

이 지식은 객체 지향 프로그래밍 패턴을 이해하고, 비동기 코드에서 컨텍스트를 올바르게 유지하며, 다양한 라이브러리와 프레임워크의 내부 동작을 파악하는 데 필수적인 기반이 될 것입니다.

다양한 상황에서 this 값을 직접 console.log()로 출력해보며 연습하는 것이 this 개념을 완전히 자기 것으로 만드는 데 가장 좋은 방법입니다.

this 오류는 대부분 함수를 어디에 선언했는지가 아니라 어떻게 넘기고 호출했는지를 놓쳐서 생깁니다.

안전한 this 바인딩은 콜백 전달, 메서드 분리, 화살표 함수 선택 상황마다 호출 주체를 기준으로 판단합니다.

this 문제는 함수 본문보다 호출 지점을 보면 대부분 원인이 드러납니다.