16.8버전이 나오기 전까지 리액트에서는 모든 컴포넌트가 클래스로 작성돼 있었다.
즉 함수를 컴포넌트로 작성하기 시작한 건 리액트 역사로 보았을 때 비교적 최근의 일이다.
과거에 작성된 리액트 코드를 읽기 위해서, 또 그러한 코드를 함수 컴포넌트로 개선하기 위해서 자바스크립트의 클래스가 어떤 식으로 작동하는지 이해해야 한다.
클래스를 이해하게 된다면, 리액트가 왜 패러다임을 바꾼지도 알 수 있고, 오래된 리액트 코드를 리팩터링하는 데도 도움이 될 것이다.
또한 자바스크립틔의 프로토타입 기반으로 작동하는 클래스의 원리를 이해하면 프로토타입에 대해서도 알 수 있다.
특정한 객체를 만들기 위한 일종의 템플릿 같은 개념
= 특정한 형태의 객체를 반복적으로 만들귀 위해 사용되는 것
클래스를 활용 => 객체를 만드는데 필요한 데이터, 이를 조작하는 코드를 추상화 가능
==> 객체 생성을 편리하게 할 수 있음
//Car 클래스 선언
class Car {
//constructor는 생성자다. 최초에 생성할 때 어떤 인수로 받을지 결정할 수 있으며,
//객체를 초기화하는 용도로도 사용된다.
constructor(name){
this.name = name
}
//메서드
honk(){
console.log(`${this.name}이 경적을 울립니다!`)
}
//정적 메서드
static.hello(){
console.log('저는 자동차입니다')
}
//setter
set age(value){
this.carAge = value
}
//getter
get age(){
return this.carAge
}
}
//Car 클래스를 활용해 car 객체를 만들었다.
const myCar = new Car('자동차')
//메서드 호출
myCar.honk()
//정적 메서드는 클래스로 만든 객체에서는 호출할 수 없다.
//Uncaught TypeError: myCar.hello is not a function
myCar.hello()
//setter를 만들면 값을 할당할 수 있다.
myCar.age = 32
//getter로 값을 가져올 수 있다.
console.log(myCar.age, myCar.name)//32자동차
constructor는 생성자로, 객체를 생성하는 데 사용하는 특수한 메서드
단 하나만 존재할 수 있다.
여러 개를 사용할 시 에러가 발생한다.
그러나 생성자에서 별 다르게 수행할 작업이 없다면 생략 가능하다.
//X
class Car{
constructor(name){
this.name = name
}
//SyntaxError: A class may only have one constructor
constructor (name){
this.name = name
}
}
//O
class Car{
//constructor는 없어도 가능하다
}
클래스로 인스턴스를 생성할 때 내부에 정의할 수 있는 속성값을 의미한다.
class Car{
constructor(name){
//값을 받으면 내부에 프로퍼티로 할당된다.
this.name = name
}
}
const myCar = new Car('자동차') //프로퍼티 값을 넘겨주었다.
기본적으로 인스턴스 생성 시 constructor 내부에는 빈 객체가 할당돼 있는데 바로 이 객체에 프로퍼티의 키와 값을 넣어서 활용할 수 있게 도와준다.
getter란 클래스에서 무언가 값을 가져올 때 사용된다.
getter를 사용하기 위해서는 get을 앞에 붙여야하고 이어서 getter의 이름을 선언해야 한다.
class Car{
constructor(name){
this.name = name
}
get firstCharactoer(){
return this.name[0]
}
}
const myCar = new Car('자동차')
myCar. firstCharacter //자
반대로 setter란 클래스 필드에 값을 할당할 때 사용한다.
마찬가지로 set이라는 키워드를 먼저 선언하고, 그 뒤를 이어서 이름을 붙이면 된다.
class Car{
constructor(name){
this.name = name
}
get firstCharacter(){
return this.name[0]
}
set firstCharacter(char){
this.name = [char, ...this.name.slice(1)].join('')
}
}
const myCar = new Car('자동차')
myCar.firstCharacter = '자'
//차를 할당한다
myCar.firstCharacter = '차'
console.log(myCar.firstCharacter, myCar.name) //차, 자동차
클래스 내부에서 선언한 메서드를 인스턴스 메서드라고 한다.
실제로 자바스크립트의 prototype에 선언되므로 프로토타입 메서드로 불리기도 한다.
class Car{
constructor(name){
this.name = name
}
//인스턴스 메서드 정의
hello(){
console.lg(`안녕하세요, ${this.name}입니다.`)
}
}
위 예제에서 Car이라는 클래스를 선언하고, 그 내부에 hello라고 하는 인스턴스 메서드를 정의했다.
이 인스턴스 메서드는 다음과 같이 선언할 수 있다.
const myCar = new Car('자동차')
myCar.hello()//안녕하세요, 자동차입니다.
위와 같이 새롭게 생성한 객체에서 클래스가 선언한 hello인스턴스 메서드에 접근할 수 있는 것을 확인할 수 있다.
이렇게 접근이 가능한 이유는 앞서 프로토타입 메서드라고 불리는 이유, 즉 메서드가 prototype에 선언됐기 때문이다.
const myCar = new Car('자동차')
Object.getPrototypeOf(myCar) // {constructor: f, hello:f}
Object.getPrototypeOf를 사용하면, 인수로 넘겨준 변수의 prototype을 확인할 수 있다.
{constructor:f, hello:f}를 반환받아 Car의 prototype을 받은 것으로 짐작할 수 있다.
Object.getPrototypeOf(myCar) === Car.prototype //true
Object.getPrototypeOf(myCar)를 Car.prototype과 비교한 결과 true가 반환되는 것을 볼 수 있다.
myCar.__proto__ === Car.prototype //true
__proto__또한 Object.getPrototypeOf와 동일하게 작동하는 것을 알 수 있다.
__proto__는 가급적 사용해서는 안되는 코드다.
typeof null === 'object'와 유사하게 원래 의도한 표준은 아니지만 과거 브라우저가 이를 사용했기 때문에 유지되는, 호환성을 지키기 위해서만 존재하는 기능이기 때문이다.
특이하게 클래스의 인스턴스가 아닌 이름으로 호출할 수 있는 메서드
class Car{
static hello(){
console.log('안녕하세요!')
}
}
const myCar = new Car()
myCar.hello()//Uncaught TypeError: myCar.hello is not a function
Car.hello() // 안녕하세요!
정적 메서드 내부의 this는 클래스로 생성된 인스턴스가 아닌, 클래스 자신을 가리키기 때문에 다른 메서드에서 일반적으로 사용하는 this를 사용할 수 없다.
이러한 이유로 리액트 클래스 컴포넌트 생명주기 메서드인 static getDerivedStateFromProps(props, state)에서는 this.state에 접근할 수 없다.
정적메서드는 비록 this에 접근할 수 없지만 인스턴스를 생성하지 않아도 사용할 수 있다.
생성하지 않아도 접근이 가능하기에 객체를 생성하지 않더라도 여러 곳에서 재사용이 가능하다.
이 때문에 애플리케이션에서 전역에서 사용하는 유틸 함수를 정적 메서드로 많이 활용하는 편이다.
리액트에서 클래스 컴포넌트를 만들기 위해서 extends React.Component 또는 extends React.Pure Component를 선언한 걸 본 적이 있을 것이다.
이 extends는 기존 클래스를 상속받아서 자식 클래스에서 이 상속받는 클래스를 기반으로 확장하는 개념이라 볼 수 있다.
class Car{
constructor(name){
this.name = name
}
honk(){
console.log(`${this.name} 경적을 울립니다!`)
}
}
class Truck extends Car {
constructor(name){
//부모 클래스의 constructor, 즉 Car의 constructor를 호출한다.
super(name)
}
load(){
console.log('짐을 싣습니다.')
}
}
const myCar = new Car('자동차')
myCar.honk()//자동차 경적을 울립니다!
const truck = new Truck('트럭')
truck.honk()// 트럭 경적을 울립니다!
truck.load(0//짐을 싣습니다!
Car를 extends한 Truck이 생성한 객체에서도, Truck이 따로 정의하지 않은 honk메서드를 사용할 수 있는 것을 볼 수 있다.
이 extends를 활용하면 기본 클래스를 기반으로 다양하게 파생된 클래스를 만들 수 있다.
클래스는 ES6에서 나온 개념으로, ES6 이전에는 프로토타입을 활용해 클래스의 작동 방식을 동일하게 구현할 수 있었다.
반대로 말하면, 클래스가 작동하는 방식은 자바스크립트의 프로토타입을 활용하는 것이라고 볼 수 있다.