
인트로
프로토타입을 읽을 때는 노션에 살짝 쿵 정리하고 넘어갔는데 마침 부트캠프에서 프로토타입 내용을 다뤄 블로그에 써보려고 합니다. 프로토타입 교체 부분에서 머리를 한번 깬 경험이 있는데요 지금은 괜찮지 않을까요?
객체지향프로그래밍부터
자바스크립트는 객체 기반 프로그래밍 언어입니다.
원시 타입을 제외한 나머지 값들이 모두 객체이니까.. 객체의 비중이 크죠.
객제지향 프로그래밍은 말 그대로 객체의 집합으로 프로그램을 표현하려는 것을 말합니다.
현생에서 사물과 같은 실체는 특징이나 성질을 나타내는 속성을 가집니다.
우린 이를 통해 실체를 인식하거나 구별합니다.
사람은 이름, 성별, 나이, 생년월일 등 다양한 속성을 가집니다. 이러한 속성들로 다른 사람과 구별하여 인식할 수 있죠.
다양한 속성 중에서 프로그램에 필요한 속성만 간추려 표현하는 걸 추상화라고 합니다.
그럼 '이름'과 '나이' 속성만 뽑아서 사람을 자바스크립트 객체로 표현해 봅시다.
const person = {
name: 'jisu',
age: '2n',
}

const circle = {
radius: 5,
getDiameter() {
return 2 * this.radius;
},
getPerimeter() {
return 2 * Math.PI * this.radius;
},
getArea() {
return Math.PI * this.radius ** 2;
}
};
console.log(circle);
console.log(circle.getDiameter());
console.log(circle.getArea());

원을 객체로 만들어서 더 자세히 알아보겠습니다. 반지름과 원의 지름 둘레 넓이를 구할 수 있는 메서드들이 있습니다.
여기서 반지름은 원의 상태를 나타내는 데이터(프로퍼티)이고 메서드는 동작입니다.
객체 지향 프로그래밍은 객체의 상태를 나타내는 데이터와 이를 조작할 수 있는 동작을 하나로 묶어서 생각합니다.
상속
객체 지향 프로그래밍의 핵심이라고 할 수 있습니다. 프로토타입 기반 상속을 통해 불필요한 중복을 제거합니다.
여기서 중복을 제거한다는 것은 코드의 재사용을 말합니다.
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * this.radius ** 2;
}
};
//반지름 1 인스턴스
const circle1 = new Circle(1);
//반지름 2 인스턴스
const circle2 = new Circle(2);
console.log(circle1.getArea === circle2.getArea); //false
console.log(circle1.getArea()); //3.141592653589793
생성자 함수 Circle을 이용한 생성한 모든 인스턴스는 radius 프로퍼티와 getArea 메서드를 가집니다.
그러나 인스턴스들의 getArea 메서드를 비교해 보니 각자 소유한 것으로 보입니다.
이는 코드를 중복 선언한 것이고 메모리도 불필요하게 낭비합니다. getArea 메서드를 하나만 생성하여 그것을 인스턴스가 공유하는 게 더 좋겠죠?
이제 프로토타입 기반 상속을 통해 불필요한 중복을 제거해 봅시다!
function Circle(radius) {
this.radius = radius;
};
Circle.prototype.getArea = function () {
return Math.PI * this.radius ** 2;
};
const circle1 = new Circle(1);
const circle2 = new Circle(2);
console.log(circle1.getArea === circle2.getArea); //true
console.log(circle1.getArea()); //3.141592653589793
이제 getArea 메서드를 Circle.prototype의 메서드로 할당하여 Circle로 생성한 모든 인스턴스가 getArea 메서드를 상속받아 사용할 수 있습니다. 이게 진짜 코드의 재사용.. 굳


프로토타입 객체
프로토타입 객체는 객체 간 상속을 구현하기 위해 사용됩니다.
프로토타입은 상위(부모) 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)를 제공합니다.
하위(자식) 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 사용가능합니다.
모든 객체는 [[Prototype]]이라는 내부 슬롯을 가집니다.(내부 슬롯 값이 null인 객체는 프로토타입이 없습니다.)
[[Prototype]]에 저장되는 프로토타입은 객체가 생성될 때 객체 생성 방식에 따라 결정됩니다.
예를 들어 객체 리터럴에 의해 생성된 객체의 프로토타입은 Object.prototype입니다.


모든 객체는 하나의 프로토타입을 가지며 모든 프로토타입은 생성자 함수와 연결되어 있습니다.
프로토타입 객체는 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근하고
생성자 함수는 자신의 prototype 프로퍼티를 통해 프로토타입 객체에 접근합니다.

__proto__
[[Prototype]] 내부 슬롯은 직접 접근할 수 없습니다. 접근하기 위해서는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입에 간접적으로 접근할 수 있습니다. 내부 슬롯은 프로퍼티가 아닙니다!

__proto__ 는 getter/setter 함수를 통해 [[Prototype]] 내부 슬롯의 값, 즉 프로토타입을 취득하거나 할당합니다.
__proto__는 Object.prototype의 프로퍼티입니다. 따라서 모든 객체는 상속을 통해 __proto__를 사용할 수 있습니다.
[[Prototype]] 내부 슬롯에 접근하기 위해 왜 접근자 프로퍼티를 사용할까요?
상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서입니다
const parent = {};
const child = {};
child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic __proto__ value
child의 프로토타입을 parent 객체로, parent의 프로토타입을 child 객체로 설정하면 에러가 발생합니다.
프로토타입 체인은 단방향 링크드 리스트로 구현되어야 하기 때문이죠.
프로퍼티 검색 방향이 한쪽으로만 되어있어야 합니다.
그렇지 않으면 프로토타입 체인의 종점이 없기 때문에 무한 루프에 빠지게 됩니다. (한무 쳇바퀴)
이런 걸 체크하기 위해 접근자 프로퍼티를 통해 프로토타입에 접근하고 교체하도록 되어있습니다.
그러나! __proto__ 를 직접 사용하는 것은 권장하지 않습니다.
모든 객체가 __proto__ 접근자 프로퍼티를 사용할 수 있는 것은 아닙니다.
직접 상속을 통해 Object.prototype을 상속받지 않은 객체는 __proto__ 를 사용할 수 없습니다. (뒤에서 더 살펴보죠.)
결론 프로토타입의 참조를 취득하고 싶다면 Object.getPrototypeOf 메서드를 사용하자.
교체하고 싶다면 Object.setPrototypeOf 메서드를 사용하자.

함수 객체의 prototype 프로퍼티
함수 객체만 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킵니다.

생성자 함수로서 호출할 수 없는 함수인 화살표 함수와 ES6 기준 메서드는 prototype 프로퍼티를 소유하지 않으며 프로토타입도 생성하지 않습니다.
일반 함수도 prototype 프로퍼티를 소유하지만 객체를 생성하지 않는다면 의미가 없습니다.
__proto__ 와 함수 객체의 prototype 프로퍼티는 동일한 프로토타입을 가리킵니다.

그러나 사용 목적이 다릅니다.
__proto__ | 객체가 자신의 프로토타입에 접근 또는 교체하기 위해 사용합니다. |
prototype 프로퍼티 | 생성자 함수가 자신이 생성할 인스턴스의 프로토타입을 할당하기 위해 사용합니다. |

constructor 프로퍼티와 생성자 함수
모든 프로토타입은 constructor 프로퍼티를 갖습니다.
constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킵니다.
이 연결은 생성자 함수가 생성될 때 이뤄집니다.

circle1 객체는 프로토타입의 constructor 프로퍼티를 통해 생성자 함수와 연결되는 겁니다.
circle1 객체는 constructor 프로퍼티가 없습니다. Circle.prototype으로부터 상속받아 사용합니다.

결국 생성자 함수에 의해 생성된 인스턴스는 프로토타입의 constructor 프로퍼티에 의해 생성자 함수와 연결되고
이때 constructor 프로퍼티가 가리키는 생성자 함수는 인스턴스를 생성한 생성자 함수입니다.
다양한 객체 생성
리터럴 표기법에 의한 생성된 객체도 프로토타입이 존재합니다. 하지만 이 경우 프로토타입의 constructor 프로퍼티가
가리키는 생성자 함수가 반드시 객체를 생성한 생성자 함수라고 할 수 없습니다.
1. 객체 리터럴
const obj = {};
console.log(obj.constructor === Object); //true
obj는 Object 생성자 함수로 생성한 객체가 아니지만 Object 생성자 함수와 constructor 프로퍼티가 연결되어 있습니다.
2. Object 생성자 함수
let obj = new Object();
console.log(obj.constructor === Object); //true
+) Object 생성자 함수를 확장한 클래스를 new 연산자와 함께 호출하는 경우
class Foo extends Object { }
let foo = new Foo();
인스턴스 -> Foo.prototype -> Object.prototype 순으로 프로토타입체인이 생성됩니다.

Object 생성자 함수 호출과 객체 리터럴의 평가는 OrdinaryObjectCreate를 호출하여 빈 객체를 생성하는 점에서 동일합니다. 그러나 new.target 확인이나 프로퍼티 추가와 같은 세부 내용은 다릅니다.
따라서 객체 리터럴에 의해 생성된 객체는 Object 생성자 함수가 생성한 객체가 아닙니다.
함수 객체의 경우 더 명확합니다. 함수 선언문과 함수 표현식을 평가하여 함수 객체를 생성한 것은 Function 생성자 함수가 아닙니다. 그러나 constructor 프로퍼티를 보면 함수의 생성자 함수는 Function 생성자 함수입니다.
function bar() { };
console.log(bar.constructor === Function); //true
리터럴 표기법에 의해 생성된 객체도 상속을 위해 프로토타입이 필요합니다.
따라서 이 친구들도 가상적인 생성자 함수를 갖습니다.
리터럴 표기법 | 생성자 함수 | 프로토타입 |
객체 리터럴 | Object | Object.prototype |
함수 리터럴 | Function | Function.prototype |
배열 리터럴 | Array | Array.prototype |
정규 표현식 리터럴 | RegExp | RegExp.prototype |
프로토타입은 생성자 함수와 생성되며 prototype, constructor 프로퍼티에 의해 연결되어 있습니다.
따라서 프로토타입과 생성자 함수는 단독으로 존재할 수 없습니다. 언제나 쌍으로 존재합니다.
프로토타입의 생성 시점
앞에서도 말했지만 프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성된다고 했습니다.
생성자 함수는 사용자 정의 생성자 함수와 자바스크립트의 빌트인 생성자 함수로 구분할 수 있는데요.
두 함수를 구분하여 프로토타입 생성 시점에 대해 알아보겠습니다.
사용자 정의 생성자 함수
일반 함수로 정의한 함수 객체는 new 연산자와 함께 생성자 함수로 호출할 수 있는데요.
constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성됩니다.
console.log(Person.prototype); //{constructor : f}
function Person(name) {
this.name = name;
}

생성자 함수와 더불어 생성된 프로토타입 내부를 봅시다.
생성된 프로토타입은 오직 constructor 프로퍼티만을 가지는 객체입니다.
프로토타입도 객체이기 때문에 자신의 프로토타입을 가집니다.
생성된 프로토타입의 프로토타입은 Object.prototype입니다.

빌트인 생성자 함수
빌트인 생성자 함수로는 Object. String, Number, Function, Array, RegExp, Date, Promise 등이 있습니다.
빌트인 생성자 함수도 빌트인 생성자 함수가 생성되는 시점에 프로토타입이 생성됩니다.
그리고 생성자 함수의 prototype 프로퍼티에 바인딩됩니다.
객체가 생성되기 이전에 함수와 프로토타입은 이미 객체화되어 존재합니다.
이후 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부에 슬롯에 할당됩니다.
객체 생성 방식과 프로토타입 결정
다음은 객체를 생성하는 방법입니다.
각 방식은 세부적인 차이가 있으나 추상 연산 OrdinaryObjectCreate에 의해 생성된다는 공통점이 있습니다.
또한 프로토타입이 OrdinaryObjectCreate에 전달되는 인수에 의해 결정됩니다.
- 객체 리터럴 - Object.prototype
- Object 생성자 함수 - Object.prototype
- 생성자 함수 - 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체
function Person(name) {
this.name = name;
}
const me = new Person('Ryu');

- Object.create 메서드 - 첫 번째 인자로 전달된 객체
const myObj = { a: 1 };
const newObj = Object.create(myObj);
console.log(newObj.__proto__ === myObj); // true
- 클래스 - 클래스의 'prototype' 프로퍼티에 저장된 객체 (클래스도 기술적으로는 함수입니다.)
프로토타입 체인
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hi My name is ${this.name}`);
}
const me = new Person('Ryu');
console.log(me.sayHello()); //Hi My name is Ryu
console.log(Object.getPrototypeOf(me) === Person.prototype); //true
console.log(me.hasOwnProperty('name')); //true
Person 생성자 함수에 의해 생성된 인스턴스는 Person.prototype뿐만 아니라 Object.porotype도 상속받습니다.
me 객체의 프로토타입은 Person.prototype이고 Person.prototype의 프로토타입은 Object.prototype입니다.
(프로토타입의 프로토타입은 언제나 Object.prototype입니다.)

자바스크립트는 객체의 프로퍼티에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없을 경우
[[Prototype]] 내부 슬롯의 참조를 따라 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색합니다.
이를 프로토타입 체인이라고 합니다.
Object.prototype은 언제나 프로토타입 체인의 최상위에 위치합니다. 즉 프로토타입 체인의 종점입니다.
따라서 Object.prototype의 프로토타입의 값은 null입니다.

오버라이딩과 프로퍼티 섀도잉
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hi My name is ${this.name}`);
}
const me = new Person('Ryu');
me.sayHello = function () {
console.log(`I am me's sayHello! Hi ${this.name}`);
}
me.sayHello(); //I am me's sayHello! Hi Ryu
인스턴스에 프로토타입 메서드와 이름이 같은 메서드를 추가했습니다.
프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면 프로포타입 프로퍼티를 덮어쓰는 것이 아니라
인스턴스 프로퍼티로 추가합니다.
이때 인스턴스 메서드 sayHello가 프로토타입 메서드 sayHello를 오버라이딩합니다.
상속 관계로 인해 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉이라고 합니다.
delete me.sayHello;
me.sayHello(); //Hi My name is Ryu
프로퍼티를 삭제할 때도 인스턴스 메서드 sayHello가 삭제됩니다.
하위 객체를 통해 프로토타입의 프로퍼티를 변경하거나 삭제하는 것을 불가능합니다.
프로토타입의 교체
프로토타입은 생성자 함수 또는 인스턴스로 교체할 수 있습니다.
저는 이 부분을 이해하는데 좀 시간이 걸렸습니다. 그림을 하나하나 직접 그리면서 2~3 회독을 하며 이해한 것 같습니다.
생성자 함수에 의한 프로토타입의 교체
아래는 Person.prototype에 객체 리터럴을 할당한 모습입니다. Person,prototype을 객체 리터럴로 교체했다고 할 수 있죠.
function Person(name) {
this.name = name;
}
Person.prototype = {
sayHello() {
console.log(`Hi My name is ${this.name}`);
}
};
const me = new Person('Ryu');


교체한 객체 리터럴에는 constructor 프로퍼티가 없습니다. constructor 프로퍼티는 자바스크립트 엔진이 암묵적으로 추가한 프로퍼티이기 때문입니다.
따라서 교체한 후 me의 생성자함수를 검색했을 때 Person이 아니라 Object가 나옵니다.

교체한 객체 리터럴에 constructor 프로퍼티를 추가하여 프로토타입의 constructor 프로퍼티를 연결해줘야 합니다.
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
sayHello() {
console.log(`Hi My name is ${this.name}`);
}
};
const me = new Person('Ryu');

인스턴스에 의한 프로토타입의 교체
인스턴스의 __proto__ 접근자 프로퍼티(Object.getPrototypeOf, Object.setPrototypeOf 메서드)를 통해 접근 및 교체를 할 수 있습니다.
function Person(name) {
this.name = name;
}
const me = new Person('Ryu');
const parent = {
sayHello() {
console.log(`Hi My name is ${this.name}`);;
}
}
Object.setPrototypeOf(me, parent);
console.log(me.__proto__ === parent); //true
me.sayHello(); //Hi My name is Ryu
위 예제를 실행할 경우 me 객체의 프로토타입은 parent 객체가 됩니다.
이때 constructor 프로퍼티가 없으므로 constructor 프로퍼티와 생성자 함수 간의 연결이 끊기며
Person 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 가리키지 않습니다.

교체한 객체 리터럴에 constructor를 추가하고 Person의 prototype 프로퍼티와 프로토타입 간 연결을 통해 이러한 상황을 해결할 수 있습니다.
생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결을 설정한 후
인스턴스의 프로토타입을 교체한 객체로 바꿔줍니다.
function Person(name) {
this.name = name;
}
const me = new Person('Ryu');
const parent = {
constructor: Person,
sayHello() {
console.log(`Hi My name is ${this.name}`);;
}
}
Person.prototype = parent;
Object.setPrototypeOf(me, parent);
me.sayHello(); //Hi My name is Ryu

이제 이해는 가는데 좀 많이 귀찮은 일인 게 느껴지네요. 연결이 다 끊기는 게 너무 개복치 같습니다.
프로토타입은 직접 교체하지 않는 게 좋다고 합니다. 직접 해보니 왜 그런지 알 것 같아요.
직접 상속이 편리하고 안전하니 이제 직접 상속을 살펴보죠.
직접 상속
Object.create에 의한 직접 상속
Obejct.create 메서드는 프로토타입을 지정하여 새로운 객체를 생성해 줍니다.
첫 번째 매개변수에 생성할 객체의 프로토타입으로 지정할 객체를 전달합니다.
두 번째 매개변수에는 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이루어진 객체를 전달합니다. 두 번째 인수는 생략 가능합니다.
//Object.prototype 상속받지못함
let obj = Object.create(null);
//Obj -> Object.prototype -> null
obj = Object.create(Object.prototype);
console.log(obj.__proto__ === Object.prototype); //true
// obj = {x:1}와 동일
obj = Object.create(Object.prototype, {
x: { value: 1, writable: true, enumerable: true, configurable: true }
});
const Proto = { x: 10 };
obj = Object.create(Proto);
console.log(obj.x); //10
console.log(obj.__proto__ === Proto); //true
프로퍼티 디스크립터 객체는 객체의 속성에 대한 정보를 포함하고 있습니다.(writable, enumerable, configurable 등)
기존 객체에 새로운 속성을 추가하거나 기존 속성의 값을 변경할 때 사용됩니다.
Object.create 메서드를 통해 직접 상속 구현이 가능하며 이것의 장점은 다음과 같습니다.
- new 연산자 없이 객체 생성
- 프로토타입을 지정하며 객체 생성
- 객체 리터럴에 의해 생성된 객체도 상속받을 수 있음
위 예제에서도 보셨듯 Object.create 메서드를 통해 프로토타입 체인의 종점에 위치하는 객체를 생성할 수 있는데요.
프로토타입 체인의 종점에 위치하는 객체는 Object.prototype의 빌트인 메서드를 사용할 수 없습니다.
const obj = Object.create(null);
obj.a = 1;
console.log(obj.hasOwnPropery('a')); //TypeError: obj.hasOwnPropery is not a function
이런 문제를 피하기 위해 Object.prototype의 빌트인 메서드는 apply/call/bind 메서드를 사용해 간접 호출하는 것이 좋습니다.
const obj = Object.create(null);
obj.a = 1;
console.log(Object.prototype.hasOwnProperty.call(obj, 'a')); //true
__proto__에 의한 직접 상속
__proto__ 접근자 프로퍼티를 사용해 직접 상속을 구현할 수 있습니다.
const myProto = { x: 10 }
const obj = {
y: 20,
__proto__: myProto
};
console.log(obj.x, obj.y); //10 20
정적 프로퍼티/메서드
생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 말합니다.
function Person(name) {
this.name = name;
}
//프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
}
//정적 프로퍼티
Person.staticProp = `static prop`;
//정적 메서드
Person.staticMethod = function () {
console.log(`staticMethod`);
}
const me = new Person('Lee');
Person.staticMethod(); //staticMethod
me.staticMethod(); //TypeError: me.staticMethod is not a function

정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조나 호출할 수 없습니다.
정적 프로퍼티/메서드는 인스턴스의 프로토타입 체인에 속하지 않기 때문입니다.
instanceof 연산자
객체 instanceof 생성자 함수
다음과 같은 형태로 사용합니다.
우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true를 그렇지 않으면
false를 반환합니다.
me instanceof Person의 경우 me 객체의 프로토타입 체인 상에 Person.prototype에 바인딩된 객체가 있는지 확인합니다.
function Person(name) {
this.name = name;
}
const me = new Person('Ryu');
const parent = {}
Object.setPrototypeOf(me, parent);
console.log(me instanceof Person); //false
console.log(me instanceof Object); //true
Person.prototype = parent;
console.log(me instanceof Person); //true
그렇기 때문에 생성자 함수에 의해 프로토타입이 교체되어도! constructor 프로퍼티와 생성자 함수 간의 연결이 파괴될 뿐
생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결을 파괴되지 않았기 때문에 instanceof에 영향을 주지 않습니다.
+) for.. in 문은 따로 다루진 않겠지만 정확한 표현이 나와있어 이 부분만 적어봅니다.
for... in 문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 이트리뷰트 [[Enumerable]]의 값이 true인 프로퍼티를 순회하면 열거합니다.
프로토타입 용어때문에 많이 헷갈렸는데 라이브 세션에서 아주 간단하게 정리해주셔서 호다닥 가져왔습니다.
생성자함수.prototype은 저장소고 __proto__는 저장소의 주소가 담겨있는 주소값이다.
드디어 끝이 났네요... 긴 여정이 끝났습니다. 확실히 정리하면서 이해를 하니 저번보다 훨씬 낫네요...
프로포타입은 console.dir로 찍어보고 그림을 직접 그려보는 게 답이다!
MDN & 모던 자바스크립트 Deep Dive 내용을 참고하였습니다.
'내가 해냄 > JS' 카테고리의 다른 글
디스트럭처링 할당 내가 해냄 (4) | 2023.03.21 |
---|---|
동기/비동기 내가 해냄 (0) | 2023.03.16 |
배열 고차 함수 내가 해냄 (0) | 2023.03.14 |
ES6 함수 내가 해냄 (0) | 2023.03.14 |
클래스 내가 해냄 (0) | 2023.03.08 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!