프로그래밍을 배우는 동안 엔지니어 분들이 강조했던 것이 있습니다. 머리로만 이해하고 넘어가지 말고 실제로 코드를 직접 작성해봐야 한다는 것이었죠. 개인적으로 그동안의 공부 패턴과 달라서 익숙해지는데 꽤나 많은 시간이 걸렸었습니다.
간단히 언급만 하고 넘어가는 개념도 있었습니다. 오늘 정리하려는 this 같은 경우가 그랬죠. 주로 당장은 이해하기 어려운 개념일 때였는데요. 그래도 꾸준히 성장한 덕분인지 당시에 검색했을 때보다는 조금 더 이해되는 느낌입니다.
먼저 객체 지향 프로그래밍에 대해 간략하게 언급하고 넘어가겠습니다.
객체 지향 프로그래밍이란 컴퓨터 프로그램을 명령어의 목록으로 보는 대신 여러 개의 독립된 "객체"들의 모임으로 이해하는 일종의 프로그래밍 패러다임입니다. 상태를 나타내는 프로퍼티와 동작을 나타내는 메소드를 묶은 "객체" 를 기본 단위로 해서, 프로그램을 이 "객체" 들의 상호작용으로 구성해나가는 것이죠.
예를 들어 자동차라고 하는 객체가 있다고 하겠습니다. 모델명이나 색상은 자동차라는 객체의 상태를 나타내는 프로퍼티가 될 수 있겠죠. 그리고 자동차를 운전하는 행위는 drive 라는 메소드로 표현할 수 있습니다. 이를 객체로 표현하면 다음과 같습니다.
const car = {
model : "SM5",
color : "white",
drive() {
console.log("운전합니다")
}
}
this 의 이해를 위해 위의 예시를 조금 변형해보겠습니다. car
라는 객체가 갖는 연료의 잔량(상태)을 currentFuel
이라는 프로퍼티로, 주유(동작)를 refuel()
이라는 메소드로 나타내는 것이죠. 현재 남은 기름이 10 리터이고, 한 번 주유할때마다 1 리터씩 추가되는 것으로 가정하면 다음과 같이 코드를 작성해볼 수 있습니다.
const car = {
currentFuel : 10,
refuel() {
currentFuel++;
}
}
car.refuel(); // ?
맨 아래에서처럼 car.refuel()
이라는 메소드를 호출하면 어떤 결과가 나타날까요? 언뜻 보기에는 currentFuel
이 11 이 될 것 같지만 실제로는 ReferenceError 를 띄웁니다. 왜 그럴까요? currentFuel
은 car 라는 객체의 프로퍼티이기 때문입니다.
프로퍼티에 접근하기 위해서는 마침표 표기법(또는 대괄호 표기법) 을 사용해야 하는데, 위의 예시에서 refuel()
메소드는 이러한 표현 없이 currentFuel
이라는 변수를 참조하게 되어있습니다. 어디에도 currentFuel
변수가 없으니 레퍼런스 에러가 뜨는 것이죠.
이 때 this 가 필요합니다. 모던 자바스크립트 Deep Dive
에서는 this 를 다음과 같이 정리하고 있습니다.
this 는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수(self-referencing variable)다. this 를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메소드를 참조할 수 있다.
...
this 가 가리키는 값, 즉 this 바인딩은 함수 호출 방식에 의해 동적으로 결정된다.
자바스크립트는 this 라는 특수한 식별자를 생성해서 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키도록 하는데요. 이를 반영해서 위의 코드의 객체를 수정하면 다음과 같습니다.
const car = {
currentFuel : 10,
refuel() {
this.currentFuel++;
}
}
이제 car.refuel()
메소드를 호출할 때마다 currentFuel
프로퍼티는 1 씩 증가합니다. this 가 가리키는 값이 car 라는 객체로 바인딩되었기 때문입니다.
위의 예시처럼 객체 리터럴로 생성한 객체라면 car.currentFuel++;
과 같은 방식으로 재귀적으로 자신을 참조하는 것이 가능은 합니다. 그러나 이러한 방법은 일반적이지 않을 뿐 아니라 바람직하지도 않다고 합니다. ES5 까지 사용된 생성자 함수나 ES6 에서 도입된 Class 문법 때문입니다.
생성자 함수 또는 Class 키워드로 정의한 객체는 new 연산자를 사용해 인스턴스를 생성하게 됩니다. 이 때 생성자 함수 또는 Class 내부의 메소드는 생성될 인스턴스를 참조해야 하겠죠. 만약 재귀적으로 자신을 참조하게 한다면 인스턴스를 생성하고 나서 메소드는 제대로 동작할 수 없을 겁니다.
class Car {
constructor() {
this.currentFuel = 10;
}
refuel() {
this.currentFuel++;
}
}
let sm5 = new Car();
sm5.refuel();
sm5 라는 변수에 클래스를 이용해 인스턴스를 생성해 할당하기 전에는 refuel()
메소드가 무엇을 가리켜야 하는지 알 수 없습니다. 만약 this 가 아니라 Car 라는 식별자를 만약에 사용했다고 하면 이 메소드는 제대로 동작할 수가 없습니다. 우리가 원하는 것은 sm5 의 currentFuel
프로퍼티 값이 변경되는 것이기 때문입니다. 이러한 이유로 this 라는 특수한 식별자가 필요한 것이죠.
객체 지향 프로그래밍과 Class 에 대해 배울 때 this 를 간략히 짚고 넘어갔다가, 이제야 this 에 대해 조금 더 깊이 공부해 볼 수 있었습니다. 사실 프로젝트를 진행하면서 this 를 사용해 본 적은 없는 것 같기도 하지만, this 는 기술 면접에서 등장할 수 있을법한 주제인 만큼 한 번쯤 짚고 넘어갈 필요가 있는 것 같습니다.
내일은 this 의 바인딩에 관해 정리해 볼 예정입니다. 오늘은 여기에서 마치도록 하겠습니다.