What is this - this란 무엇인가?

Chobo.dev·2024년 10월 13일
0

📝 TL;DR

this

  • 자신이 속한 객체 또는 자신이 생성할 객체(instance)를 가리키는 자기 참조 변수
  • 함수를 어떤 방식으로 호출하느냐에 따라 this의 값이 달라짐

실행 문맥 Execution Context

  • 전역 문맥(Global Context)
    - 브라우저에서는 window 객체
    - NodeJS에서는 global 객체 또는 module.exports에서 사용 가능한 해당 모듈
  • 함수 문맥(Function Context)
    - 일반 함수 호출 : window 객체
    - 메서드 호출 : 호출한 객체 자체
    - 생성자 함수 호출 : 생성자 함수가 (미래에) 생성할 객체(instance)
    - apply, call, bind 메서드에 의한 간접 호출 : 각 메서드에 첫 번째 인자(argument)로 전달한 객체
    - 화살표 함수 호출 : 자신이 정의된 블록 내에서 가장 바깥 범위 객체의 this 값

정적 범위 Lexical Scope

  • 자신이 선언된 위치에 따라 전역 범위와 지역 범위로 나뉨
  • 함수를 어디서 호출했는지, 어디서 정의했는지에 따라 동적 범위와 정적 범위로 나뉨
  • 자바스크립트는 정적 범위(Lexical Scope)를 따르는 언어임

🎯 들어가기에 앞서

  • Javascript에서 객체(Object)란 상태(State)를 나타내는 속성(Property)과 동작(Behavior)을 나타내는 메서드(Method)를 하나로 엮은 자료구조입니다.
  • 여기서 메서드는 자신이 속한 객체의 속성을 참조하려면 먼저 자신이 속한 객체를 가리키는 식별자(Identifier)를 참조할 수 있어야 합니다.

🧐 this 넌 누구냐

  • this는 자신이 속한 객체나 자신이 생성할(new) 객체(instance)를 가리키는 자기 참조 변수로서, this를 통해 자신이 속한 객체나 자신이 생성할 인스턴스의 속성과 메서드를 참조할 수 있게 됩니다.
  • this는 자바스크립트 엔진에 의해 암묵적으로 생성되며, 코드 어디서든 참조할 수 있습니다.

🔄 this 그때 그때 달라요

  • this는 아래 설명할 실행 문맥(Execution Context)이 생성될 때 함께 결정되며, 실행 문맥은 함수를 호출할 때 생성되므로 this는 함수를 호출할 때 동적으로 결정되는 값입니다.
  • 즉 함수를 어떤 방식으로 호출하느냐에 따라 그때마다 this의 값이 달라지게 됩니다.
  • 실행 문맥의 두 가지 종류인 전역 문맥(Global Context)과 함수 문맥(Function Context)를 현실 생활의 예시를 통해 들어보겠습니다.

전역 문맥(Global Context)에서의 this

  • 당신은 글로벌 다국적기업 의 CEO라고 상상해봅시다. 운영, 마케팅, 재무, 유통, 인사 등 회사의 모든 것을 책임지고 있는 것은 당신입니다.
  • 이와 같이 전역 문맥에서의 this는 CEO 본인과 같습니다.
  • 브라우저 환경에서 this는 window 객체로 객체, 생성자, 함수, 이름공간 등 DOM에 속한 모든 것들에 접근합니다.
  • node 환경에서의 this는 global 객체 또는 module.exports를 통해 사용 가능한 해당 모듈 객체로, 브라우저에서처럼 DOM이 존재하지 않고 NodeJS의 내장 모듈(fs, http, process 등) 및 사용자가 정의한 모듈을 전역적으로 사용할 수 있게 해 줍니다.
console.log(this); // window 객체를 반환
this.ceoName = `Tim Cook`; // 글로벌 변수 선언
const printCeoName = () => {
	console.log(window.ceoName); // this는 window 객체를 가리키므로 Tim Cook 출력
}
printCeoName();

환경별 특징 및 차이점 정리

  • 즉 window 객체, global 객체 모두 프로퍼티와 메서드를 통해 각각 브라우저와 NodeJS 환경에서 전역 문맥을 제공하는 역할을 합니다.
특징브라우저(window)Node.js(global)
환경웹 브라우저서버(nodejs 엔진)
DOM존재함, 문서 조작없음
핵심기능브라우저 API, DOM 인터페이스, 기본 함수모듈 시스템 연결, 서버 환경정보, 내장모듈

함수 문맥(Function Context)에서의 this

  • CEO가 모든 업무를 직접 처리하는 것은 불가능하므로 마케팅, 인사 담당 등 각 부서에 책임자를 두게 됩니다.
  • 이처럼 함수 문맥에서는 this각 각 부서의 책임자와 같습니다.
  • 함수가 실행될 때 해당 함수의 범위(Scope) 내에서 this 키워드를 통해 자신이 속한 부서(즉 호출된 객체)를 나타냅니다.
const researchAndDevDept = {
	budget: 10000000,
	allocateFunds: function () {
		console.log(this.budget); // this는 researchAndDevDept 객체를 가리키므로 10000000 출력
	} 
};
researchAndDevDept.allocateFunds(); // 출력: 10000000

함수 호출 방식에 따라 달라지는 this

  • 앞서 함수가 어떻게 호출되었는지에 따라 this가 동적으로 결정된다고 하였습니다.
  • 이에 따라 여러 가지 함수 호출 방식에 따라 this가 어떻게 결정되는지 알아보겠습니다.

일반 함수 호출

  • 일반 함수 호출에서 내부의 this는 아래와 같이 전역 객체 window를 가리키게 됩니다.
const func = function () {
	console.log(this); // 일반 함수 호출에서 this는 전역 객체 window를 가리킴
}
func(); // window 객체가 반환됨

메서드 호출

  • 메서드(Method)란 객체(Object)의 속성(Property)으로 정의된 함수입니다.
  • 객체의 메서드를 호출할 때 this는 호출한 객체 자체를 가리키게 됩니다. 즉, 아래 코드와 같이 메서드가 어떤 객체에 속해 있는지를 가리키게 됩니다.
const introduce = {
	name: 'ChoboDev',
	sayHello: function () {
		console.log(`안녕하세요. 나는 ${this.name} 입니다.`); // 해당 메서드의 객체에 들어있는 name을 참조함
	}
};
introduce.sayHello(); // 안녕하세요 나는 ChoboDev 입니다.
  • 마찬가지로 이벤트 핸들러를 등록하기 위해 사용하는 addEventListener 또한 메서드이기 때문에 this는 이벤트가 발생한(이벤트 핸들러가 속한) 객체를 가리키게 됩니다.
const boo = document.getElementById('boo');

boo.addEventListener('click', function() {
	console.log(this); // 이벤트가 발생한 버튼 요소(#boo)를 가리킴
});

생성자 함수 호출

  • 생성자 함수는 이름 그대로 객체(instance)를 생성하는 함수입니다. new 연산자를 통해 함수를 호출하게 되면 해당 함수는 생성자 함수로 동작하게 됩니다.
  • 여기서 instance는 실제 메모리 상에 할당된 객체를 의미합니다.
  • 생성자 함수 내부에서의 this는 생성자 함수가 (미래에) 생성할 객체(instance)를 가리키게 됩니다.
const Circle = function (radius) {
	this.radius = radius; // this는 new 연산자를 통해 생성될 Circle 객체
	this.getArea = function () {
		return Math.PI * this.radius * this.radius;
	}
}
const circle5 = new Circle(5); // 생성자 함수로 동작하여 Circle instance 생성
const circle8 = new Circle(8);
console.log(circle5.getArea()); // 78.53981633974483
console.log(circle8.getArea()); // 201.06192982974676

apply, call, bind 메서드에 의한 간접 호출

  • Javascript는 아래 정적 범위(Lexical Scope)에서 설명할 변수나 함수가 선언될 때 범위가 결정되는 정적 범위(Lexical Scope)를 따르는 언어입니다.
  • ES2015(ES6)가 등장하기 전, this가 가리키는 객체를 변경하고 싶을 때 apply, call, bind 등의 메서드를 통해 명시적으로 this가 가리키는 객체를 변경할 수 있는 유일한 방법이었습니다.
  • 즉 ES2015 이전에는 일반 함수에서 this는 실행 문맥에 따라 달라지거나 apply, call, bind 등을 통해 명시적으로 this가 가리키는 값을 변경(명시적 바인딩)하는 방법이 있었습니다.
  • apply 예시
    - sum 함수는 인수들을 arguments 객체를 통해 처리하며,
    - apply() 를 사용하여 두 번째 인자로 배열 numbers 를 전달함으로써 각 요소가 arguments 에 들어가 함수에 인수로 전달됩니다.
const sum = function () {
	let total = 0;
	for (let i = 0; i < arguments.length; i++) {
		total += arguments[i]; // arguments 객체는 함수 내에서 모든 인수를 참조할 수 있음
	}
	return total;
}
const numbers = [1, 2, 3, 4];
const result = sum.apply(null, numbers); // 결과: 10
// 첫 번째 인수로 주어진 this의 값을 명시적으로 null로, 두 번째 인수에 numbers 배열을 받아서 함수를 호출하게 됩니다.
  • call 예시
    - salute 함수의 this는 일반적으로 호출하는 경우 window 객체가 되지만, call() 을 사용하여 officer 객체를 첫 번째 인자로 전달함으로써 함수 실행 중 this를 officer 객체로 변경합니다.
    - 즉, salute 안에서 this.rank 는 officer 객체의 rank 속성 (값은 "Captain") 를 참조하게 됩니다.
const salute = function (prefix, rank) {
	console.log(`${prefix}! ${this.rank}!`); 
}
const officer = { rank: `Captain` };
salute.call(officer, `Aye aye Sir`, `Peter`); // 출력: Aye aye Sir! Captain!
// officer 객체는 rank 속성을 참조하여 Captain 값이 출력되며, call 호출 시 전달된 두 번째 인자는 사용되지 않았으므로 Peter는 무시됩니다.
  • bind 예시
    - salute.bind(officer) 을 통해 새로운 함수 bindSalute 가 생성되고, 이 함수는 salute 함수의 원본과 동일한 기능을 가지지만, 항상 this 값이 officer 객체로 고정됩니다.
    - 그래서 bindSalute('Yes sir', 'Peter') 호출 시에도 this 는 officer 객체를 가리키고 'Captain' 이 출력됩니다.
const salute = function (prefix, rank) {
	console.log(`${prefix}! ${this.rank}!`); 
}
const officer = { rank: `Captain` };
const bindSalute = salute.bind(officer); // this를 officer로 고정
bindSalute('Yes sir', 'Peter'); // 출력: Yes sir! Captain!

화살표 함수 호출

  • ES2015(ES6)에서 화살표 함수(arrow function)의 등장으로 새로운 방식으로 this를 결정하게 되었습니다.
  • 화살표 함수는 자신이 정의된 블록 내의 가장 바깥 범위(Scope)의 this 값을 가리킵니다(묵시적 바인딩).
  • 화살표 함수는 그 자체로 이미 this의 값이(묵시적으로) 결정되어 있으므로 apply, call, bind 등의 메서드를 통해 this가 가리키는 값을 변경할 수 없습니다.
  • 그리고 화살표 함수 내에서 this가 어떤 객체를 가리키는지는 함수가 호출되는 맥락에 따라 달라지는 데 일반적으로 호출된 위치(객체 메서드, 이벤트 핸들러 등)의 this 값이 상속됩니다.
  • 화살표 함수는 함수 자체의 this 바인딩(this가 가리키는 값)을 갖지 않으므로, 화살표 함수 내부에서 this를 참조하면 상위 범위의 this를 그대로 참조합니다. 이를 lexical this라 합니다.
  • 즉 this가 함수가 정의된 위치에 의해 결정되는 것입니다.
const officer = {
	rank: `Captain`,
	salute: () => { // 화살표 함수 사용
		console.log(`Aye aye Sir! ${this.rank}!`); // rank의 출력: undefined
	},
	oldSchooledSalute: function() { // 일반 함수 사용 (비교용)
		console.log(`Aye aye Sir! ${this.rank}!`); // rank의 출력: Captain
	}
};
officer.salute(); // this.rank는 화살표 함수 내부에서 정의된 객체(officer 객체가 아닌)에서 rank 속성이 없으므로 undefined를 반환함
officer.oldSchooledSalute(); // 일반 함수이므로 호출될 때 this가 호출하는 객체 자체인 officer 객체로 설정되므로 정상적으로 Officer 객체의 rank 속성(Captain)을 반환함

함수 호출별 총 정리

함수 호출 방식this가 가리키는 값
일반 함수 호출전역 객체(window)
메서드 호출메서드를 호출한 객체(이벤트 리스너의 경우 그 요소)
생성자 함수 호출생성자 함수가 (미래에) 생성할 객체(instance)
apply, call, bind 등 간접 호출apply, call, bind 메서드에 첫 번째 인수(argument)로 전달한 객체
화살표 함수자신이 정의된 블록 내에서 가장 바깥 범위의 this 값

💻 실행 문맥(Execution Context)에 관하여

실행 문맥에 대해

  • 위에서 크게 전역 문맥과 함수 문맥으로 설명을 했는데 둘 다 실행 문맥의 한 형태입니다.
  • 실행 문맥(Execution Context)이란 코드가 실행될 때, 어떤 객체와 변수를 사용할 수 있는 환경을 의미하며 그 공간 안에서 this, 함수, 변수 등을 활용하는 지역을 말합니다.
  • 종류로는 위에서 이미 언급한 전역 문맥(Global Context)과 함수 문맥(Function Context) 그리고 Eval 실행 문맥(Eval execution Context), 모듈 문맥(Module Context)이 있습니다.

전역 실행 문맥(Global Execution Context)

  • 전역 코드는 전역 변수를 관리하기 위해 최상위 범위(Scope)를 생성해야 합니다.
  • 전역 실행 문맥은 브라우저 창이 열릴 때 처음 생성되며, this는 window 객체를 가리킵니다.
  • 모든 전역 변수는 이곳에 저장되고 함수와 객체도 여기에 선언될 수 있습니다.
const globalVar = `I am inevitable`;
const globalFunc = function () {
	// 코드 실행
}

함수 실행 문맥(Function Execution Context)

  • 함수 코드는 지역 범위(Scope)를 생성하고 지역 변수, 매개변수, arguments 객체를 관리해야 합니다.
  • 모든 함수가 호출될 때 함수 실행 문맥이 생성되며, 이전에 설명한 바와 같이 함수의 종류와 호출 방식에 따라 this가 가리키는 값이 달라집니다.
  • 함수 내부에서 선언된 변수는 함수 범위(Function Scope) 내에 있으며, 호출 외부에서는 접근할 수 없습니다.
const myFunc =  function() {
	let localVar = `I am Iron Man`;
	console.log(localVar); // 출력 가능(내부에서만 사용 가능)
}
myFunc(); // 실행 후 'localVar' 는 메모리에서 제거됨 
console.log(localVar); // 참조 에러: localVar가 정의되지 않음, 외부에서는 접근 불가능

Eval 실행 문맥(Eval Execution Context)

  • eval 코드는 strict mode(엄격 모드)에서 자신만의 독자적인 범위(Scope)를 생성합니다.
  • eval() 함수를 사용할 때 생성되며, 이 함수는 문자열을 Javascript 코드로 해석하여 실행합니다.
  • eval() 호출 시 새로운 실행 문맥이 만들어지며 이때 this 값은 호출한 위치의 this 값이 됩니다.
  • eval()은 인자로 받은 코드를 호출한 함수의 권한으로 실행하는 매우 위험한 함수이므로 악의적인 사용자(해커 등)에 의해 실행되므로 사용해서는 안 됩니다.
  • 관련하여 좀 더 상세한 이유를 알고 싶다면 MDN eval() - eval을 절대 사용하지 말 것! 을 확인하세요.

모듈 실행 문맥(Module Execution Context)

  • 모듈 코드는 모듈별로 독립적인 모듈 범위(Scope)를 생성합니다.
  • ECMAScript Modules(ES Modules) 기능이 활용될 때 해당 문맥이 사용(import, export 키워드) 됩니다.
  • 각 모듈은 독립적인 실행 문맥을 가지며, 변수와 함수는 모듈 내에서만 접근할 수 있으며, 다른 모듈과의 상호작용을 하려면 import, export 키워드를 통해 제어합니다.
 // myModule.js (모듈 파일)
 const message = "Hello from module!";
 export { message }; // 모듈 내 변수를 export 

 // main.js (주 프로그램)
 import { message } from './myModule.js';
 console.log(message); //  출력: Hello from module! (모듈에서 import한 값 사용)

📌 정적 범위(Lexical Scope)에 관하여

코드의 범위(Scope)

  • 코드는 전역(Global)과 지역(Local)으로 구분할 수 있으며, 이 때 변수는 자신이 선언된 위치에 따라 자신이 유효한 범위(Scope)가 결정됩니다.
  • 즉 전역에서 선언된 변수는 전역 범위(Global Scope)를 갖는 전역 변수이고,
  • 지역에서 선언된 변수는 지역 범위(Local Scope)를 갖는 지역 변수입니다.
구분설명범위변수
전역코드의 가장 바깥 영역전역 범위전역 변수
지역함수 몸체 내부지역 범위지역 변수

범위(Scope)의 결정 방법

  • 우선 아래 코드의 실행 결과를 생각해봅시다.
// Apple Originals [Severance: 단절] 에서 인용함
const outieFunc = () => {
	let outieSay = `Mark(outie): 좋은 사람은 규칙을 따르지만 훌륭한 사람은 스스로를 따를 겁니다.`;
	const innieFunc = () => {
		let innieSay = `Mark(innie): 왜 우리는 여전히 어둠 속에서 일하고 있을까요?`;
		console.log(outieSay); // outie 메시지 출력됨
		console.log(innieSay); // innie 메시지 출력됨
	}
	innieFunc();
	console.log(innieSay); // ReferenceError: innieSay not defined
}
outieFunc();
  1. outieFunc는 외부 함수이며, 이 함수 내에서 outieSay라는 변수가 선언되고,
  2. innieFunc은 outieFunc 내에 정의된 내부 함수입니다.
    1. 내부 함수에서 외부 함수에 선언된 outieSay에 접근할 수 있지만
    2. 반대로 외부 함수에서 내부 함수의 변수인 innieSay에는 접근할 수 없습니다.
  3. 이는 정적 범위(Lexical Scope)에 의해 내부 함수는 바깥 범위(Outer Scope)에 접근할 수는 있으나 그 반대는 불가능하기 때문입니다.
  • 위에서처럼 실행 결과는 함수의 상위 범위가 무엇인지에 따라 두 가지 형태로 결정될 것입니다.
    - 함수를 어디서 호출했는지에 따라 상위 범위를 결정하기
    - 함수를 어디서 정의했는지에 따라 상위 범위를 결정하기

동적 범위(Dynamic Scope)

  • 첫 번째 방법을 동적 범위(Dynamic Scope)라 합니다.
  • 이는 함수를 정의하는 시점에는 함수가 어디서 호출될 지 알 수 없으므로 함수가 호출되는 시점에 동적으로 결정해야 하기 때문에 동적 범위(Dynamic Scope)라 부릅니다.

정적 범위(Lexical Scope)

  • 두 번째 방법을 정적 범위(Static Scope 또는 Lexical Scope)라 합니다.
  • 위와 같이 상위 범위가 동적으로 변하지 않고 함수의 정의가 평가(evaluation)되는 시점에서 상위 범위가 정적으로 결정되기 때문에 정적 범위(Lexical Scope)라 부릅니다.
  • 또한 Javascript를 포함한 많은 언어에서 Lexical Scope를 따릅니다.

📚 참고자료

도서

  • Flanagan, D. (2022). 자바스크립트 완벽 가이드(7판). 인사이트.
  • 이웅모. (2020). 모던 자바스크립트 Deep Dive : 자바스크립트의 기본 개념과 동작 원리. 위키북스.
  • 정재남. (2019). 코어 자바스크립트 : 핵심 개념과 동작 원리로 이해하는 자바스크립트 프로그래밍. 위키북스.

웹사이트

profile
좌충우돌 초보 개발자

2개의 댓글

comment-user-thumbnail
2024년 10월 13일

와우 넘 잘 만드셨네요. 글의 깊이가 느껴집니다. 근데 젤 첨에 TL;DR <-- 이거 뭐에요??

1개의 답글