자바스크립트 객체지향 프로그래밍
Javascript에서 객체 지향 프로그래밍 실현하기
객체 지향 프로그래밍의 개념을 이해하며 읽었던 여러 개의 글에서 대부분 ‘자바’의 예시를 사용하고 있었다. 자바스크립트의 클래스 문법을 따로 공부하다보니 자바스크립트가 아닌 자바를 활용한 이유를 조금은 알 것 같다 🥲 타입스크립트를 사용할까 생각도 해봤는데, 역시 나는 어렵게 구현할 때 더 많은 배움이 있을 것 같아 자바스크립트 위주로 살펴보겠다. 이후에는 타입스크립트와의 비교까지 반드시 정리해보자.
클래스의 기본적인 문법을 알고가자
클래스가 무엇일까?
클래스가 무엇일까? 객체와 클래스 그리고 객체지향형 프로그래밍 등, 다양한 용어들이 등장하여 어질어질하다. 위키백과에서는 클래스를 다음과 같이 정의한다. “클래스는 객체 지향 프로그래밍에서 특정 객체를 생성하기 위해 변수와 메소드를 정의하는 일종의 틀로, 객체를 정의하기 위한 상태(멤버 변수)와 메서드(함수)로 구성된다.”
내가 이해한 클래스는 한 마디로 객체 지향 프로그래밍 패러다임을 프로그래밍 언어로 구현하는 단계에서 편리하게 정의하기 위한 문법이다. 꼭 클래스 문법을 사용하지 않고 new 연산자, 생성자 함수를 통해서도 사용할 수 있기 때문에 일종의 하나의 선택지처럼 해석했다. 클래스는 ECMAScript 2015(ES6)에 등장한 문법이니, 이전에는 new 연산자, 생성자 함수가 유일한 선택지였을 것 같다.
그런데 나의 해석은 일부는 틀리고 일부는 맞다고 할 수 있다. 단순히 기존 기능과 동일하지만 편리함을 위해 존재하는 문법을 ‘문법 설탕 (syntactic sugar)’이라고 하는데 자바크스립트 클래스는 문법 설탕이 아니다! 이를 알아보기 위해서 자바스크립트 클래스의 동작 원리를 깊게 파헤쳐보자.
자바스크립트 클래스는 문법 설탕이 아니라고!🍯
자바스크립트의 클래스는 함수의 한 종류다. 여기서부터 위기다. 아래의 간단한 User class의 예시를 살펴보자.
1
2
3
4
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
콘솔로 타입을 찍어보면 함수라고 나올 것이다.
1
console.log(typeof User); // function
클래스로 User를 정의하면 자바스크립트는 아래와 같은 일들을 한다.
- 해당 클래스 이름을 가진 함수를 만든다.
- 함수 본문은 생성자 메서드로 구성한다.
- 생성자 메서드가 없으면 빈 상태다.
클래스명.prototype에 클래스 내에 정의한 메서드를 저장한다.
다음과 같이 new User를 호출하여 객체를 만든다면, jihoo.prototype에 메서드가 저장되어있기 때문에 jihoo라는 객체에서 User클래스에서 정의된 메서드를 쓸 수 있는 것이다.
1
2
const jihoo = new User("jihoo");
jihoo.sayHi();
위의 1번 과정이 믿기지 않는다면 다음과 같이 테스트해보자.
1
console.log(User === User.prototype.constructor) // true
위와 같은 클래스는 크게 2가지 측면에서 문법 설탕이 아닌 고유한 특성을 가진다.
- class 키워드로 만든 함수는 특수 내부 프로퍼터인
[[IsClassConstructor]]:true를 꼬리표처럼 달고 다닌다. 이를 통해 클래스 생성자를 new와 호출하지 않았을 때 에러를 발생시키고, 문자열로 형반환할 때도 class를 붙인 문자열로 만든다. - class에 정의된 메서드는 열거할 수 없다. for … in 문을 통해 객체를 순회할 때 이러한 특성 때문에 메서드는 제외되고 프로퍼티만 순회할 수 있다.
- class에는 자동으로 엄격 모드로 실행된다. 적용 범위는 클래스 생성자 안 코드 전체다.
클래스에 필드에 대해
- 클래스 필드의 중요한 특징은
클래스명.prototype이 아닌 개별 객체에서만 설정된다는 점이다.⚠️ 클래스 필드에서 화살표 함수로 메서드를 정의할 때 참고사항
우리는 앞에서
클래스명.prototype에 메서드를 저장하는 것을 살펴보았다. 그러나 클래스 필드로 화살표 함수를 통해 메서드를 정의하면클래스명.prototype메서드에 저장되지 않는다. 아래의 예시를 통해 직접 살펴보자.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
class Button { constructor(value) { this.value = value; this.click = this.click1.bind(this); } // 1️⃣ 화살표 함수 이용한 클래스 필드 click1 = () => { console.log(this.value); }; // 2️⃣ 일반적인 메서드 방식 click2() { console.log(this.value + "🦆"); } } let button = new Button("난 부덕이야."); console.log(Button.prototype.click1); // undefined console.log(Button.prototype.click2); // [Function: click2] // 개별 객체에서는 확인할 수 있다. console.log(button.click1); // [Function: click1]
- 클래스 필드에 복잡한 표현식이나 함수의 호출 결과도 사용할 수 있다.
- 클래스 필드를 통해 바인딩 된 메서드를 우아하게 만들 수 있다.
🔎 바인딩 문제와 클래스 필드를 포함한 3가지 해결법
### 바인딩 문제상황
this는 동적으로 결정된다. 아래와 같은 상황에서 this의 컨텍스트를 알 수 없게 된다. 따라서 button.click을 통해 value를 출력해도
undefined가 나올 뿐이다.1 2 3 4 5 6 7 8 9 10 11 12 13
class Button { constructor(value) { this.value = value; } click() { console.log(this.value); } } let button = new Button("난 부덕이야."); setTimeout(button.click, 1000);
### 👉 해결책1: 래퍼함수를 전달한다.
아마도 제일 익숙한 방법일 것이다. 이제 “난 부덕이야”가 잘 출력된다.
1
setTimeout(()=> button.click(), 1000);
### 👉 해결책2: 생성자 안에서 메서드를 객체에 바인딩한다.
이 방법을 통해서도 “난 부덕이야”가 잘 출력된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class Button { constructor(value) { this.value = value; this.click = this.click.bind(this); // 바인딩하는 부분 } click() { console.log(this.value); } } let button = new Button("안녕하세요."); setTimeout(button.click, 1000);
### 👉 해결책3: 클래스 필드를 활용한다.
가능한 이유는 클래스 필드는 생성하는 각 Button 객체에 독립적인 함수를 만들고 → 이 함수의 this를 객체에 바인딩시켜주기 때문이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Button { // 클래스 필드로 정의 click = () => { console.log(this.value); }; constructor(value) { this.value = value; this.click = this.click.bind(this); } } let button = new Button("난 부덕이야."); setTimeout(button.click, 1000);
상속을 실현해보자
간단하다. extends 키워드를 사용한다. 문법은 쉬운데 동작 원리가 어려우니 일단 문법부터 살펴보자.
문법
아래와 같은 Animal 클래스를 만들었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
}
stop() {
this.speed = 0;
alert(`${this.name} 이/가 멈췄습니다.`);
}
}
이를 Rabbit에서 상속하고 싶다면 class Rabbit extends Animal처럼 extends 키워드를 끼워준다.
1
2
3
4
5
6
7
class Rabbit extends Animal {
hide() {
alert(`${this.name} 이/가 숨었습니다!`);
}
}
let rabbit = new Rabbit("흰 토끼");
동작 원리
위의 예시에서 Rabbit 클래스가 Animal 클래스를 상속받으면 어떤 일이 벌어질까? 상속은 프로토타입 기반으로 이루어진다.
- extends 키워드를 사용하면 Rabbit.prototype.[[Prototype]]을 Animal.prototype으로 설정한다.
- 메서드 탐색 시 Rabbit 객체에서 찾고 → Rabbit.prototype에서 찾고 → 없으면 Animal.prototype에서 찾는다.
오버라이딩
우리는 객체지향프로그래밍의 상속 특징에서 “상위클래스의 기능 및 속성을 그대로 받아서 활용해도 되지만, 선택적으로 재정의할 수 있음”을 살펴보았다. 지식 클래스에서 같은 이름으로 사용하지만 담당하는 내용을 바꾸는 것이다. 이때 오버라이딩을 사용한다.
메서드 오버라이딩
만약 이미 부모 클래스 Animal에 있는 stop메서드를 자식 클래스에도 정의하면, 자식 클래스에 있는 메서드가 사용된다.
1
2
3
4
5
class Rabbit extends Animal {
stop() {
// 여기에 정의한 메서드를 사용
}
}
이때 부모 클래스의 메서드를 그대로 사용하고 일부만 재정의하고 싶다면 super키워드를 사용할 수 있다. Rabbit에서 stop 메서드에 접근하면 아래에서 정의한 stop을 사용하지만, 내부의 super.stop은 부모 클래스 Animal의 stop을 사용한다.
1
2
3
4
5
6
class Rabbit extends Animal {
stop() {
super.stop()
// 재정의한 새로운 내용 추가 ...
}
}
생성자 오버라이딩
https://ko.javascript.info/class-inheritance 여기서부터 살짝 멘붕
클래스필드 오버라이딩
오버로딩 VS 오버라이딩
다형성을 실현해보자
GPT에게 질문했다. 자바스크립트에는 자바와 같이 interface가 없습니다. 자바스크립트에서 객체 지향 프로그래밍을 실천할 때 다형성은 어떻게 구현할 수 있을까요?
ChatGPT
자바스크립트에는 Java와 같은
interface가 없지만, 다형성(polymorphism)을 구현할 수 있는 여러 가지 방법이 있습니다. 다형성은 주로 다른 형태의 객체들이 동일한 인터페이스를 통해 동작할 수 있도록 하는 객체 지향 프로그래밍의 원칙입니다. 이를 자바스크립트에서 구현하는 몇 가지 방법을 살펴보겠습니다.### 1. 덕 타이핑(Duck Typing)
자바스크립트는 동적 타이핑 언어이므로, 객체의 메서드와 속성이 존재하는지 여부에 따라 동작을 결정하는 덕 타이핑을 사용할 수 있습니다. 즉, 특정 메서드를 가진 객체라면 그 타입을 갖는 것으로 간주합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
javascript코드 복사 class Duck { quack() { console.log("Quack!"); } } class Person { quack() { console.log("I'm pretending to be a duck!"); } } function makeItQuack(quacker) { quacker.quack(); } const duck = new Duck(); const person = new Person(); makeItQuack(duck); // Quack! makeItQuack(person); // I'm pretending to be a duck!
위 예제에서
Duck과Person클래스는 모두quack메서드를 가지고 있으며,makeItQuack함수는 이 메서드를 호출합니다. 두 객체가 동일한 메서드를 가지고 있기 때문에 동일한 인터페이스를 따른다고 간주됩니다.### 2. 클래스 계층 구조와 상속
자바스크립트의 클래스 상속을 이용하여 다형성을 구현할 수 있습니다. 이는 자바의 상속과 유사합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
javascript코드 복사 class Animal { speak() { console.log("Animal speaks"); } } class Dog extends Animal { speak() { console.log("Woof!"); } } class Cat extends Animal { speak() { console.log("Meow!"); } } function letAnimalSpeak(animal) { animal.speak(); } const dog = new Dog(); const cat = new Cat(); letAnimalSpeak(dog); // Woof! letAnimalSpeak(cat); // Meow!
위 예제에서
Animal클래스는 기본speak메서드를 가지고 있으며,Dog와Cat클래스는 이 메서드를 각각의 방식으로 재정의(오버라이딩)합니다.letAnimalSpeak함수는 어떤Animal객체든 받아서 해당 객체의speak메서드를 호출합니다.### 3. 인터페이스와 비슷한 객체 형태 강제
타입스크립트를 사용하면 자바스크립트에 인터페이스와 비슷한 형태를 강제할 수 있습니다. 타입스크립트는 자바스크립트의 상위 집합으로, 정적 타입 검사와 함께 인터페이스를 제공합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
typescript코드 복사 interface Quacker { quack(): void; } class Duck implements Quacker { quack() { console.log("Quack!"); } } class Person implements Quacker { quack() { console.log("I'm pretending to be a duck!"); } } function makeItQuack(quacker: Quacker) { quacker.quack(); } const duck = new Duck(); const person = new Person(); makeItQuack(duck); // Quack! makeItQuack(person); // I'm pretending to be a duck!
타입스크립트에서는
Quacker인터페이스를 정의하고,Duck과Person클래스가 이를 구현하도록 합니다.makeItQuack함수는Quacker인터페이스를 구현한 객체만을 받을 수 있습니다.### 4. 믹스인(Mixin) 패턴
믹스인 패턴을 사용하여 여러 객체에 동일한 메서드를 공유할 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
javascript코드 복사 let quackerMixin = { quack() { console.log(this.sound); } }; class Duck { constructor() { this.sound = "Quack!"; } } class Person { constructor() { this.sound = "I'm pretending to be a duck!"; } } Object.assign(Duck.prototype, quackerMixin); Object.assign(Person.prototype, quackerMixin); const duck = new Duck(); const person = new Person(); duck.quack(); // Quack! person.quack(); // I'm pretending to be a duck!
여기서
quackerMixin객체는quack메서드를 정의하고, 이 메서드를Duck과Person클래스의 프로토타입에 추가합니다. 이를 통해 두 클래스 모두 동일한 메서드를 가지게 되어 다형성을 구현할 수 있습니다.### 결론
자바스크립트에서 다형성을 구현하는 방법은 다양합니다. 덕 타이핑, 상속, 믹스인 패턴 등을 통해 자바스크립트의 유연한 객체 지향 프로그래밍을 실현할 수 있습니다. 이 방법들은 코드의 재사용성을 높이고 유지보수를 용이하게 하며, 더 나은 구조를 갖춘 프로그램을 작성하는 데 도움을 줍니다.
https://webclub.tistory.com/406 ← 자바스크립트 다형성 예제 코드 1
https://wikidocs.net/224805 ← 자바스크립트 다형성 예제 코드 2
https://dkrnfls.tistory.com/301 ← 자바스크립트 다중상속과 믹스인
https://velog.io/@wooyong99/객체지향-모델링-설계-회고 ← 인터페이스 이해 참고
<인터페이스 참고="" 클래스="" 다이어그램=""> 개선전  개선 후  ## 캡슐화를 실현해보자 위에서 언급했듯이 자바스크립트에서는 **public / protected / private의 3가지 접근제어자**를 사용할 수 있다. public은 별도의 설정이 필요 없으니 protected와 private을 알아보자. **`protected`** - protected 프로퍼티명 앞에는 _을 붙인다. 작동을 강제하는 것이 아니라 비슷하게 구현하기 위한 약속이다. - getter와 setter함수를 정의한다. 읽기 전용 등으로 권한을 제한하고 싶다면 setter를 없애면 된다. ```jsx class CoffeeMachine { _waterAmount = 0; setWaterAmount(value) { if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다."); this._waterAmount = value; } getWaterAmount() { return this._waterAmount; } } new CoffeeMachine().setWaterAmount(100); ``` **`private`** - protected와 유사하게 작동을 강제하는 문법은 없었으나, 최근 등재된 문법이 있다. > Starting from ECMAScript 2022, **private class fields are defined using a hash # prefix**, From the above example, we can modify it to include a private class field to prevent the property from being changed outside of the class method. > - 프로퍼티와 메서드 앞에 **#을 붙여** 클래스 내부에서만 접근 가능하도록 강제할 수 있다. 인터페이스>
