1.3.1 클래스란 무엇인가?
클래스
의미: 특정한 객체를 만들기 위한 일종의 템플릿과 같은 개념. 즉, 특정한 형태의 객체를 반복적으로 만들기 위해 사용.
활용: 객체를 만드는 데 필요한 데이터나 이를 조작하는 코드를 추상화해 객체 생성을 더욱 편리하게 할 수 있음
클래스가 나오기 이전(ES6)에는 클래스라는 개념이 없어 객체를 만드는 템플릿 같은 역할을 함수가 했음. 반대로 말하면, 자바스크립트에서 클래스로 하는 모든 것들을 함수로도 동일하게 표현할 수 있음
// 클래스 예제 // 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() // 정적 메서드는 클래스에서 직접 호출 Car.hello() // 정적 메서드는 클래스로 만든 객체에서는 호출 불가능 // Uncaught TypeError: myCar.hello is not a function myCar.hello() // setter를 만들면 값 할당 가능 myCar.age = 32 // getter로 값 가져오기 가능 console.log(myCar.age, myCar.name) // 32 자동차
아래에서 클래스 내부 특징들에 대해 살펴보자.
constructor
의미: 생성자로, 객체를 생성하는 데 사용하는 특수한 메서드
특징: 단 하나만 존재할 수 있으며, 여러 개를 사용하면 에러 발생. 생성자에서 별다르게 수행할 작업이 없다면 생략 가능.
// 불가능 class Car { constructor(name) { this.name = name } // SyntaxError: A class may only have one constructor constructor(name) { this.name = name } } // 가능 class Car { // constructor는 없어도 가능 }
프로퍼티
의미: 클래스로 인스턴스를 생성할 때 내부에 정의할 수 있는 속성값
class Car { constructor(name) { // 값을 받으면 내부에 프로퍼티로 할당됨 this.name = name } } const myCar = new Car('자동차') // 프로퍼티 값을 넘겨줌
특징
- 기본적으로 인스턴스 생성 시 constructor 내부에는 빈 객체가 할당돼 있는데 이 빈 객체에 프로퍼티의 키와 값을 넣어서 활용할 수 있게 도와줌
- 다른 언어처럼 접근 제한자가 완벽하게 지원되는 것은 아니지만, #을 붙여서 private을 선언하는 방법이 ES2019에 추가됐고, 또 타입스크립트를 활용하면 다른 언어와 마찬가지로 private, protected, public을 사용할 수 있음. 물론 타입스크립트에서 가능한 것일 뿐, 자바스크립트에서는 기본적으로 모든 프로퍼티가 public임. 과거 private이 없던 시절에는 _를 붙여 접근해서는 안 된다는 의미의 코딩 컨벤션이 있긴 했지만 어디까지나 컨벤션일 뿐 기능적으로 private와 동일한 것은 아님.
getter와 setter
getter
의미: 클래스에서 무언가 값을 가져올 때 사용. 사용하기 위해서는 get을 앞에 붙여야 하고, 뒤이어서 getter의 이름을 선언해야 함.
class Car { constructor(name) { this.name = name } get firstCharacter() { 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.log(`안녕하세요, ${this.name}입니다.`) } } 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 반환. 이 외에 __proto__ 또한 해당 변수의 prototype을 확인할 수 있음.
myCar.__proto__ === Car.prototype // true
하지만 __proto__는 가급적 사용해서는 안 되는 코드. 1장에서 설명했던 typeof null === 'object'와 유사하게 원래 의도한 표준은 아니지만 과거 브라우저가 이를 사용했기 때문에 유지되는, 호환성을 지키기 위해서만 존재하는 기능이기 때문.
프로토타입 체이닝: 직접 객체에서 선언하지 않았음에도 프로토타입에 있는 메서드를 찾아서 실행을 도와주는 것. 모든 객체는 프로토타입을 가지고 있는데, 특정 속성을 찾을 때 자기 자신부터 시작해서 이 프로토타입을 타고 최상위 객체인 Object까지 훑음. 이 경우 myCar에서 시작해서 부모인 Car에서 hello를 찾는 프로토타입 체이닝을 거쳐 비로소 hello를 호출할 수 있게 됐음. 이와 비슷한 원리로 toString을 예로 들 수 있음. toString은 객체 어디에서도 선언하는 경우가 없지만 대부분의 객체에서 모두 사용할 수 있음. 이는 toString도 마찬가지로 프로토타입 체이닝을 거쳐 Object에 있는 toString을 만나기 때문
결론적으로 이 프로토타입과 프로토타입 체이닝이라는 특성 덕분에 생성한 객체에서도 직접 선언하지 않은, 클래스에 선언한 hello() 메서드를 호출할 수 있고, 이 메서드 내부에서 this도 접근해 사용할 수 있게 됨
정적 메서드
의미: 특이하게 클래스의 인스턴스가 아닌 이름으로 호출할 수 있는 메서드.
class Car { static hello() { console.log('안녕하세요!') } } const myCar = new Car() myCar.hello() // TypeError: myCar.hello is not a function Car.hello() // 안녕하세요!
정적 메서드 내부의 this는 클래스로 생성된 인스턴스가 아닌, 클래스 자신을 가리키기 때문에 다른 메서드에서 일반적으로 사용하는 this를 사용할 수 없음. 이러한 이유로 리액트 클래스형 컴포넌트 생명주기 메서드인 static getDerivedStateFromProps(props, state)에서는 this.state에 접근할 수 없음.
정적 메서드는 this에 접근할 수 없지만 인스턴스를 생성하지 않아도 사용할 수 있다는 점, 그리고 생성하지 않아도 접근할 수 있기 때문에 객체를 생성하지 않더라도 여러 곳에서 재사용이 가능하다는 장점이 있음. 이 때문에 애플리케이션 전역에서 사용하는 유틸 함수를 정적 메서드로 많이 활용하는 편.
상속
의미: 리액트에서 클래스형 컴포넌트를 만들기 위해 extends React.Component 또는 extends React.PureComponent를 선언한 것을 본 적 있을 것. 이 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() // 짐을 싣습니다!
Car를 extends 한 Truck이 생성한 객체에서도, Truck이 따로 정의하지 않은 honk 메서드를 사용할 수 있는 것을 볼 수 있음. 이 extends를 활용하면 기본 클래스를 기반으로 다양하게 파생된 클래스를 만들 수 있음.
'Modern React Deep Dive > 01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트' 카테고리의 다른 글
1.4.1 클로저의 정의 (0) | 2024.04.09 |
---|---|
1.3.2 클래스와 함수의 관계 (0) | 2024.04.08 |
1.2.4 함수를 만들 때 주의해야 할 사항 (0) | 2024.04.02 |
1.2.3 다양한 함수 살펴보기 (0) | 2024.04.02 |
1.2.2 함수를 정의하는 4가지 방법 (0) | 2024.04.02 |