JavaScript (클래스 고급)

83989 단어 JavaScriptJavaScript

클래스의 고급기능

클래스라는 문법은 객체를 더 안전하고 효율적으로 생성하기 위해 만들어진 문법이다. 즉 클랙스 문법들은 '어떤 위험이 있어서', '어떤 비효율이 있어서'라는 이유를 기반으로 만들어졌다. 따라서 '어떤 위험'과 '어떤 비효율'이 있었는지 이해할 수 있어야 문법을 제대로 활용할 수 있다.

상속

다음 코드는 Rectangle 이라는 사각형을 나타내는 클래스를 선언하고 사용하는 예이다. getPerimeter()라는 사각형의 둘레를 구하는 메소드와 getArea()라는 사각형의 넓이를 구하는 메소드를 추가했다.

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  // 사각형의 둘레를 구하는 메소드
  getPerimeter() {
    return 2 * (this.width + this.height);
  }
  // 사각형의 넓이를 구하는 메소드
  getArea() {
    return this.width * this.height;
  }
}

const rectangle = new Rectangle(10, 20);
console.log(`사각형의 둘레: ${rectangle.getPerimeter()}`);
console.log(`사각형의 넓이: ${rectangle.getArea()}`);

실행하면

사각형의 둘레: 60
사각형의 넓이: 200

위와같이 출력된다. 도형을 더 추가하고 싶어 Square라는 이름의 정사각형을 나타내는 클래스를 추가한다.

// 사각형 클래스
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  // 사각형의 둘레를 구하는 메소드
  getPerimeter() {
    return 2 * (this.width + this.height);
  }
  // 사각형의 넓이를 구하는 메소드
  getArea() {
    return this.width * this.height;
  }
}

// 정사각형 클래스
class Square {
  constructor(length) {
    this.length = length;
  }

  // 정사각형의 둘레를 구하는 메소드
  getPerimeter() {
    return 4 * this.length;
  }

  // 정사각형의 넓이를 구하는 메소드
  getArea() {
    return this.length * this.length;
  }
}

// 클래스 사용하기
const square = new Square(10);
console.log(`정사각형의 둘레: ${square.getPerimeter()}`);
console.log(`정사각형의 넓이: ${square.getArea()}`);

실행하면

정사각형의 둘레: 40
정사각형의 넓이: 100

코드를 보면 Rectangle 클래스와 Square 클래스는 큰 차이가 없다. 둘 다 사각형이다 보니 둘레를 구하는 메소드와 넓이를 구하는 메소드가 비슷하다.

클래스를 분리하는 것이 클래스를 활용하는 쪽에서는 편리하겠지만, 분리하면 클래스 선언 부분이 복잡해지는 문제가 발생한다. 이러한 문젤르 해결하기 위해 나온 것이 상속이다. 상속(inheritance)은 클래스의 선언 코들르 중복해서 작성하지 않도록 함으로써 코드의 생산 효율을 올리는 문법이다.

기본적인 형태는

class 클래스이름 extends 부모클래스이름 {

}

상속은 '상속'이라는 이름처럼 어떤 클래스가 가지고 있는 유산(속성과 메소드)을 다른 클래스에게 물려주는 형태로 사용한다. 이때 유산을 주는 클래스를 부모 클래스(parent class). 유산을 받는 클래스를 자식 클래스(child class)라고 부른다.

// 사각형 클래스
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
  // 사각형의 둘레를 구하는 메소드
  getPerimeter() {
    return 2 * (this.width + this.height);
  }
  // 사각형의 넓이를 구하는 메소드
  getArea() {
    return this.width * this.height;
  }
}

// 정사각형 클래스
class Square extends Rectangle {
  // Square 는 자식 클래스 Rectangle 부모 클래스
  constructor(length) {
    super(length, length);
    // super는 부모의 생성자 함수를 호출하는 코드
  }
  // getPerimeter(), getArea() 메소드 제거
}

// 클래스 사용하기
const square = new Square(10);
const rectangle = new Rectangle(10, 20);

// getPerimeter(), getArea() 메소드를 선언하지 않았지만, 상속 받았으므로 사용 가능
console.log(`정사각형 둘레: ${square.getPerimeter()}`);
console.log(`정사각형 넓이: ${square.getArea()}`);
console.log(`직사각형 둘레: ${rectangle.getPerimeter()}`);
console.log(`직사각형 넓이: ${rectangle.getArea()}`);

실행하면

정사각형 둘레: 40
정사각형 넓이: 100
직사각형 둘레: 60
직사각형 넓이: 200

Square 클래스에서 getPaerimeter() 메소드와 getArea() 메소드를 선언하지 않았다. 하지만 부모 클래스인 Rectangle 클래스에서 유산(속성관 메소드)을 상속받았으므로 사용할 수 있다.

getPerimeter() 메소드와 getArea() 메소드 내부에서 width 속성과 height 속성을 사용하고 있는데 Square 클래스를 보면 이러한 속성을 선언하는 코드조차 없어서 이상할 수 있다. 여기서 주목해야하는 코드는

super(length, length)

이다. super() 함수는 부모의 생성자를 나타내는 함수이다. super()를 호출하면 Rectangle 클래스의 constructor(width, height)가 호출되어 width 속성과 height 속성이 들어간다.

프로그래밍은 분업화가 매우 잘 되어 있는 분야이다. 그래서 프로그램을 개발할 때 사용하는 거대한 규모의 클래스, 함수. 도구 등의 집합을 의미하는 프레임워크(framework)엔진(engine)이라는 것을 만드는 개발자와 이를 활용해서 다수를 대상으로 하는 서비스. 애플리케이션, 게임을 개발하는 개발자가 다른 경우가 많다. 전자를 프레임워크 개발자 또는 엔진 개발자 등으로 불리며, 후자를 애플리케이션 개발자 등으로 부른다.

이떄 애플리케이션 개발자들이 프레임워크와 엔진을 활용하는 가장 기본적인 방법이 상속이다. 그래서 상속을 어느 정도 알아야 프레임워크와 엔진을 다룰 수 있다.

2021-07-06-Javascript

private 속성과 메소드

개발의 규모가 커지면서 프레임워크 개발자와 애플리케이션 개발자가 나뉘자, 코드들이 위험해지기 시작했다. 프레임워크 개발자와 애플리케이션 개발자가 나뉘자, 코드들이 위험해지기 시작했다. 프레임워크 개발자가 Square 클래스를 만들고. 이를 애플리케이션 개발자가 활용한다고 가정하고 코드를 살펴보면

// 정사각형 클래스
class Square {
  constructor(length) {
    this.length = length;
  }

  // 정사각형의 둘레를 구하는 메소드
  getPerimeter() {
    return 4 * this.length;
  }

  // 정사각형의 넓이를 구하는 메소드
  getArea() {
    return this.length * this.length;
  }
}

// 클래스 사용
const square = new Square(-10); // 길이에 음수를 넣어서 사용
console.log(`정사각형의 둘레: ${square.getPerimeter()}`);
console.log(`정사각형의 둘레: ${square.getArea()}`);

현재 코드를 보면 Square 객첼르 생성할 때 생성자의 매개변수로 음수를 전달했다. 그런데 '길이'라는 것은 음수가 나올 수 없다. 프레임워크 개발자들은 Square 클래스를 만들 때 "설마 누가 길이를 음수로 넣겠어" 라고 생각했을 것이다. 하지만 활용하는 사람은 이러한 사실을 몰랐을 수 있다.

정사각형의 둘레: -40
정사각형의 둘레: 100

실행하면 결과가 위와같이 나온다.

이러한 문제를 막는 방법으로는 다음 코으와 같이 조건문을 활용해 0이하의 경우 예외를 발생시켜 클래스의 사용자에게 그렇게는 할 수 없다고 인지시켜준느 방법이 있다.

// 정사각형 클래스
class Square {
  constructor(length) {
    if (length <= 0) {
      throw "길이는 0보다 커야 합니다.";
    }
    // throw 키워드를 사용해 강제로 오류를 발생
    this.length = length;
  }

  // 정사각형의 둘레를 구하는 메소드
  getPerimeter() {
    return 4 * this.length;
  }

  // 정사각형의 넓이를 구하는 메소드
  getArea() {
    return this.length * this.length;
  }
}

// 클래스 사용
const square = new Square(-10); // 길이에 음수를 넣어서 사용
console.log(`정사각형의 둘레: ${square.getPerimeter()}`);
console.log(`정사각형의 둘레: ${square.getArea()}`);

실행하면

길이는 0보다 커야 합니다.

하지만 이러한 코드만으로는 다음과 같이 생성자로 객체를 생성한 이후에 사용자가 length 속성을 변경하는 것을 막을 수 없다.

사용자의 잘못된 사용 예

// 클래스 사용
const square = new Square(10);
square.length = -10; // 이렇게 음수를 지정하는 것은 막을 수 없다.
console.log(`정사각형의 둘레: ${square.getPerimeter()}`);
console.log(`정사각형의 둘레: ${square.getArea()}`);

이처럼 클래스 사용자가 클래스 속성(또는 메소드)을 의도하지 않은 방향으로 사용하는 것을 막아 클래스의 안정성을 확보하기 위해 나온 문법이 private 속성과 메소드이다. 문법은

class 클래스이름 {
	#속성이름
	#메소드이름 () {
	
	}
}

속성과 메소드 이름 앞에 #을 붙이기만 하면 된다. 이처럼 #이 붙어있는 속성과 메소드는 모두 private 속성과 메소드가 된다. 주의할 것이 있다면 private 속성은 사용하기 전에 미리 외부에 어떤 속성을 private 속성으로 사용하겠다고 선언해줘야 한다는 것이다.

이전 코드 length 속성을 #length 속성으로고 변경해보려고 한다.

// 정사각형 클래스
class Square {
  #length; // 이 위치에 해당 속성을 private 속성으로 사용하겠다고 선언

  constructor(length) {
    if (length <= 0) {
      throw "길이는 0 보다 커야 합니다.";
    }

    this.#length = length;
  }

  getPerimeter() {
    return 4 * this.#length;
  }
  getArea() {
    return this.#length * this.#length;
  }
}

// 클래스 사용
const square = new Square(10);
console.log(`정사각형 둘레: ${square.getPerimeter()}`);
console.log(`정사각형 넓이: ${square.getArea()}`);

실행해보면

정사각형 둘레: 40
정사각형 넓이: 100

위와같이 출력된다.

이렇게 private 속성으로 변경하면 클래스 외부에서는 해당 속성에 접근할 수 없다. 예를 들어 square 객체의 length 속성을 변경해보려고 하는데 변경해도 클래스 내부에서 사용하고 있는 속성은 #length 속성이지 length 속성이 아니기 때분에 결과에는 어떠한 영향도 주지 않는다.

// 정사각형 클래스
class Square {
  #length; // 이 위치에 해당 속성을 private 속성으로 사용하겠다고 선언

  constructor(length) {
    if (length <= 0) {
      throw "길이는 0 보다 커야 합니다.";
    }

    this.#length = length;
  }

  getPerimeter() {
    return 4 * this.#length;
  }
  getArea() {
    return this.#length * this.#length;
  }
}

// 클래스 사용
const square = new Square(10);
square.length = -10; // 클래스 내부의 length 속성을 사용하여 변경
console.log(`정사각형 둘레: ${square.getPerimeter()}`);
console.log(`정사각형 넓이: ${square.getArea()}`);

실행하면

정사각형 둘레: 40
정사각형 넓이: 100

위와같이 나온다.

만약에 #length 속성을 사용하면 오류가 난다.

// 클래스 사용
const square = new Square(10);
square.#length = -10; 
console.log(`정사각형 둘레: ${square.getPerimeter()}`);
console.log(`정사각형 넓이: ${square.getArea()}`);

실행하면

SyntaxError: Private field '#length' must be declared in an enclosing class

이렇게 만든 private 속성은 클래스 외부에서는 접근할 수 없으므로 클래스 사용자가 클래스를 잘못사용하는 문제를 줄일 수 있다.

게터와 세터

private 속성을 사용하면 외부에서는 #length 속성에 아예 접근할 수 없는 문제가 발생한다. 현재 square 객체의 length 속성이 몇인지 확인할 수도 없고, length 속성을 변경하고 싶어도 변경할 수 없다. 그래서 프레임워크 개발자들은 상황에 따라서 속성을 읽고 쓸 수 있는 메소드를 만들어서 제공한다.

// 정사각형 클래스
class Square {
  #length;

  constructor(length) {
    this.setLength(length);
  }

	// 함수를 사용하므로, 내부에서 예외 처리 등을 할 수 있다.
  setLength(value) {
    if (value <= 0) {
      throw `길이는 0보다 커야 합니다.`;
    }
    this.#length = value;
  }

  getLength(value) {
    return this.#length;
  }

  getPerimeter() {
    return 4 * this.#length;
  }
  getArea() {
    return this.#length * this.#length;
  }
}

// 클래스 사용
const square = new Square(10);
console.log(`한 변의 길이는 ${square.getLength()} 입니다.`);

// 예외 발생시기키
square.setLength(-10);

실행하면

한 변의 길이는 10 입니다.
길이는 0보다 커야 합니다.

코드를 보면 getLength() 메소드와 setLength()메소드가 추가된 것을 볼 수 있다.

이때 getOO() 메소드처럼 속성 값을 확인할 때 사용하는 메소드게터(getter)라고 부르며, setOO()메소드처럼 속성에 값을 지정할 때 사용하는 메소드세터(setter)라고 부른다.

처음 게터와 세터를 배우면 모든 private 속성에 게터와 세터를 붙이려고 하는 경우가 있다. 게터 세터는 필요한 경우에만 사용한다. 만약 사용자가 값을 읽는 것을 거부하겠다면 게터를 만들지 않아도 된다. 또한 사용자가 값을 지정하는 것을 거부하겠다면 세터를 만들지 않아도 된다. 아예 속성에 접근하지 못하게 둘 다 막을 수 있다.

get 키워드와 set 키워드 문법

class 클래스이름 {
	get 이름 () { return}
	set 이름 (value) {  }
}

get 키워드와 set 키워드 조합하기

// 정사각형 클래스
class Square {
  #length;

  constructor(length) {
    this.length = length;
    // this.length에 값을 지정하면, set length(length) 메소드 부분이 호출된다.
  }

  get length() {
    return this.#length;
  }

  get perimeter() {
    return this.#length * 4;
  }

  get area() {
    return this.#length * this.#length;
  }

  set length(length) {
    if (length <= 0) {
      throw `길이는 0 보다 커야 합니다.`;
    }
    this.#length = length;
  }
}

// 클래스 사용
const squareA = new Square(10);
console.log(`한 변의 길이: ${squareA.length}`);
console.log(`둘레: ${squareA.perimeter}`);
console.log(`넓이: ${squareA.area}`);

// 예외 발생
const squareB = new Square(-10);

실행하면

한 변의 길이: 10
둘레: 40
넓이: 100
길이는 0 보다 커야 합니다.

Square 클래스가 갖고 있던 모든 getOO()과 setOO() 형태의 코드에서 get과 set 뒤에 띄어쓰기를 넣었다. 클래스쪽은 큰 변경이 없는 것 같지만, 클래스를 활용하는 쪽에서는 단순하게 속성을 사용하는 형태처럼 게터와 세터를 사용할 수 있게 되었다.

이렇게 코드를 작성하면 코드를 사용하는 쪽에서 게터와 세터를 훨씬 더 쉽게 사용할 수 있다.

static 속성과 메소드

프레임워크 개발자들은 더 효율적으로 프레임워크를 개발할 수 있게 다양한 패턴을 고안한다. 이러한 패턴을 디자인패턴(design pattern)이라고 부른다.

원래 자바스크립트에는 클래스라는 기능이 없었다. 하지만 여러 디자인 패턴을 활용하기 위해서 클래스 문법들이 계속해서 추가된 것이라 할 수 있다. 비교적 최근 추가된 문법으로 static 속성static 메소드가 있다. static을 정적이라는 한국어로 불러서 정적 속성, 정적 메소드라고 부르기도 한다.

class 클래스이름 {
	static 속성 =static 메소드 () {

	}
}

static 속성과 메소드는 인스턴스를 만들지 않고 사용할 수 있는 속성과 메소드이다. 그냥 일반적인 변수와 함수처럼 사용할 수 있다.

클래스 이름.속성
클래스 이름.메소드()

static 키워드 사용하기

class Square {
  #length;
  static #counter = 0; // private 특성과 static 특성은 한꺼번에 적용할 수도 있다.
  static get counter() {
    return Square.#counter;
  }

  constructor(length) {
    this.length = length;
    Square.#counter += 1;
  }

  static perimeterOf(length) {
    return length * 4;
  }

  static areaOf(length) {
    return length * length;
  }

  get length() {
    return this.#length;
  }

  get perimeter() {
    return this.#length * 4;
  }

  get area() {
    return this.#length * this.#length;
  }

  set length(length) {
    if (length <= 0) {
      throw `길이는 0보다 커야 합니다.`;
    }
    this.#length = length;
  }
}

// static 속성 사용
const squareA = new Square(10);
const squareB = new Square(20);
const squareC = new Square(30);
console.log(`지금까지 생성된 Square 인스턴스는 ${Square.counter}개 입니다.`);

// static 메소드 사용하기
console.log(
  `한 변의 길이가 20인 정사각형의 둘레는 ${Square.perimeterOf(20)} 입니다.`
);
console.log(
  `한 변의 길이가 30인 정사각형의 넓이는 ${Square.areaOf(30)} 입니다.`
);

실행하면

지금까지 생성된 Square 인스턴스는 3개 입니다.
한 변의 길이가 20인 정사각형의 둘레는 80 입니다.
한 변의 길이가 30인 정사각형의 넓이는 900 입니다.

#counter라는 이름의 static 속성과 counter()라는 이름의 static 메소드(게터)를 만들었다. #counter라는 속성은 Square 객체의 생성자가 호출될 때마다 1씩 증가하도록 했다. 이를 활용하면 현재까지 Square 객체가 몇 개 생성되었는지 확인할 수 있다.

또한 perimeterOf() 메소드와 areaOf() 메소들르 추가해 Square 객체를 생성하지 않고도 둘레와 넒이를 간단하게 구할 수 있게 해주는 메소드이다.

위의 코드를 보고 나면 외부에 변수와 함수를 선언해도 되겠다라는 생각을 할 수 있는데, 이렇게 변수와 함수를 클래스 내부에 작성하면 다음과 같은 장점이 있다.

  • 어떤 속성과 함수가 클래스 내부에 귀속되어 있다는 것을 명시적으로 나타낼 수 있다.
  • private 특성과 게터, 세터를 부여해서 조금 더 안전한 변수와 함수로 사용할 수 있다.

오버라이드

부모가 갖고 있는 함수를 자식에서 다시 선언해서 덮어쓰는 것오버라이드(override)라고 부른다. 프레임워크를 다룰 때 반드시 활용하는 개념이다. 물론 오버라이드라는 개념을 몰라도 코드를 작성할 수는 있지만, 알고나면 "내부적으로 어떤 코드가 있길래 이렇게 동작하는지"를 알 수 있다.

다음 코드는 LifeCycle이라는 간단한 클래스를 선언하고 사용하는 예이다. LifeCycle 클래스에는 a(), b(), c()라는 이름의 메소드가 있고, call()이라는 이름의 메소드에서 이를 호출하고 있다.

메소드에서 순서대로 메소드 호출하기

// 클래스 선언
class LifeCycle {
  call() {
    this.a();
    this.b();
    this.c();
  }

  a() {
    console.log("a() 메소드 호출");
  }
  b() {
    console.log("b() 메소드 호출");
  }
  c() {
    console.log("c() 메소드 호출");
  }
}

// 인스턴스 생성
new LifeCycle().call();

실행하면

a() 메소드 호출
b() 메소드 호출
c() 메소드 호출

이어서 이번 코드에서는 LifeCycle 클래스에 상속받는 Child라는 이름의 클래스를 선언하고, 내부에서 부모에 있던 a()라는 이름의 메소드를 만든다. 이를 '오버라이드했다'고 표현한다.

// 클래스 선언
class LifeCycle {
  call() {
    this.a();
    this.b();
    this.c();
  }

  a() {
    console.log("a() 메소드 호출");
  }
  b() {
    console.log("b() 메소드 호출");
  }
  c() {
    console.log("c() 메소드 호출");
  }
}

class Child extends LifeCycle {
  a() {
    console.log("자식의 a() 메소드");
  }
}

// 인스턴스 생성
new Child().call();

실행하면

자식의 a() 메소드
b() 메소드 호출
c() 메소드 호출

코드를 실행하면 원래 a() 메소드에 있던 출력이 바뀌는 것을 볼 수 있다. call() 메소드에서 a() 메소드를 실행하는데, a() 메소드가 덮어 쓰여졌으니 새로운 a() 메소드의 내용을 출력하는 것이 전부이다.

만약 부모에 있던 메소드의 내용도 사용하고 싶다면 다음과 같이 super.메소드() 형태의 코드를 사용한다. super.a()는 부모의 a() 모소드를 실행하는코드이다.

// 클래스 선언
class LifeCycle {
  call() {
    this.a();
    this.b();
    this.c();
  }

  a() {
    console.log("a() 메소드 호출");
  }
  b() {
    console.log("b() 메소드 호출");
  }
  c() {
    console.log("c() 메소드 호출");
  }
}

class Child extends LifeCycle {
  a() {
    super.a();
    console.log("자식의 a() 메소드");
  }
}

// 인스턴스 생성
new Child().call();

실행하면

a() 메소드 호출
자식의 a() 메소드
b() 메소드 호출
c() 메소드 호출

오버라이드는 정말 많은 곳에 활용된다. 오버라이드의 예를 살펴보면

오버라이드 예

지금까지 어떤 객체를 문자열로 만드는 메소드는 toString() 메소드라는 이름으로 만들었다.

자바스크립트의 모든 객체는 toString()이라는 메소드를 갖는다. 숫자, 문자열, 불린, 배열, 함수, 클래스, 클래스의 인스턴스 모두 toString()이라는 메소드가 있다. 이는 자바스크립트가 Object라는 최상위 클래스를 가지며, 어떤 클래스를 만들어도 자동으로 Object 클래스를 상속받게 되어서 발생하는 현상이다. 따라서 toString()이라는 이름으로 메소드를 만들면 Object 클래스에 있던 toString() 메소드를 오버라이드하는 것이 된다.

자바스크립트는 내부적으로 어떤 객체를 문자열로 만들 때 toString() 메소드를 호출한다. 따라서 toString() 메소드를 오버라이드하면 내부적으로 문자열로 변환되는 형태를 바꿀 수 있다.

class Pet {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  toString() {
    return `이름 ${this.name}\n나이: ${this.age}`;
  }
}

const pet = new Pet("구름", 6);
console.log(pet + "");

실행하면

이름 구름
나이: 6

toString()메소드를 오버라이드 했으므로 바꾼형태로 출력되는 것을 볼 수 있다.

또한 문자열과 다른 자료형을 결합할 때도 내부적으로 다른 자료형을 문자열로 변환한 뒤 결합한다. 따라서 문자열 결합 연산자를 호출할 때도 오버라이드한 toString() 메소드의 리턴값이 나오는 것을확인할 수 있다.

5가지 키워드로 정리하는 핵심 포인트

  • 상속은 어떤 클래스가 갖고 있는 유산(속성과 메소드)을 기반으로 새로운 클래스를 만드는 것이다.
  • private 속성/메소드는 클래스 내부에서만 접근할 수 있는 속성/메소드이다.
  • 게터는 getOO() 형태로 값을 확인하는 기능을 가진 메소드를 의미한다.
  • 세터는 setOO() 형태로 값을 지정하는 기능을 가진 메소드를 의미한다.
  • 오버라이드는 부모가 갖고 있는 메소드와 같은 이름으로 메소드를 선언해서 덮어 쓰는 것을 의미한다.

좋은 웹페이지 즐겨찾기