인강 & 책 스터디 노트/Udemy JavaScript Pro: Mastering Advanced

자바스크립트 객체지향을 이해하기 위한 여정: 클래스, 생성자, 상속, static을 중심으로

thisisamrd 2024. 9. 30.

제가 좋아라하는 Udemy 강사인 Colt Steele의 자바스크립트 심화강의인 JavaScript Pro: Mastering Advanced Concepts and Technique에서는 평소 제가 궁금해했던 자바스크립트의 난이도 있는 개념들, 그 중에서도 객체지향 개념을 잘 다뤄주고 있습니다. 오늘 포스팅은 해당 강의의 내용에 대한 복습입니다. 

 

 

 

 

 

 

 

 

 

자바스크립트에서 객체지향 프로그래밍이란?

 

 

 

 

자바스크립트에서 객체지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그램을 여러 객체로 나누어 관리하는 프로그래밍 방식입니다. 이 객체들은 데이터(속성)와 해당 데이터를 처리하는 메서드를 함께 포함하여, 서로 상호작용하면서 기능을 수행하게 됩니다.

 

자바스크립트는 본래 객체지향 언어로 설계된 것은 아니지만, ES6부터 클래스(class) 문법이 도입되면서 객체지향 프로그래밍의 특징을 더 잘 반영하게 되었습니다. 객체지향 프로그래밍을 사용하면 프로그램을 더욱 모듈화하고, 유지보수와 확장이 쉬워지며, 코드 재사용성이 높아집니다. 특히, 상속과 같은 개념을 통해 중복 코드를 줄이고, 객체 간의 구조를 일관되게 유지할 수 있습니다.

 

 

 

 

클래스와 생성자(Constructor)

 

자바스크립트에서 클래스는 객체를 생성하기 위한 청사진 역할을 합니다. 클래스 내부에는 생성자 함수(Constructor)가 포함되는데, 이 생성자는 새로운 객체를 만들 때 자동으로 호출되어 객체의 초기 값을 설정하는 중요한 역할을 합니다. 생성자는 반드시 constructor라는 이름을 가져야 하며, 이를 통해 클래스의 속성들을 초기화하거나 입력된 데이터를 검증할 수 있습니다.

 

 

생성자는 새로운 객체를 만들 때 초기 값을 설정해주는 중요한 역할을 합니다. 자바스크립트에서 클래스와 생성자를 활용하면 코드의 가독성과 유지보수성을 높일 수 있으며, 데이터 검증을 통해 더 안전한 객체 생성이 가능합니다.

 

 

 

 

생성자의 기본 동작

 

클래스에서 생성자는 객체가 생성될 때 호출됩니다. 예를 들어, Triangle 클래스에서는 두 변 a와 b를 전달받아, 각각의 값을 this.a와 this.b에 할당할 수 있습니다. 이를 통해 새로운 삼각형 객체를 생성할 때, 바로 해당 값을 설정할 수 있습니다.

 

class Triangle {
    constructor(a, b) {
        this.a = a;
        this.b = b;
    }

    getArea() {
        return (this.a * this.b) / 2;
    }

    getHypotenuse() {
        return Math.sqrt(this.a ** 2 + this.b ** 2);
    }
}

const triangle1 = new Triangle(3, 4);
console.log(triangle1.getArea());        // 6
console.log(triangle1.getHypotenuse());  // 5

 

 

위의 코드에서 Triangle 클래스는 두 개의 속성 a와 b를 가지며, getArea 메서드를 사용해 삼각형의 넓이를 계산하고, getHypotenuse 메서드를 통해 빗변의 길이를 구할 수 있습니다. 이때, 새로운 Triangle 객체를 만들 때, new Triangle(3, 4)를 통해 생성자가 호출되며 this.a는 3, this.b는 4로 설정됩니다.

 

 

 

This 키워드

 

this는 해당 클래스의 특정 인스턴스를 참조하는데 사용됩니다. 예를 들어 this.a와 this.b는 각각 Triangle의 데이터를 참조하게 됩니다. 중요한 점은 메서드 안에서 다른 메서드를 호출할 때에도 this를 사용해야 한다는 것입니다. 예를 들어, 메서드 getArea를 다른 메서드 sayHi에서 호출할 때, this.getArea()처럼 호출해야 합니다. 그렇지 않으면 자바스크립트는 해당 메서드를 찾을 수 없습니다.

 

class Triangle {
    constructor(a, b) {
        this.a = a;
        this.b = b;
    }

    getArea() {
        return (this.a * this.b) / 2;
    }

    sayHi() {
        console.log(`The triangle with side A of ${this.a}, side B of ${this.b}, and an area of ${this.getArea()} says hi.`);
    }
}

 

 

 

데이터 검증과 예외 처리

 

생성자에서는 데이터를 검증할 수도 있습니다. 만약 삼각형의 한 변이 음수이거나 숫자가 아닌 값이 전달되면, 오류를 발생시키는 방식으로 유효하지 않은 데이터를 방지할 수 있습니다.

 

class Triangle {
    constructor(a, b) {
        if (a <= 0 || b <= 0) {
            throw new Error("길이는 양수여야 합니다.");
        }
        this.a = a;
        this.b = b;
    }

    getArea() {
        return (this.a * this.b) / 2;
    }

    getHypotenuse() {
        return Math.sqrt(this.a ** 2 + this.b ** 2);
    }
}

try {
    const triangle2 = new Triangle(-3, 4);  // 오류 발생
} catch (e) {
    console.error(e.message);  // "길이는 양수여야 합니다."
}

 

 

 

 

 

 

 

 

상속과 오버라이딩

 

자바스크립트에서 상속(inheritance)은 객체지향 프로그래밍의 중요한 개념으로, 클래스 간에 기능을 공유하고 재사용할 수 있게 해줍니다. 한 클래스가 다른 클래스의 속성과 메서드를 물려받을 수 있기 때문에, 중복 코드를 줄이고 유지보수성을 높일 수 있습니다. 상속을 통해 부모 클래스의 기능을 자식 클래스가 그대로 사용할 수 있으며, 필요한 부분만 수정하거나 추가할 수 있습니다.

 

 

 

 

상속의 기본 개념

 

상속을 사용하면 부모 클래스의 기능을 자식 클래스에서 그대로 사용할 수 있습니다. 예를 들어, 아래 예시에서 삼각형의 속성과 메서드를 정의한 Triangle 클래스를 부모 클래스로 하고, 이를 상속받는 ShyTriangle 클래스를 만들 수 있습니다. 이렇게 하면 ShyTriangle 클래스는 Triangle 클래스의 메서드와 속성을 모두 사용할 수 있으며, 특정 메서드를 재정의해 다른 동작을 수행하도록 만들 수도 있습니다.

 

class Triangle {
    constructor(a, b) {
        this.a = a;
        this.b = b;
    }

    getArea() {
        return (this.a * this.b) / 2;
    }

    getHypotenuse() {
        return Math.sqrt(this.a ** 2 + this.b ** 2);
    }

    describe() {
        return `I am a triangle with an area of ${this.getArea()}.`;
    }
}

class ShyTriangle extends Triangle {
    describe() {
        return 'Runs and hides.';
    }

    beShy() {
        return 'I am shy.';
    }
}

 

위의 예시에서, ShyTriangle 클래스는 Triangle 클래스를 상속받아 getArea와 getHypotenuse 메서드를 그대로 사용할 수 있습니다. 하지만 describe 메서드는 재정의(오버라이딩)하여 다른 동작을 수행하도록 만들었죠.

 

 

 

오버라이딩(Overriding)

 

오버라이딩자식 클래스에서 부모 클래스의 메서드를 재정의하는 것을 말합니다. 자식 클래스가 부모 클래스와 동일한 이름의 메서드를 정의하면, 자식 클래스의 메서드가 호출됩니다. 이를 통해 자식 클래스는 부모 클래스의 메서드를 확장하거나 일부 동작을 변경할 수 있습니다.

 

const regularTri = new Triangle(3, 4);
console.log(regularTri.describe());  // "I am a triangle with an area of 6."

const shyTri = new ShyTriangle(3, 4);
console.log(shyTri.describe());  // "Runs and hides."
console.log(shyTri.getHypotenuse());  // 5
console.log(shyTri.beShy());  // "I am shy."

 

위 코드에서 regularTri는 Triangle 클래스를 기반으로 한 객체로, describe 메서드를 호출하면 부모 클래스에서 정의한 기본 설명이 출력됩니다. 반면, shyTri는 ShyTriangle 클래스의 인스턴스로, 오버라이딩된 describe 메서드를 호출하여 다르게 동작하는 것을 확인할 수 있습니다.

 

 

 

 

 

 

 

 

super 키워드

 

super 키워드는 자바스크립트에서 상속을 사용할 때, 자식 클래스부모 클래스의 생성자나 메서드를 호출하는 데 사용됩니다. 상속 관계에서 자식 클래스는 부모 클래스의 기능을 물려받아 사용하면서, 필요한 부분을 재정의하거나 확장할 수 있습니다. 이 과정에서 super를 통해 부모 클래스의 생성자나 메서드에 접근할 수 있습니다.

 

 

 

 

부모 클래스의 생성자 호출

자식 클래스에서 생성자를 정의할 때, 부모 클래스의 생성자를 먼저 호출해야 합니다. 이를 위해 super()를 사용합니다. 만약 super()를 호출하지 않고 자식 클래스에서 this를 사용하려고 하면, 자바스크립트는 오류를 발생시키며 "자식 클래스에서 'this'를 사용하기 전에 반드시 부모 클래스의 생성자를 호출해야 한다"는 경고 메시지를 보여줍니다.

 

class Triangle {
    constructor(a, b) {
        this.a = a;
        this.b = b;
    }

    getArea() {
        return (this.a * this.b) / 2;
    }
}

class ColorTriangle extends Triangle {
    constructor(a, b, color) {
        super(a, b);  // 부모 클래스의 생성자 호출
        this.color = color;  // 새로운 속성 추가
    }

    describe() {
        return `This triangle has an area of ${this.getArea()} and is ${this.color}.`;
    }
}

const colorTri = new ColorTriangle(3, 4, 'blue');
console.log(colorTri.describe());  // "This triangle has an area of 6 and is blue."

 

 

위 예시에서, ColorTriangle 클래스는 Triangle 클래스를 상속받고 있습니다. 자식 클래스 ColorTriangle의 생성자에서 super(a, b)를 사용하여 부모 클래스의 생성자를 먼저 호출한 후, 새로운 color 속성을 추가합니다. 이렇게 하면 부모 클래스의 속성인 a와 b도 자식 클래스에서 제대로 초기화되고, 추가적인 속성인 color도 설정할 수 있습니다.

 

 

 

부모 클래스의 메서드 호출

 

super는 생성자 외에도 부모 클래스의 메서드를 호출하는 데 사용될 수 있습니다. 자식 클래스가 부모 클래스의 메서드를 오버라이딩하면서도, 부모 클래스의 원래 메서드를 사용하고 싶을 때 super.methodName()을 호출할 수 있습니다. 이를 통해 부모 클래스의 기능을 확장하는 방식으로 사용할 수 있습니다.

 

 

class Animal {
    speak() {
        return "Animal speaks.";
    }
}

class Dog extends Animal {
    speak() {
        return `${super.speak()} Woof!`;  // 부모 클래스의 speak 메서드 호출
    }
}

const dog = new Dog();
console.log(dog.speak());  // "Animal speaks. Woof!"

 

 

위 예시에서, Dog 클래스는 Animal 클래스를 상속받습니다. Dog 클래스의 speak 메서드에서 super.speak()를 호출하여 부모 클래스의 speak 메서드를 먼저 실행한 후, 추가로 "Woof!"라는 문자열을 더해서 반환합니다. 이렇게 하면 자식 클래스는 부모 클래스의 기능을 기반으로 확장된 동작을 수행할 수 있습니다.

 

 

 

super를 사용한 상속의 효율성

 

super를 사용하면 코드 중복을 줄이고, 부모 클래스에서 정의한 기능을 재사용할 수 있습니다. 이를 통해 자식 클래스는 부모 클래스의 기능을 간단하게 상속받으면서, 필요할 경우 원하는 대로 확장하거나 수정할 수 있습니다. 예를 들어, 여러 클래스가 공통적으로 사용할 메서드는 부모 클래스에 정의하고, 자식 클래스에서는 그 메서드를 호출하거나 재정의하는 방식으로 구현할 수 있습니다.

 

 

 

 

 

 

 

 

static 속성과 메서드

 

자바스크립트에서 정적(static) 속성과 메서드는 객체의 인스턴스가 아닌 클래스 자체에 속하는 속성과 메서드를 의미합니다. 이는 특정 인스턴스에 종속되지 않고, 클래스 전체에서 공통적으로 사용되는 기능을 정의할 때 유용합니다. 정적 속성은 각 객체에 따로 저장되지 않고, 클래스 이름을 통해 직접 접근할 수 있으며, 정적 메서드도 마찬가지로 클래스 이름을 통해 호출됩니다.

 

 

 

 

정적 속성(static properties)

 

정적 속성은 static 키워드를 사용해 클래스에 직접 연결됩니다. 이 속성은 각 인스턴스마다 존재하는 속성이 아니라, 클래스 자체에 존재하며 공통된 정보를 저장할 때 유용합니다.

 

class Cat {
    constructor(name, breed) {
        this.name = name;
        this.breed = breed;
    }

    // 정적 속성 정의
    static species = "Felis catus";
}

const blue = new Cat("Blue", "Scottish Fold");
console.log(blue.name);  // "Blue"
console.log(blue.breed);  // "Scottish Fold"

// 정적 속성은 클래스 이름으로 접근
console.log(Cat.species);  // "Felis catus"

 

 

이 예시에서, species는 모든 고양이가 공유하는 속성으로 Cat 클래스 자체에 정의되었습니다. 각각의 고양이 인스턴스는 이름과 품종을 개별적으로 가지지만, species는 모든 고양이가 동일하게 공유하는 정적 속성입니다. 따라서 blue.species로는 접근할 수 없고, Cat.species로 접근해야 합니다.

 

 

 

 

정적 메서드(static methods)

 

정적 메서드는 정적 속성과 유사하게, 특정 인스턴스가 아닌 클래스 자체에서 호출되는 메서드입니다. 정적 메서드는 주로 인스턴스와 관계없는 작업을 수행하거나, 관련된 기능들을 그룹화할 때 사용됩니다. 예를 들어, 여러 수학적 연산을 처리하는 메서드를 그룹화하거나, 객체 생성을 도와주는 팩토리 메서드를 정의할 때 유용합니다.

 

class MyMath {
    // 정적 메서드: 두 숫자의 합을 계산
    static add(a, b) {
        return a + b;
    }

    // 정적 메서드: 두 숫자의 곱을 계산
    static multiply(a, b) {
        return a * b;
    }
}

// 인스턴스를 만들 필요 없이 클래스 이름으로 메서드 호출
console.log(MyMath.add(2, 3));        // 5
console.log(MyMath.multiply(4, 5));   // 20

 

MyMath 클래스의 add와 multiply 메서드는 특정 인스턴스에 종속되지 않고, 클래스를 통해 직접 호출되는 정적 메서드입니다. 이러한 방식은 공통적인 계산이나 데이터 처리 로직을 클래스에 묶어놓고, 언제든지 호출할 수 있도록 할 때 유용합니다.

 

 

 

 

팩토리 메서드로서의 활용

정적 메서드는 팩토리 메서드로도 자주 사용됩니다. 팩토리 메서드는 새로운 객체 인스턴스를 생성할 때 사용되며, 객체 생성에 필요한 로직을 깔끔하게 관리할 수 있습니다.

 

 

class Cat {
    constructor(name, breed) {
        this.name = name;
        this.breed = breed;
    }

    // 정적 메서드로서의 팩토리 함수
    static registerStray() {
        const randomNames = ["Mittens", "Fluffy", "Whiskers"];
        const randomName = randomNames[Math.floor(Math.random() * randomNames.length)];
        return new Cat(randomName, "Unknown");
    }
}

const strayCat = Cat.registerStray();  // 새 고양이 생성
console.log(strayCat);  // Cat { name: 'Fluffy', breed: 'Unknown' }

 

 

위 코드에서 registerStray는 고양이 이름을 무작위로 선택하고, Unknown 품종으로 고양이 객체를 생성하는 정적 메서드입니다. 이 메서드는 고양이 객체를 생성하는 데 사용되지만, 특정 고양이 인스턴스와는 무관하므로 정적 메서드로 정의되었습니다.

 

 

 

정적 속성 및 메서드의 활용 장점

1. 정적 속성과 메서드는 각 객체 인스턴스에 중복으로 존재하지 않고, 클래스 자체에 한 번만 정의됩니다. 이를 통해 메모리 효율성을 높일 수 있습니다.

2. 공통된 정보나 로직을 한 곳에서 관리할 수 있기 때문에, 변경이 필요할 경우 클래스의 정적 속성 또는 메서드만 수정하면 모든 인스턴스에 변경 사항이 적용됩니다.

3. 특정 기능들을 하나의 클래스에 묶어 정리할 수 있어, 코드의 가독성과 재사용성을 높일 수 있습니다.

 

 

 

 

 

 

 

이상으로 제가 초반에 배울때 굉장히 까다롭게 생각했던 자바스크립트 객체 지향의 기본 개념에 대한 포스팅을 마치겠습니다. 예시만 몇번 반복해서 읽어도 생각보다 이해하기 쉬운 개념인 것 같아요.

댓글