본문 내용은 리액트보다는 js에 관한 내용입니다.
개발하는데 몰라도 전혀 지장없는 내용이니 스킵하셔도 됩니다.
TLDR : 리액트는 React.Component를 상속한 클래스만 클래스 컴포넌트로 취급합니다.
해당 클래스의 객체 플래그를 이용해 리액트는 해당 컴포넌트가 클래스인지 구분합니다.
Component.prototype.isReactComponent = {};
함수 컴포넌트와 클래스 컴포넌트. 개발은 이렇게 합니다
함수 컴포넌트
function Greeting() {
return <p>Hello</p>;
}
클래스 컴포넌트
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
사용할 때는 아무 차이 없죠
// Class or function — whatever.
<Greeting />
하지만 리액트에게는 다릅니다.
함수 컴포넌트는 아래와 같이 호출합니다.
// Your code
function Greeting() {
return <p>Hello</p>;
}
// Inside React
const result = Greeting(props); // <p>Hello</p>
클래스 컴포넌트는 아래와 같이 호출합니다.
즉, 인스턴스화 하고 렌더 메서드를 호출합니다.
// Your code
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
// Inside React
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
두 경우 모두 React의 목표는 렌더링된 노드를 얻는 것입니다(이 예에서는 <p>Hello</p>).
그러나 정확한 단계는 Greeting이 정의된 방식에 따라 다릅니다.
그렇다면 React는 무언가가 클래스인지 함수인지 어떻게 알 수 있을까요?
이 내용은 이전 게시물 (previous post)과 마찬가지로 실무 개발 시에 전혀 지장이 없습니다.
오히려 리액트보다는 js에 관한 내용이며, 저(dan) 또한 수 년간 해당 내용을 전혀 모르고 있었습니다.
제발 면접에서 이 질문을 하지 마시길 바랍니다(...).
// If Greeting is a function
const result = Greeting(props); // <p>Hello</p>
// If Greeting is a class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
자바스크립트에서 new 연산자가 무엇을 하는지 대략적으로 알아봅시다.
// Just a function
function Person(name) {
this.name = name;
}
var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 Won’t work
자바스크립트씨
{} 객체를 생성하고
Person 함수 내부 this가 이 객체를 가리키도록 하세요
저는 해당 객체에 this.name과 같은 항목을 할당할 거에요
그 다음 그 새로운 객체를 리턴해 주세요"
그것이 new 오퍼레이터가 하는 일입니다.
var fred = new Person('Fred'); // Same object as `this` inside `Person`
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
alert('Hi, I am ' + this.name);
}
var fred = new Person('Fred');
fred.sayHi();
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
alert('Hi, I am ' + this.name);
}
}
let fred = new Person('Fred');
fred.sayHi();
함수를 작성하면 JavaScript는 그것이 alert()처럼 호출되도록 의도된 것인지 또는
new Person()과 같은 생성자 역할을 하는지 추측할 수 없습니다.
Person과 같은 함수에 대해 new를 지정하는 것을 잊어버리면 혼란스러운 동작이 발생할 수 있습니다.
주 : 클래스도 함수일 뿐!
let fred = new Person('Fred');
// ✅ If Person is a function: works fine
// ✅ If Person is a class: works fine too
let george = Person('George'); // We forgot `new`
// 😳 If Person is a constructor-like function: confusing behavior
// 🔴 If Person is a class: fails immediately
이것은 this.name과 george.name 대신 window.name으로 처리되는 버그가 발생하는 것을 기다리는 대신
실수를 조기에 포착하는 데 도움이 됩니다.
그러나 이는 React가 클래스를 호출하기 전에 new를 넣어야 함을 의미합니다.
JavaScript가 오류로 처리하므로 일반 함수로 호출할 수 없습니다!
class Counter extends React.Component {
render() {
return <p>Hello</p>;
}
}
// 🔴 React can't just do this:
const instance = Counter(props);
이것은 함수 컴포넌트와 클래스 컴포넌트를 동일하게 취급하면 오류가 발생함을 의미합니다.
React가 이 문제를 해결하는 방법을 보기 전에
React를 사용하는 대부분의 사람들이 Babel과 같은 컴파일러를 사용하여
구형 브라우저용 클래스와 같은 최신 기능을 컴파일한다는 것을 기억하는 것이 중요합니다.
따라서 설계 시 컴파일러를 고려해야 합니다.
초기 버전의 Babel에서는 new 없이 클래스 함수를 호출할 수 있었습니다.
그러나 이것은 이후 몇 가지 추가 코드를 생성하여 수정되었습니다.
function Person(name) {
// A bit simplified from Babel output:
if (!(this instanceof Person)) {
throw new TypeError("Cannot call a class as a function");
}
// Our code:
this.name = name;
}
new Person('Fred'); // ✅ Okay
Person('George'); // 🔴 Cannot call a class as a function
new Person() | Person() | |
class | ✅ this는 Person의 인스턴스 | 🔴 타입에러 |
function | ✅ this는 Person의 인스턴스 | 😳 this는 window 혹은 undefined |
JavaScript의 함수에서 클래스를 구분할 수 있다고 해도
Babel과 같은 도구로 처리되는 클래스에서는 여전히 작동하지 않습니다.
브라우저에게는 단순한 함수일 뿐입니다. React에게는 운이 좋지 않습니다.
function Greeting() {
// We wouldn’t expect `this` to be any kind of instance here
return <p>Hello</p>;
}
const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor
class Friends extends React.Component {
render() {
const friends = this.props.friends;
return friends.map(friend =>
<Friend
// `this` is resolved from the `render` method
size={this.props.size}
name={friend.name}
key={friend.id}
/>
);
}
}
const Person = (name) => {
// 🔴 This wouldn’t make sense!
this.name = name;
}
따라서 JavaScript는 new로 화살표 함수를 호출하는 것을 허용하지 않습니다.
이건 어쨋든 실수이므로 일찍 알리는 것이 좋습니다.
이것은 JavaScript가 new 없이 클래스를 호출하는 것을 허용하지 않는 것과 유사합니다.
(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
function Greeting() {
return 'Hello';
}
Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}
그러나 JavaScript는 new로 호출된 함수가 다른 객체를 반환하여 new의 반환 값을 재정의하는 것도 허용합니다.
아마도 이것은 인스턴스를 재사용하려는 풀링과 같은 패턴에 유용한 것으로 간주되었습니다.
// Created lazily
var zeroVector = null;
function Vector(x, y) {
if (x === 0 && y === 0) {
if (zeroVector !== null) {
// Reuse the same instance
return zeroVector;
}
zeroVector = this;
}
this.x = x;
this.y = y;
}
var a = new Vector(1, 1);
var b = new Vector(0, 0);
var c = new Vector(0, 0); // 😲 b === c
그러나 new는 리턴의 객체가 아닌 경우 함수의 반환 값을 완전히 무시합니다.
문자열이나 숫자를 반환하면 아무것도 반환하지 않은 것과 같습니다.
function Answer() {
return 42;
}
Answer(); // ✅ 42
new Answer(); // 😳 Answer {}
React는 new로 클래스(Babel 출력 포함)를 호출해야 하지만
new 없이 일반 함수 또는 화살표 함수(Babel 출력 포함)를 호출해야 합니다.
그리고 그것들을 구별할 수 있는 확실한 방법은 없습니다.
컴포넌트를 클래스로 정의할 때 this.setState()와 같은 내장 메소드를 사용하기 위해
React.Component를 확장하고 싶을 것입니다.
모든 클래스를 감지하려고 하는 대신 React.Component 자손만 감지하면 어떨까요?
class A {}
class B extends A {}
console.log(B.prototype instanceof A); // true
따라서 "프로토타입 체인"은 prototype.prototype.prototype보다
__proto__.__proto__.__proto__와 비슷합니다.
이것을 이해하는 데 몇 년이 걸렸습니다.
그렇다면 함수나 클래스의 프로토타입 속성은 무엇입니까?
해당 클래스나 함수로 새로 생성된 모든 객체에 부여되는 __proto__입니다!
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
alert('Hi, I am ' + this.name);
}
var fred = new Person('Fred'); // Sets `fred.__proto__` to `Person.prototype`
fred.sayHi();
// 1. Does fred have a sayHi property? No.
// 2. Does fred.__proto__ have a sayHi property? Yes. Call it!
fred.toString();
// 1. Does fred have a toString property? No.
// 2. Does fred.__proto__ have a toString property? No.
// 3. Does fred.__proto__.__proto__ have a toString property? Yes. Call it!
클래스를 사용하면 이 메커니즘에 직접 노출되지 않지만 extends 또한 오래된 좋은 개념인 프로토타입 체인 위에서 동작합니다.
이것이 우리의 React 클래스 인스턴스가 setState와 같은 메소드에 액세스하는 방법입니다.
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
let c = new Greeting();
console.log(c.__proto__); // Greeting.prototype
console.log(c.__proto__.__proto__); // React.Component.prototype
console.log(c.__proto__.__proto__.__proto__); // Object.prototype
c.render(); // Found on c.__proto__ (Greeting.prototype)
c.setState(); // Found on c.__proto__.__proto__ (React.Component.prototype)
c.toString(); // Found on c.__proto__.__proto__.__proto__ (Object.prototype)
// `extends` chain
Greeting
→ React.Component
→ Object (implicitly)
// `__proto__` chain
new Greeting()
→ Greeting.prototype
→ React.Component.prototype
→ Object.prototype
두 단계의 상위 체인이 있습니다. (to React, to Object)
__proto__ 체인은 클래스 계층 구조를 반영하므로
Greeting.prototype으로 시작하여 __proto__ 체인을 따라가면
Greeting이 React.Component를 확장하는지 확인할 수 있습니다.
// `__proto__` chain
new Greeting()
→ Greeting.prototype // 🕵️ We start here
→ React.Component.prototype // ✅ Found it!
→ Object.prototype
편리하게도 x instanceof Y는 정확히 이런 종류의 검색을 수행합니다.
거기에서 Y.prototype을 찾는 x.__proto__ 체인을 따릅니다.
일반적으로 무언가가 클래스의 인스턴스인지 여부를 결정하는 데 사용됩니다.
let greeting = new Greeting();
console.log(greeting instanceof Greeting); // true
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype (✅ Found it!)
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype
console.log(greeting instanceof React.Component); // true
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype (✅ Found it!)
// .__proto__ → Object.prototype
console.log(greeting instanceof Object); // true
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype (✅ Found it!)
console.log(greeting instanceof Banana); // false
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype (🙅 Did not find it!)
console.log(Greeting.prototype instanceof React.Component);
// greeting
// .__proto__ → Greeting.prototype (🕵️ We start here)
// .__proto__ → React.Component.prototype (✅ Found it!)
// .__proto__ → Object.prototype
// Inside React
class Component {}
Component.isReactClass = {};
// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Yes
그러나 우리가 목표로 삼고자 하는 일부 클래스 구현은 정적 속성을 복사하지 않았으므로 (또는 비표준 __proto__를 설정함)
플래그가 손실되었습니다.
// Inside React
class Component {}
Component.prototype.isReactComponent = {};
// We can check it like this
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ✅ Yes
이게 다입니다!
React.Component를 확장하지 않으면 React는 프로토타입에서 isReactComponent를 찾지 못하고
component를 클래스로 취급하지 않습니다.
최종 API가 성공하면 사용자는 이 프로세스에 대해 생각할 필요가 없습니다.
대신 앱을 만드는 데 집중할 수 있습니다.
그러나 당신이 내부 또한 궁금하다면… 그것이 어떻게 작동하는지 아는 것이 좋습니다.
'FrontEnd' 카테고리의 다른 글
리덕스 이후의 삶(Life after Redux) : 리덕스 없이 리덕스 장점 누리기 (0) | 2022.10.03 |
---|---|
리액트 엘리먼트에 $$typeof 속성이 존재하는 이유 (0) | 2022.10.01 |
리액트 : 리렌더링 하도록 코딩하기 (부제 : memo 대신 useMemo) (0) | 2022.09.27 |
리액트 쿼리 : 리액트 라우터와 연계하기 (6) | 2022.09.27 |
javascript 프로젝트에 d.ts를 이용하여 타입스크립트 도입하기 (0) | 2022.09.22 |