본문 바로가기

This 키워드와 바인딩

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

앞 글 클로저와 실행 컨텍스트에서는 함수가 바깥 변수의 이름을 정의한 위치를 기준으로 찾는다는 것을 살펴봤습니다. this는 이와 다르게 동작합니다. 일반 함수의 this는 함수를 어디에 정의했는지가 아니라 어떻게 호출했는지에 따라 정해지고, 화살표 함수(arrow function)는 자기 this를 만들지 않고 바깥의 this를 그대로 씁니다.

같은 함수라도 호출하는 방식에 따라 this가 달라지므로, 많은 사람이 이 부분에서 헷갈립니다. 이 글에서는 호출 방식마다 this가 무엇이 되는지 규칙을 정리하고, 메서드를 다른 곳에 넘길 때 생기는 문제를 피하는 방법을 익힙니다.

호출 방식으로 판별하는 this

DECISION TREE · ECMASCRIPT CALL SITE

호출 방식으로 판별하는 this

먼저 화살표 함수를 구분한다. 일반 함수는 생성자 호출·명시 바인딩·메서드·독립 호출 순으로 판별하며, 독립 호출의 결과는 함수의 strict 여부에 달려 있다.

함수 종류와 호출 방식에 따른 this화살표이면 바깥 this를 사용하고 질문을 끝낸다. 일반 함수는 new, 명시바인딩, 메서드 순으로 확인하며 예는 오른쪽 결과로 끝나고 아니오만 다음 질문으로 간다. 독립호출은 함수의 엄격여부에 따라 undefined 또는 전역객체로 분기한다. 모바일도 결과로 끝난 가지를 다음질문에 연결하지 않는다. 원시값 thisArg의 변환과 bound/new 예외는 아래 본문을 따른다. 화살표 함수?자체 this 없음바깥 this정의한 환경 사용예아니오new로 호출?생성 가능한 함수새 인스턴스bound this 무시예아니오명시 바인딩?bound / call / apply지정한 thisArg기존 bound 값 우선예아니오메서드 호출?receiver.method()수신자obj.method() → obj예아니오독립 호출함수 자체가 엄격한가?undefined엄격한 함수전역 객체비엄격한 함수예아니오
함수 종류와 호출 방식에 따른 this화살표이면 바깥 this를 사용하고 질문을 끝낸다. 일반 함수는 new, 명시바인딩, 메서드 순으로 확인하며 예는 오른쪽 결과로 끝나고 아니오만 다음 질문으로 간다. 독립호출은 함수의 엄격여부에 따라 undefined 또는 전역객체로 분기한다. 모바일도 결과로 끝난 가지를 다음질문에 연결하지 않는다. 원시값 thisArg의 변환과 bound/new 예외는 아래 본문을 따른다.화살표 함수?자체 this 없음바깥 this정의한 환경 사용예아니오 → 다음 질문new로 호출?생성 가능한 함수새 인스턴스bound this 무시예아니오 → 다음 질문명시 바인딩?bound / call / apply지정한 thisArg기존 bound 값 우선예아니오 → 다음 질문메서드 호출?receiver.method()수신자obj.method() → obj예아니오 → 다음 질문독립 호출엄격한 함수인가?undefined예 · 엄격한 함수전역 객체아니오 · 비엄격아니오 → 전역 객체
  1. 화살표 함수인지 먼저 구분

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

  2. new 호출

    생성 가능한 함수를 new로 호출하면 bound this 대신 새 인스턴스가 this다.

  3. 명시 바인딩

    call / apply / bind의 지정값이 receiver보다 우선한다. 이미 bind된 함수는 뒤이은 call/apply로 this를 바꿀 수 없다.

  4. 메서드 호출

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

  5. plain call

    strict 함수의 독립 호출은 undefined, non-strict 함수는 globalThis다. ES module에서 정의한 함수는 strict다.

호출 규칙의 예외도 함수 종류에 달려 있다

화살표 함수는 call/apply/bind로 this를 바꾸지 못하고 new로 호출할 수 없다. 이미 bind된 함수는 이후 call/apply로 this를 바꾸지 못한다. 생성 가능한 bound 함수를 new로 호출하면 bound this를 무시한다. non-strict 일반 함수는 null/undefined thisArg를 전역 객체로, 다른 원시값을 wrapper 객체로 변환한다.

위 그림은 this를 판별하는 순서를 질문으로 나타낸 것입니다. 먼저 화살표 함수인지 묻고, 화살표 함수라면 호출 형태와 관계없이 정의한 곳의 바깥 this를 쓰므로 더 묻지 않습니다. 일반 함수라면 new로 호출했는지, call(), apply(), bind()로 this를 지정했는지, obj.method()처럼 메서드로 호출했는지를 차례로 묻고, 처음으로 “예”가 나오는 곳에서 결과가 정해집니다.

세 질문에 모두 “아니오”라면 독립 호출이며, 이때는 함수가 엄격 모드(strict mode)인지에 따라 엄격한 함수는 undefined, 비엄격 함수는 전역 객체가 this가 됩니다. ES 모듈에서 정의한 함수는 항상 엄격 모드입니다. 그림 아래의 예외들, 즉 화살표 함수와 이미 bind()한 함수는 this를 다시 바꿀 수 없다는 점과 비엄격 함수가 null이나 원시값을 변환하는 규칙은 본문에서 차례로 살펴봅니다.


this는 호출할 때 정해집니다

this는 함수가 실행될 때 실행 컨텍스트의 일부로 함께 정해지는 값입니다. 그래서 this를 함수가 영원히 속한 객체라고 생각하면 예측이 틀립니다. 객체의 메서드라도 변수에 담아 따로 호출하면 그 객체를 가리키지 않으며, 엄격한 함수를 독립 호출하면 this는 객체 대신 undefined가 됩니다.

이 글의 예제는 브라우저의 일반 <script>(classic script)를 기준으로 합니다. 실행 형식에 따라 결과가 달라지는 예제가 있으므로, 각 예제는 새 HTML 파일의 <script>에 따로 넣어 실행하세요. 여러 예제가 같은 이름의 user를 선언하므로, 한 스크립트에 이어 붙이면 이름이 겹친다는 오류가 납니다.


전역 코드의 this

함수 밖의 맨 바깥 코드에서 this가 무엇인지는 실행 형식에 따라 다릅니다. 브라우저의 classic script에서는 엄격 모드 여부와 관계없이 window를 가리킵니다. ES 모듈에서는 브라우저와 Node.js 모두 맨 바깥의 this가 undefined입니다. Node.js의 CommonJS 파일은 모듈을 감싸는 함수 안에서 실행되므로, 맨 바깥의 this가 처음의 module.exports 객체입니다.

// 브라우저의 비엄격 classic script에서 실행
console.log(this === window); // true

let globalVar = "나는 전역 변수";
console.log(this.globalVar); // undefined (let은 window의 속성을 만들지 않음)

function checkGlobalThis() {
    console.log(this === window); // 일반 함수의 독립 호출이므로 true
}
checkGlobalThis();

앞 글에서 본 것처럼 전역의 let과 const는 window의 속성이 되지 않으므로 this.globalVar는 undefined입니다. checkGlobalThis() 안의 this가 window인 이유는 전역 코드라서가 아니라, 비엄격 함수를 독립 호출했기 때문입니다. 이 차이는 다음 절에서 다룹니다.


일반 함수 호출

함수를 객체 없이 greet()처럼 독립적으로 호출하면, 그 함수가 비엄격 모드일 때 this는 전역 객체가 됩니다. 브라우저에서는 window, Node.js에서는 global이며 두 환경 모두 globalThis로 가리킬 수 있습니다.

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

let name = "전역"; // 전역 let이므로 window.name 속성을 바꾸지 않음
greet(); // window.name이 빈 문자열이면 Hello, 세계

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

showThis(); // 브라우저: window 객체, Node.js CommonJS: global 객체

greet() 안의 this는 window이므로 this.name은 전역 let name이 아니라 window.name 속성을 읽습니다. 브라우저 창의 이름을 나타내는 window.name은 보통 빈 문자열이라, || 뒤의 '세계'가 출력됩니다.

엄격 모드의 일반 호출

엄격 모드('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();

outer 안에 정의한 inner도 독립 호출했으므로 this는 undefined입니다. 함수가 다른 함수 안에 있다고 바깥 함수의 this를 물려받지 않는다는 점에 주의하세요. 엄격 모드에서는 this가 없을 때 undefined에서 속성을 읽으려다 TypeError가 나므로, 잘못된 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는 호출할 때 점(.) 바로 앞에 있는 객체로 정해집니다. 그래서 company.ceo.greetCEO()의 this는 company가 아니라 바로 앞의 ceo 객체이고, 출력되는 이름도 "ABC"가 아니라 "박영희"입니다.

메서드를 변수에 담아 호출할 때

메서드를 변수에 담아 호출하면 더 이상 메서드 호출이 아니라 독립 호출이 됩니다. 앞의 user 예제에 이어 다음 코드를 실행해 보세요.

const myGreet = user.greet; // user.greet 메서드를 myGreet 변수에 할당
myGreet(); // 비엄격 classic script에서 window.name이 빈 문자열이면: 안녕하세요, 제 이름은 입니다.
           // user.greet 자체가 엄격한 함수라면 this.name 접근에서 TypeError

myGreet는 user.greet와 같은 함수지만, 호출할 때 점 앞에 객체가 없으므로 비엄격 함수의 독립 호출 규칙에 따라 this는 window가 됩니다. window.name이 빈 문자열이라 이름 자리가 비어 출력됩니다. 메서드를 콜백으로 넘길 때도 같은 일이 생기며, 아래의 bind()와 화살표 함수가 이 문제를 푸는 방법입니다.


생성자 호출

new 키워드와 함께 함수를 호출하면 그 함수는 생성자 함수(constructor function)로 동작하고, this는 새로 만들어지는 인스턴스(instance)를 가리킵니다. 다음 코드에서 주석 1과 2는 new가 내부에서 대신 해 주는 일을 적은 것으로, 직접 쓰는 코드가 아닙니다. 같은 생성자를 두 번 호출해 서로 다른 객체 두 개를 만든다는 점도 보세요. 생성자 이름은 일반 함수와 구분하려고 Person처럼 대문자로 시작하는 것이 관례입니다.

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 Person("김민준", 25)를 실행하면 먼저 빈 객체가 만들어져 this가 됩니다. 이어서 생성자의 본문이 실행되며 this에 name과 age 속성을 추가하고, 생성자가 다른 객체를 직접 반환하지 않으면 이 새 객체가 new의 결과가 됩니다. Person처럼 아무것도 반환하지 않는 생성자는 언제나 새 인스턴스를 돌려주며, 생성자가 별도의 객체를 return하면 그 객체가 결과가 됩니다.


call, apply, bind로 this 지정하기

일반 함수는 Function.prototype에서 call(), apply(), bind() 메서드를 물려받으며, 이 메서드로 this가 가리킬 객체를 직접 지정할 수 있습니다.

call(thisArg, arg1, arg2, ...)은 함수를 곧바로 호출하면서, 첫 번째 인수를 this로 쓰고 나머지 인수를 하나씩 함수에 전달합니다.

function introduce(job, city) {
    console.log(`안녕하세요, 저는 ${this.name}입니다. 직업은 ${job}이고 ${city}에 살고 있습니다.`);
}
const person = { name: "최지수" };
introduce.call(person, "개발자", "서울");
// 안녕하세요, 저는 최지수입니다. 직업은 개발자이고 서울에 살고 있습니다.

apply(thisArg, [argsArray])도 함수를 곧바로 호출하며 첫 번째 인수를 this로 씁니다. 다른 점은 함수에 넘길 인수를 배열 하나로 전달한다는 것입니다.

introduce.apply(person, ["디자이너", "부산"]);
// 안녕하세요, 저는 최지수입니다. 직업은 디자이너이고 부산에 살고 있습니다.

bind(thisArg, arg1, ...)는 함수를 호출하지 않고, this와 앞쪽 인수를 고정한 새 함수를 돌려줍니다. 돌려받은 함수는 나중에 언제 호출하든 지정한 this를 씁니다.

const anotherPerson = { name: "정우진" };
const boundIntroduce = introduce.bind(anotherPerson, "학생"); // this와 첫 인수를 고정
boundIntroduce("제주");
// 안녕하세요, 저는 정우진입니다. 직업은 학생이고 제주에 살고 있습니다.

boundIntroduce는 this가 anotherPerson으로, job이 "학생"으로 고정된 함수이므로 호출할 때 넘긴 "제주"가 city가 됩니다. 이처럼 bind()는 메서드를 이벤트 리스너나 콜백으로 넘길 때 this를 유지하는 데 쓰입니다.

몇 가지 예외도 알아 두세요. 이미 bind()한 함수는 그 뒤에 call()이나 apply()로 this를 바꿀 수 없습니다. 다만 bind()한 함수를 new로 호출하면 지정한 this 대신 새 인스턴스를 씁니다. 또 비엄격 함수에 this로 null이나 undefined를 넘기면 전역 객체로, 숫자나 문자열 같은 원시값을 넘기면 그 값을 감싼 객체로 바뀝니다. 엄격한 함수는 넘긴 값을 그대로 씁니다.


화살표 함수의 this

ES2015에서 추가된 화살표 함수는 자기 this를 만들지 않습니다. 대신 화살표 함수를 정의한 곳의 바깥 this를 그대로 쓰므로, 호출하는 방식과 관계없이 this가 바뀌지 않습니다. 화살표 함수에 call(), apply(), bind()로 this를 넘겨도 무시되고, 화살표 함수는 new로 호출할 수도 없습니다.

const user = {
    name: "이아림",
    age: 28,
    // 일반 메서드 (메서드 호출 규칙 적용)
    greet: function() {
        console.log(`안녕하세요, ${this.name}입니다.`); // this는 user 객체
    },
    // 화살표 함수 메서드 (바깥의 this를 그대로 사용)
    // 객체 리터럴은 this를 만들지 않으므로 바깥인 전역 코드의 this, 즉 window를 사용
    arrowGreet: () => {
        console.log(`안녕하세요, ${this.name || '알 수 없는 사람'}입니다.`);
    },
    // 메서드 안의 콜백에서 화살표 함수 활용
    delayedGreet: function() { // 일반 함수 (여기서의 this는 user 객체)
        console.log(`(1) 일반 함수 내의 this: ${this.name}`);

        // 최소 1초 뒤에 실행할 일반 함수
        setTimeout(function() {
            console.log(`(2) setTimeout 내부 일반 함수 this: ${this.name}`); // 브라우저는 this로 window를 전달
        }, 1000);

        // 최소 2초 뒤에 실행할 화살표 함수
        setTimeout(() => {
            console.log(`(3) setTimeout 내부 화살표 함수 this: ${this.name}`); // delayedGreet의 this (user)
        }, 2000);
    }
};

user.greet();          // 안녕하세요, 이아림입니다.
user.arrowGreet();     // 안녕하세요, 알 수 없는 사람입니다. (window.name이 빈 문자열일 때)
user.delayedGreet();
// (1) 일반 함수 내의 this: 이아림
// (2) setTimeout 내부 일반 함수 this: (window.name 값, 보통 빈 문자열)
// (3) setTimeout 내부 화살표 함수 this: 이아림

arrowGreet는 객체 리터럴 안에 적었지만, 객체 리터럴은 this를 만드는 범위가 아니므로 바깥인 전역 코드의 this, 즉 window를 씁니다. 그래서 user.arrowGreet()처럼 메서드로 호출해도 this는 user가 아닙니다. 객체의 메서드는 화살표 함수가 아니라 일반 함수나 메서드 단축 문법으로 정의하는 편이 안전합니다.

delayedGreet 안의 두 콜백은 차이가 분명합니다. 브라우저의 타이머는 일반 함수 콜백을 호출할 때 this로 window를 넘기므로 (2)는 window.name을 읽습니다. 화살표 함수 콜백은 넘겨받은 값 대신 자신을 만든 delayedGreet 호출의 this인 user를 쓰므로 (3)은 "이아림"을 출력합니다. 이처럼 콜백이나 이벤트 핸들러 안에서 바깥 메서드의 this를 유지하고 싶을 때 화살표 함수가 잘 맞습니다. 지정한 지연 시간은 최소한 기다리는 시간이라 실제 출력은 더 늦어질 수 있고, Node.js의 타이머는 일반 콜백에 다른 this를 넘기므로 (2)의 결과는 브라우저에서만 이렇습니다.


같은 함수를 여러 방식으로 호출하기

다음 예제는 하나의 함수를 네 가지 방식으로 호출해 결과를 비교합니다. read 함수에 "use strict"를 직접 넣었으므로, 독립 호출의 결과가 실행 환경과 관계없이 undefined로 고정됩니다.

const boxForThis = {
    label: "상자",
    read() {
        "use strict";
        return this === undefined ? "수신자 없음" : this.label;
    }
};
const readForThis = boxForThis.read;
const boundReadForThis = readForThis.bind(boxForThis);
console.log(boxForThis.read());
console.log(readForThis());
console.log(readForThis.call({ label: "다른 상자" }));
console.log(boundReadForThis.call({ label: "바꾼 상자" }));

출력은 상자, 수신자 없음, 다른 상자, 상자입니다. 첫 줄은 메서드 호출이라 this가 boxForThis입니다. readForThis()는 같은 함수를 독립 호출한 것이라 엄격한 함수의 this가 undefined가 됩니다. call()은 지정한 객체를 this로 쓰므로 "다른 상자"가 나옵니다. 마지막 줄은 이미 boxForThis로 bind()한 함수라, call()로 다른 객체를 넘겨도 this가 바뀌지 않습니다.

함수를 어디서 꺼냈는지가 아니라 실제로 어떤 형태로 호출했는지가 결과를 정한다는 점을 확인하세요. bind()는 함수를 실행하지 않고 새 함수를 만들 뿐이라는 점도 함께 기억해 둡니다.


정리

  • 일반 함수의 this는 정의한 위치가 아니라 호출하는 방식으로 정해지고, 화살표 함수는 정의한 곳의 바깥 this를 씁니다.
  • 일반 함수는 new 호출, call()·apply()·bind()로 지정한 값, 메서드를 호출한 객체, 독립 호출의 순서로 this를 판별합니다.
  • 독립 호출의 this는 엄격한 함수에서 undefined, 비엄격 함수에서 전역 객체이며, ES 모듈의 함수는 항상 엄격합니다.
  • 메서드를 변수에 담거나 콜백으로 넘기면 독립 호출이 되어 원래 객체를 잃으므로, bind()나 화살표 함수로 this를 유지합니다.
  • call()과 apply()는 곧바로 호출하며 인수를 넘기는 방식만 다르고, bind()는 this를 고정한 새 함수를 돌려주며 이후의 call()로 바뀌지 않습니다.
  • 객체 리터럴 안에 화살표 함수로 메서드를 적어도 this는 그 객체가 되지 않습니다.

연습 문제

1. 이미 묶은 함수를 다시 묶기

앞 절의 마지막 예제에서 const readForThis = boxForThis.read;만 const readForThis = boxForThis.read.bind({ label: "예약한 상자" });로 바꿨습니다. 다른 줄은 그대로일 때 네 출력을 적어 보고, 세 번째 줄의 call()이 지정한 객체를 바꿀 수 있는지 설명해 보세요.

해설 보기

출력은 상자, 예약한 상자, 예약한 상자, 예약한 상자입니다. 첫 줄은 여전히 원래 메서드를 boxForThis로 호출하므로 상자입니다.

나머지 세 줄은 모두 이미 { label: "예약한 상자" }로 묶은 함수를 호출합니다. 세 번째 줄의 call()도, boundReadForThis를 만든 두 번째 bind()도 처음 묶은 this를 덮어쓰지 못합니다.

2. 콜백으로 넘긴 메서드

메서드 호출 절의 user 객체(이름이 "김철수")로 setTimeout(user.greet, 0);을 브라우저의 비엄격 classic script에서 실행했습니다. 무엇이 출력될까요? 또 "김철수"가 출력되도록 고치는 방법을 두 가지 적어 보세요.

해설 보기

안녕하세요, 제 이름은 입니다.가 출력됩니다(window.name이 빈 문자열일 때). setTimeout()에는 함수만 넘어가고 user라는 정보는 넘어가지 않으므로, 타이머가 일반 함수를 호출하며 this로 window를 넘깁니다.

setTimeout(user.greet.bind(user), 0);처럼 bind()로 this를 고정하거나, setTimeout(() => user.greet(), 0);처럼 화살표 함수 안에서 메서드로 호출하면 안녕하세요, 제 이름은 김철수입니다.가 출력됩니다.

3. 중첩된 함수의 this

다음 코드의 두 출력을 예측해 보세요. 두 번째 출력이 "팀 A"가 되게 하려면 inner를 어떻게 바꾸면 될까요?

"use strict";
const team = {
    name: "팀 A",
    show() {
        console.log(this.name);
        function inner() {
            console.log(this === undefined ? "this 없음" : this.name);
        }
        inner();
    }
};
team.show();
해설 보기

출력은 팀 A, this 없음입니다. show()는 메서드로 호출해 this가 team이지만, inner()는 독립 호출이고 스크립트가 엄격 모드이므로 this가 undefined입니다. 안쪽 함수는 바깥 함수의 this를 물려받지 않습니다.

const inner = () => { console.log(this === undefined ? "this 없음" : this.name); };처럼 화살표 함수로 바꾸면, inner가 정의된 show 호출의 this인 team을 써서 팀 A가 출력됩니다.

this의 호출별 규칙은 MDN의 this 문서에서, 생성자 호출의 과정은 MDN의 new 연산자 문서에서 확인할 수 있습니다.

다음 글 프로토타입과 상속에서는 객체가 프로토타입 체인을 따라 속성과 메서드를 찾는 방식과, 생성자와 class로 상속을 만드는 원리를 살펴봅니다.