본문 바로가기

JavaScript

클래스

Prototype

JavaScript는 Prototype 기반의 언어입니다. 

 

const fruits = ["apple", "banana", "cherry"];

위와 같이 과일을 담을 배열이 있으며, 해당 배열을 리터럴 방식을 사용하여 배열을 생성하였습니다.

 

const fruits = new Array("apple", "banana", "cherry");

리터럴 방식을 사용하지 않고 배열을 생성할 경우 위와 같이 생성자 함수를 사용하여 배열을 생성할 수 있습니다. Array 클래스에 인수로 배열로 생성할 요소를 전달하여 배열을 생성하는 방법입니다.

 

console.log(fruits);
console.log(fruits.length);
console.log(fruits.includes("apple"));

이렇게 생성된 배열에 length를 사용하여 배열의 길이를 구할 수 있으며, 그 외에도 배열에 사용 가능한 메서드들을 사용할 수 있고 이때 사용하는 length와 Includes 등을 프로토타입 메서드라고 합니다.

 

Array.prototype.rati = function () {
  console.log(this);
};

fruits.rati(); // ['apple', 'banana', 'cherry'] 출력

위와 같이 Array.prototype 뒤에 지정 이름을 할당하여 사용자 정의 프로토타입 메서드를 생성할 수 있으며 생성된 메서드는 배열에서 사용이 가능한 메서드가 됩니다. 앞서 사용한 length와 Includes의 경우도 JavaScript에서 prototype속성에 등록이 된 메서드에 해당하합니다.

 

 

const rati = {
  firstName: "rati",
  lastName: "Lee",
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  },
};

console.log(rati.getFullName()); // rati Lee 출력

객체 rati는 내부에 getFullName이라는 메서드가 선언되어 있습니다. 

 

const merry = {
  firstName: "merry",
  lastName: "Lee",
};

console.log(rati.getFullName.call(merry)); //merry Lee 출력

객체 merry에는 내부에 메서드 getFullName가 선언되어 있지 않고 이름을 콘솔로 출력하고 싶은 상황이며, call 메서드를 사용하여 객체 rati는 내부에 getFullName이라는 메서드를 빌려서 사용하고 있습니다. 이러한 방식으로 같은 구조의 객체에서 선언된 메서드를 빌려 활용을 할 수 있지만, 만약 객체 내부에 선언된 메서드들이 많은 경우 해당 메서드를 빌려서 사용할 때마다 call 메서드를 사용해야 하는 등의 불편함이 많이 발생하게 될 것입니다. 이러한 단점을 보완하기 위해 사용할 수 있는 것이 Prototype입니다.

 

function User(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

const rati = new User("rati", "Lee");

console.log(rati);// User {firstName: 'rati', lastName: 'Lee'} 출력

firstName, lastName 두 개의 인수를 전달받는 함수 User(첫 글자 대문자)를 생성해 줍니다. 생성한 함수에 인수를 전달해주고 상수 rati에 할당해주게 되면 인수로 전달한 값을 속성의 값으로 가지는 객체 데이터를 할당받게 됩니다.

 

function User(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

User.prototype.getFullName = function () {
  return `${this.firstName} ${this.lastName}`;
};

const rati = new User("rati", "Lee");

console.log(rati.getFullName()); // rati Lee 출력

생성한 함수 User에 prototype 속성의 getFullName 메서드를 생성해주게 되면 동일한 결과를 확인할 수 있게 되었으며, getFullName 메서드를 재사용함에 있어 보다 편리하게 사용할 수 있게 되었습니다.

 

*ES6에서는 위의 prototype 속성의 메서드를 추가하여 사용하는 방법을 class 문법을 사용하여 보다 간소화된 방법으로 사용할 수 있습니다.

 

 

 

Class 기본 문법
class User {
  constructor(first, last) {
    this.firstName = first;
    this.lastName = last;
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const rati = new User("rati", "Lee");

console.log(rati.getFullName()); // rati Lee 출력

constructor 함수는 기존에 만든 함수 User의 역할을 하게 되고, 기존의 Prototype을 사용해 생성한 메서드는 클래스 내부에서 선언을 해주면 동일한 기능을 하게 됩니다.

 

 

 

getter / setter

getter는 값을 얻는 용도의 메서드, setter는 값을 지정하는 용도의 메서드입니다.

 

class User {
  constructor(first, last) {
    this.firstName = first;
    this.lastName = last;
  }

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const rati = new User("rati", "Lee");

console.log(rati.fullName);

기존의 클래스 내부에 생성했던 getFullName 메서드를 위와 같이 수정해 줍니다. get의 경우 함수 앞에 붙여서 사용하는 키워드로 get을 사용한 함수는 값을 얻어내기 위해 사용되는 함수가 되었으며, 이것이 getter입니다.

 

get을 사용한 fullName 메서드는 반환되는 데이터가 반환되는 하나의 속성으로 변경되게 되며, 사용 시 속성처럼 사용이 가능해지게 됩니다.

 

 

class User {
  constructor(first, last) {
    this.firstName = first;
    this.lastName = last;
  }

  // Getter
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  // Setter
  set fullName(value) {
    console.log(value);
  }
}

const rati = new User("rati", "Lee"); // rati Lee 출력

console.log(rati.fullName);

rati.fullName = "merry Lee"; // merry Lee 출력

메서드 fullName 앞에 set 키워드를 사용할 경우 fullName의 값을 지정할 수 있습니다. 하단에서 fullName 메서드에 할당 연산자를 사용하여 새로운 값을 할당, 즉 set 해주게 되면  할당된 값은 작성한 setter의 인수로 전달되고 setter가 작동하게 됩니다. 

 

setter는 정상적으로 작동하지만 아직 firstName과 lastName의 값은 변경되지 않았기 때문에 인수로 들어온 값은 split 메서드를 사용해 쪼개주고 각각의 속성에 배열 구조 분해를 사용하여 할당해주게 되면 정상적으로 속성의 값이 변경된 것을 확인할 수 있습니다.

 

 

 

정적 메서드

클래스의 정적 메서드란 protopype 속성을 사용하지 않고 사용하는 메서드를 의미합니다.

 

class User {
  constructor(first, last) {
    this.firstName = first;
    this.lastName = last;
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const rati = new User("rati", "Lee");

console.log(rati.getFullName());

위의 코드의 경우 클래스 User 안에 생성된 getFullName 메서드는 프로토타입 메서드에 해당합니다. 이때 생성된 메서드는 User의 인스턴스에서 사용이 가능한 메서드이지 클래스 User 자체적으로 호출할 수 없습니다.

 

class User {
  constructor(first, last) {
    this.firstName = first;
    this.lastName = last;
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  static isUser(user) {
    if (user.firstName && user.lastName) {
      return true;
    }

    return false;
  }
}

const rati = new User("rati", "Lee");

console.log(User.isUser(rati)); // true 출력

static 키워드를 메서드 앞에 붙여주게 되면 해당 메서드는 정적 메서드가 됩니다. 이렇게 생성된 메서드는 클래스 자체에서 바로 호출이 가능해집니다. 정적 메서드의 경우 클래스에서 사용이 가능하며 인스턴스에서는 사용이 불가능합니다.

 

정적 메서드 isUser에 객체 데이터를 전달하게 되면 내부의 조건에 맞는 값을 반환하게 됩니다.

 

 

 

상속
// 운송수단
class Vehicle {
  constructor(acceleration = 1) {
    this.speed = 0;
    this.accelration = accelration;
  }

  accelerate() {
    this.speed += this.acceleration;
  }

  decelerate() {
    if (this.speed <= 0) {
      console.log("정지!");
      return;
    }

    this.speed -= this.acceleration;
  }
}

클래스 Vehicle은 constructor 함수를 통해 기본 내용이 작성되어 있고, 인수로 가속도를 전달받으며, accelerate 메서드는 speed 속성에 가속도를 더하여 할당하고, decelerate 메서드는 속도가 0 이하이면 그대로 종료하고 0보다 크면 speed 속성에 가속도를 빼서 할당하고 있습니다.

 

// 자전거
class Bicycle extends Vehicle {
  constructor(price = 100, acceleration) {
    super(acceleration); // 클래스 Vehicle의 constructor함수에 인수 전달
    this.price = price;
    this.wheel = 2;
  }
}


const bicycle = new Bicycle(120);

console.log(bicycle); // Bicycle {speed: 0, acceleration: 1, price: 120, wheel: 2} 출력

클래스 Bicycle에 클래스 Vehicle의 기본 내용을 그대로 사용하고, 새로운 내용을 추가하고 싶습니다. 이때 extends를 사용하면 클래스 Vehicle 기본 내용을 상속받아 확장하여 사용할 수 있습니다. 이렇게 확장받아 사용할 경우 super를 사용해주어야 정상적으로 사용이 가능합니다.

 

super는 클래스 Vehicle의 constructor 함수에 해당하는 부분이며, 클래스 Vehicle는 가속도를 인수로 전달해야 사용 가능한 클래스이기 때문에 super의 인수로 가속도를 전달해 줍니다. 

 

콘솔을 확인해보면 클래스 Vehicle의 속성을 상속받아 정상적으로 사용하고, 클래스 Bicycle에 생성한 속성도 정상적으로 정의된 것을 확인할 수 있습니다.

 

 

// 자동차
class Car extends Bicycle {
  constructor(license, price, acceleration) {
    super(price, acceleration);
    this.license = license;
    this.wheel = 4;
  }
  accelrate() {
    if (!this.license) {
      console.log("무면허!");
      return;
    }

    this.speed += this.acceleration;
    console.log("가속!", this.speed);
  }
}

const car = new Car(false, 7000, 10);
console.log(car);// Car {speed: 0, acceleration: 10, price: 7000, wheel: 4, license: false} 출력

car.accelrate(); // 무면허! 출력

클래스 Car는 클래스 Bicycle를 상속받고 있으며, super를 통해 클래스 Bicycle의 constructor 함수에 인수를 전달하여 사용하고 있고 확장하여 추가 속성도 생성해주고 있습니다.

 

클래스 Car는 this.wheel의 값을 4로 할당해주고 있는데, 클래스 Bicycle에서 상속받은 wheel 속성의 값을 덮어쓰고 있고, accelrate 메서드를 재정의하고 있습니다.

 

*클래스 상속 시 상속받을 클래스 내부의 속성과 메서드 사용이 가능하며, 속성과 메서드를 재정의하여 사용할 수 있고 이것을 오버라이딩이라고 합니다.

 

 

 

instanceof
console.log(car instanceof Car); // true 출력
console.log(car instanceof Bicycle); // true 출력

instanceof를 사용하면 앞쪽의 클래스가 뒤쪽의 클래스에서 인스턴스로 만들어졌는지 확인할 수 있습니다. 

 

클래스 car의 경우 클래스 Bicycle를 상속받아 만들어진 클래스이기 때문에 Bicycle의 인스턴스에 해당하기 때문에 위와 같은 결과를 확인할 수 있게 됩니다.

 


 

 

// HTML
<body>
	<div id="app"></div>
</body>



// JS
const productList = {
// 상품 목록의 삼품 데이터
  products: [
    {
      title: "A Pillow ",
      imageUrl:""
      price: 19.99,
      description: "A soft pillow!",
    },
    {
      title: "A Carpet ",
      imageUrl:""
      price: 89.99,
      description: "A carpet which you might like - or not.",
    },
  ],
  // 상품 목록을 렌더링 시키기 위한 로직 및 메서드
  render() {
    const renderHook = document.getElementById("app");

    const productList = document.createElement("ul");
    productList.className = "product-list";

    for (const prod of this.products) {
      const prodEl = document.createElement("li");
      prodEl.className = "product-item ";
      prodEl.innerHTML = `
      <div>
        <img src = "${prod.imageUrl}" alt="${prod.title}" />
        <div class="prduct-item__countent">
            <h2>${prod.title}</h2>
            <h3>\$${prod.price}</h3>
            <p>${prod.description}</p>
            <button>Add to Cart</button>
        </div>
      </div>
      `;
      productList.append(prodEl);
    }

    renderHook.append(productList);
  },
};

productList.render();

예를 들어 쇼핑몰의 상품 목록 객체에 상품 목록을 구성하는 상품 데이터, 상품 목록을 렌더링 하기 위한 로직, 메서드 등이 들어가 있는 것입니다. 위의 코드처럼 객체 안에 해당 객체와 관련된 모든 코드를 넣어지만 특별히 무엇인가 절약이 되었다거나 좋은 점이 느껴지지 않고 있습니다. 이는 객체 리터럴 표기법의 문제 때문이며 적당한 객체 지향 코드를 작성하는 것은 다소 어렵게 느껴집니다.

 

*객체 리터럴이란 자바스크립트에서 객체를 생성하는 가장 일반적인 방법입니다. (중괄호를 사용하여 중괄호 내부에 프로퍼티를 정의하는 방법입니다.)

 

객체 리터럴 표기법은 데이터를 그룹으로 묶을 때는 유용하게 사용할 수 있지만, 객체 리터럴 표기법을 사용하면 재사용이 가능한 객체 코드를 쓰기가 어렵고, 해당 객체에 product를 추가해야 하는 경우가 생기면 product의 속성을 빠트리거나 오타가 나지 않게 주의해야 합니다.

 

때문에 product를 추가할 때 호출할 수 있는 함수를 만들어 인수를 전달하면 product 객체가 생성되고 생성된 객체를 반환하게 하면 좋을 것이며, 상품을 렌더링 하는 로직인 render에서 여러 번 생성이 가능한 독립된 객체가 있으면 좋을 것입니다. innerHTML 부분의 로직을 상품목록 객체에 넣는 것이 아닌 별도로 저장하여 단순히 실행만 되게 하면 좋을 것입니다. 

 

쉽게 정리하면 상품 목록 객체 하나에서 모든 로직과 데이터를 관리하는 것이 아닌 어떤 객체는 렌더링을 담당하고 어떤 객체는 데이터를 담당하게 컴포넌트 단위로 잘게 쪼개어 관리하는 것입니다.

 

 

 

 

클래스

클래스를 사용하면 객체를 보다 쉽게 만들 수 있고 객체를 위한 청사진을 정의할 수 있게 해 주기 때문에 클래스를 기반으로 하면 객체를 재생성하기가 쉬워집니다. 사실상 객체는 클래스의 인스턴스라고 불리기도 합니다.

 

클래스는 단지 객체가 어떻게 구성되어 있는지에 대한 정의이며, 속성과 메서드를 보여주고 로직을 어디에 저장했는지도 나타냅니다.

 

앞서 언급한 객체 리터럴의 문제를 클래스 사용이 대안법이 될 수 있으며, 클래스 사용이 항상 낫다고는 볼 수 없지만 빠르게 데이터를 묶는 등의 작업에서는 객체 리터럴보다 적합할 수 있습니다.

 

만약 재사용 가능한 로직을 한 곳에서 정의한 뒤 그것을 기반으로 여러 개의 객체를 생성하길 원한다면 클래스를 사용하는 것이 유용할 것입니다.

 

위의 객체 리터럴 표기법을 클래스 사용으로 수정해보도록 하겠습니다.

 

 

class ProductItem {
  title = "DEFAULT"; // 속성의 기본값을 필드에서 설정해줍니다.
  imageUrl = "";
  description = "";
  price = "";
}

class 키워드를 사용하여 이름이 ProductItem(첫 자는 대문자로 해줍니다.)인 클래스를 생성해 줍니다.

 

보통 클래스 안에는 객체가 클래스를 기반으로 어떻게 생성될지에 대한 청사진(객체의 속성과 메서드에 대한 정보)이 들어있게 됩니다. 때문에 클래스 필드에서 속성을 추가할 수 있으며, 상품 클래스는 항상 객체가 어떻게 생성될지에 대한 청사진으로 적용되기 때문에 속성의 기본값을 명시해 줍니다.

 

클래스 정의에는 값을 객체 리터럴처럼 콜론으로 지정하는 것이 아닌 등호로 지정을 하며 세미콜론으로 끝을 내줍니다.

 

클래스 필드에서 속성의 기본값을 설정해주게 되면 상품에 기반한 객체는 위의 네 가지 속성을 갖는 것으로 정의되게 됩니다.

 

*클래스는 객체를 대신하지 못하며 단지 클래스를 기반으로 필드에 설정한 속성의 기본값을 기반으로 객체를 만드는 것입니다.

 

class ProductItem {
  title = "DEFAULT";
  imageUrl = "";
  description = "";
  price = "";

  constructor(title, image, desc, price) {
    this.title = title;
    this.imageUrl = image;
    this.description = desc;
    this.price = price;
  }
}

생성한 클래스를 필요한 곳에서 함수처럼 호출하여 인수로 필드에서 설정한 속성에 값을 설정하여 객체를 만들기 위해 생성자 메서드 constructor를 추가해 줍니다. 생성자 메서드에 넣은 값으로 속성의 값이 초기화된 객체가 만들어지게 됩니다.

 

 

const productList = {
  products: [
  	// 기존의 상품 객체가 있던 곳에서 클래스 사용
    new ProductItem(
      "A Pillow",
      "",
      "A soft pillow!",
      19.99
    ),
    new ProductItem(
      "A Carpet",
      "",
      "A carpet which you might like - or not",
      89.99
    ),
  ],

new 키워드를 사용해 앞서 생성한 클래스를 기반으로 객체를 생성해주게 되면 클래스 필드에서 설정한 구조를 가진 새로운 객체를 반환하게 됩니다.

 

클래스의 인수로 전달되는 값들은 클래스에 있는 생성자 메서드에 전달되어 속성의 값이 전달된 인수로 설정된 객체가 반환되게 됩니다. 이렇게 클래스를 이용하여 보다 쉽고, 재사용이 가능한 객체를 생성해주었고 이로 인해 객체를 생성하는 과정에서 속성을 빼먹거나 오타를 내는 것에 대한 걱정이 사라지게 되었습니다.

 

 

 

 

클래스 필드와 프로퍼티

앞서 속성의 기본값을 설정한 부분을 클래스 필드라고 하며, 생성자 메서드로 인수를 전달받아 필드의 값을 변경했던 부분을 클래스 속성이라고 합니다.

 

클래스 필드와 클래스 속성은 이론적으로 분리해 놓은 것으로 클래스 기반으로 객체를 생성하면 필드가 속성이 되기 때문에 속성이라고 부릅니다. 객체가 생성되는 과정에서 생성자가 호출되어 속성을 얻게 되기 때문입니다. 

 

 

 

 

 

인스턴스 필드, 프로퍼티, 메서드 차이점

 

정적 프로퍼티와 메서드는 맨 앞에 static 키워드가 붙습니다. 인스턴스 프로퍼티와 달리 정적 프로퍼티와 메서드는 클래스 자체에서 액세스 할 수 있으므로 클래스를 인스턴스화할 필요가 없습니다. 일반적으로 헬퍼 클래스나 전역 구성에 사용됩니다.

 

인스턴트 프로퍼티는 static 키워드의 사용 없이 정의됩니다. 클래스를 기반으로 하는 인스턴스에서만 액세스 가능하므로 핵심적인 재사용 논리에 사용하는데 앞서 작성한 클래스 사용 예시도 인스턴스로만 작업을 하였고 new를 모든 클래스에서 사용했습니다.

 

 

 

 

 

'JavaScript' 카테고리의 다른 글

함수  (2) 2022.12.25
Closure  (0) 2022.12.25
정규표현식  (0) 2022.12.24
JavaScript 동작 원리(동기와 비동기)  (0) 2022.12.23
Memory Leak  (0) 2022.12.23