[2주차] 클래스
제로초님 자바스크립트 강의복잡한 코드를 정리할 수 있는 클래스를 배워보도록 하겠다. 클래스는 객체를 생성하기 위한 템플릿(서식)이다.
1. 함수로 객체를 생성하는 방법
ES2015에서 클래스를 추가하기 전에는 객체를 동적으로 생성하려면 함수를 사용했다. 객체를 동적으로 생성한다는 것은 객체를 미리 만들어 두지 않고 필요할 때마다 생성한다는 뜻이다.
함수로 객체를 생성하는 방법은 다음과 같다. 객체를 반환하는 함수를 만들면 되는데, 이런 함수를 공장 함수라고 한다. 공장처럼 객체를 찍어낸다고 해서 붙은 이름이다. 새로운 객체가 필요하면 그때마다 함수를 호출하면 된다.
function creatMonster(name, hp, att) {
return { name, hp, att};
}
const monster1 = createMonster('슬라임', 25, 10);
const monster2 = createMonster('슬라임', 26, 9);
const monster3 = createMonster('슬라임', 25, 11);
예제 코드의 creatMonster() 함수는 나중에 텍스트 롤플레잉 게임(RPG)을 만들 때 사용할 몬스터 정보를 객체로 만드는 함수이다. createMonster() 함수를 세번 호출했으니 몬스터 객체 셋을 생성한 것과 같다. 슬라임 몬스터 객체는 각각 체럭(hp), 공격력(att)이 조금씩 다르다. 이렇게 달라지는 부분은 함수의 인수로 넘기면 된다.
공장 함수 대신 다음과 같이 생성할 수도 있다. 이 코드에서는 객체의 속성을 this에 대입했다. 그리고 함수를 호출할 때는 함수 이름 앞에 new를 붙인다. new를 붙여 호출할 때마다 새로운 객체가 생성된다. 이때 this는 새롭게 생성된 객체를 가리킨다. 즉, this.name = name;은 새롭게 생성된 객체의 속성을 수정하는 코드이다.
function Monster(name, hp, att) {
this.name = name;
this.hp = hp;
this.att = att;
}
cost monster1 = new Monster('슬라임', 25, 10);
cost monster2 = new Monster('슬라임', 26, 9);
cost monster3 = new Monster('슬라임', 25, 11);
이렇게 new를 붙여 호출하는 함수를 생성자 함수(constructor function)라고 한다. new를 붙이지 않고 호출하면 this.name = name;을 할 때 window.name의 값을 바꾸게 되니 반드시 new를 붙여 호출해야 한다. window.name의 값이 바뀌는 이유는 나중에 알아보자.
생성자 함수의 이름은 보통 대문자로 시작한다. 필수는 아니지만, 자바스크립트 개발자들이 많이 사용하는 규칙이다. 대문자로 시작하는 함수를 본다면 생성자 함수라고 생각해도 된다.
이번에는 객체 메서드를 추가해 보겠다. 한 몬스터가 다른 몬스터를 공격하는 메서드이다. 공장함수라면 다음과 같이 작성한다. 공격하면 공격받은 몬스터의 체력(monster.hp)이 공격한 몬스터의 공격력(this.att)만큼 줄어든다.
function creatMonster(name, hp, att) {
return {
name, hp, att,
attack(monster) {
monster.hp -= this.att;
},
};
}
const monster1 = createMonster('슬라임', 25, 10);
const monster2 = createMonster('슬라임', 26, 9);
monster1.attack === monster2.attack; // false
공장 함수에서 객체를 생성할 때마다 attack() 메서드도 같이 생성된다. attack() 메서드는 모든 객체에서 똑같은데도 계속 메서드를 새로 만드니 매우 비효율적이다. 이럴 때는 attack() 메서드를 creatMonster 함수 외부로 빼서 재사용하면 새로 생성되는 객체가 attack() 메서드를 공유한다.
function attack(monster) {
monster.hp -= this.att;
}
function createMonster(name, hp, att) {
return {
name, hp, att,
attack,
};
}
const monster1 = createMonster('슬라임', 25, 10);
const monster2 = createMonster('슬라임', 26, 9);
monster1.attack === monster2.attack; //true
monster1;
공장 함수로 몬스터 객체를 생성하면 attack() 메서드가 들어 있는 것을 확인할 수 있다.
생성자 함수는 다음과 같이 작성한다.
function Monster(name, hp, att) {
this.name = name;
this.hp = hp;
this.att = att;
}
Monster.prototype.attack = function*monster) {
moster.hp -= this.att;
};
const moster1 = new Monster('슬라임', 25, 10);
const moster2 = new Monster('슬라임', 26, 9);
monster1.attack === monster2.attack; // true
attack() 메서드에 프로토타입(prototype)이라는 새로운 속성이 보인다. prototype은 생성자 함수로 생성한 객체가 공유하는 속성이다. 생성자 함수에서 이처럼 prototype 속성 안에 메서드를 추가해야 메서드를 재사용할 수 있다. 마지막 줄을 보면 attack() 메서드가 공유되는 것을 확인할 수 있다.
콘솔에 monster1을 입력해 보면 생성자 함수로 생성한 몬스터 객체 내부에 [[Prototype]]이라는 속성이 있고, 그 안에 attack() 메서드가 들어 있는 것을 확인할 수 있다. 또한, 객체 앞에 Monster라고 붙어 있는데, 생성자 함수로 생성한 객체는 앞에 생성자 함수의 이름이 붙는다. 콘솔에서 객체 앞에 이름이 붙어 있으면 해당 생성자로 만들었다고 생각하면 된다.
공장 함수와 생성자 함수에서는 메서드를 선언하는 부분이 분리되어 있다. 공장 함수에서는 attack() 메서드가 createMonster() 함수 바깥에 있고, 생성자 함수에서는 Monster() 함수 바깥에 prototype 코드가 있다. 이렇게 속성과 메서드가 분리되어 있으면 관리하기가 까다롭다. 분리되어 있는 속성과 메서드는 클래스를 사용해 한데로 모을 수 있다.
2. this 이해하기
this는 상황에 따라 값이 달라진다. this는 기본으로 window 객체를 가리키니 그 외 경우에 어떤 값을 가지는지만 알아 두면 된다.
다음 코드에서는 this가 window 객체를 가리킨다.
function a() {
console.log(this);
};
a(); // window {}
그래서 new를 붙이지 않고 생성자 함수를 호출하면 this.name = name;에서 window.name의 값을 바꾸게 된다.
function Monster(name, hp, att) {
this.name = name;
this.hp = hp;
this.att = att;
}
const monster1 = Monster('슬라임', 25, 10); // window 객체의 name, hp, att 수정
그러면 this가 어떤 경우에 window 객체가 아닌지 알아보겠다.
1. 객체 메서드로 this를 사용하면 this는 해당 객체를 가리킨다.
const b = {
name: '제로초',
sayName() {
console.log(this === b);
}
};
b.sayName(); // true
단, 메서드에 구조분해 할당을 적용하면 this가 객체 자신을 가리키지 않으니 주의해야 한다. 반드시 객체.메서드() 형태로 사용해야만 this가 객체 자신이 된다.
const { sayName } = b;
sayName(); // false
**구조분해 할당(Destructuring Assignment)**은 배열이나 객체의 값을 분해하여 변수에 쉽게 할당하는 JavaScript의 문법입니다. 예를 들어, 객체의 속성을 변수로 간단히 추출하거나, 배열의 요소를 변수에 나눠 담을 수 있습니다.
객체에서의 구조분해 할당
const obj = { name: '철수', age: 25 };
const { name, age } = obj; // obj의 속성을 분해하여 변수 name, age에 할당
console.log(name); // '철수'
console.log(age); // 25
배열에서의 구조분해 할당
const arr = [10, 20, 30];
const [a, b, c] = arr; // 배열 요소를 분해하여 변수 a, b, c에 할당
console.log(a); // 10
console.log(b); // 20
console.log(c); // 30
2. 함수의 this는 bind() 메서드를 사용해 값을 바꿀 수 있다.
bind() 메서드는 this를 수정하는 역할을 한다. 다음 코드는 bind() 메서드로 this를 obj로 바꾼 뒤 한 번 더 호출해야 함수가 실행된다.
const obj = { name: 'zerocho' };
function a() {
console.log(this);
}
a.bind(obj)(); // { name: 'zerocho' }
하지만 화살표 함수는 bind()를 해도 this를 바꿀 수 없다. 다음 코드는 this가 바뀌지 않아서 window 객체가 그대로 나온다.
const b = () => {
console.log(this)
}
b.bind(obj)(); // window
따라서 this가 외부 요인 때문에 바뀌는 것을 원하지 않는다면 함수 선언문 대신 화살표 함수를 사용하면 된다. 여기서 화살표 함수의 this가 무조건 window 객체라고 오해할 수 있는데, 화살표 함수는 기존 this를 유지할 뿐 this를 어떤 값으로 바꾸지는 않는다.
const b = {
name: '제로초',
sayName() {
const whatIsThis = () => {
console.log(this);
};
whatIsThis();
}
};
b.sayName(); // b
sayName() 메서드를 호출하면 그 안에 whatIsThis() 함수도 같이 호출된다. 화살표 함수는 this를 window 객체로 만드는게 아니라 기존의 this를 유지하므로 b.sayName()의 this인 b가 그대로 유지된다.
이와 반대로 whatIsThis 함수를 함수 선언문으로 선언하면 this는 기본값인 window가 된다.
const b = {
name: '제로초',
sayName() {
const whatIsThis = function() {
console.log(this);
};
whatIsThis();
}
};
b.sayName(); // window
3. 생성자 함수를 호출할 때 new를 붙이면 this는 생성자 함수가 새로 생성하는 객체가 된다.
3. 클래스로 객체를 생성하는 방법
Monster 생성자 함수를 클래스로 바꿔보겠다.
function Monster(name, hp, att) {
this.name = name;
this.hp = hp;
this.att = att;
}
클래스로 바꾸면 다음과 같다.
class Monster {
constructor(name, hp, att) {
this.name = name;
this.hp = hp;
this.att = att;
}
}
class 예약어로 클래스를 선언하고, 생성자 함수 이름을 클래스 이름으로 넣는다. 매개변수를 포함한 기존 함수의 코드는 constructor() 메서드 안에 넣으면 된다. 객체와 마찬가지로 클래스 내부에 선언된 함수도 메서드라고 한다.
class <클래스 이름> {
constructor(매개변수1, 매개변수2, ...) {
// 생성자 함수 내용
}
}
클래스에 new를 붙여 호출하면 constructor() 메서드가 실행되고 객체가 반환된다. 이때 this는 생성된 객체 자신을 가리키게 된다.
const monster1 = new Monster('슬라임', 25, 10);
const monster2 = new Monster('슬라임', 26, 9);
const monster3 = new Monster('슬라임', 25, 11);
여기까지만 보면 클래스 문법을 사용해서 얻는 장점을 알 수 없다. 클래스 문법의 장점은 객체의 속성과 메서드를 하나로 묶을 수 있다는 데 있다.
다음 코드는 생성자 함수와 메서드가 클래스 안에 한 덩어리로 묶여 있어서 보기에 편하다.
class Monster {
constructor(name, hp, att) {
this.name = name;
this.hp = hp;
this.att = att;
}
attack(monster) {
monster.hp -= this.att;
}
}
4. 클래스 상속하기
클래스의 장점은 상속하기 쉽다는 점이다. 공장 함수나 생성자 함수도 상속을 구현할 수 있지만 자바스크립트는 클래스를 통해 깔끔하게 상속할 수 있는 문법을 제공한다.
class Hero {
constructor(name, hp, att) {
this.name = name;
this.hp = hp;
this.att = att;
this.maxHp = hp;
}
attack(monster) {
monster.hp -= this.att;
}
heal() {
this.hp = this.maxHp;
}
}
Hero 클래스와 Monster 클래스를 비교해보면 공통부분이 많다. 이름, 체력, 공격력 등의 속성과 attack() 메서드가 중복으로 들어 있다. 이런 중복 코드는 없앨 수 있지 않을까?
여기서 클래스의 상속이라는 개념이 등장한다. Hero와 Monster 클래스에서 공통되는 부분만 추려 새로운 클래스로 만든다. 그리고 Hero와 Monster 클래스는 새로운 클래스에서 공통부분을 가져와 사용한다. 이를 상송받는다고 표현한다. 이때 공통부문을 모아 만든 클래스를 부모 클래스, 상속받는 클래스를 자식 클래스라고 한다.
공통부분을 모아 부모 클래스인 Unit을 만들면 다음과 같다.
class Unit {
constructor(name, hp, att) {
this.name = name;
this.hp = hp;
this.att = att;
}
attack(target) {
target.hp -= this.att;
}
}
Unit 클래스를 상속받도록 Hero와 Monster 클래스를 수정하면 다음과 같다. 자식 클래스에서 부모 클래스를 상속받을 때는 extends라는 예약어를 사용한다.
class Hero extends Unit {
constructor(name, hp, att) {
super(name, hp, att); // 부모 클래스의 생성자 메서드 호출
this.maxHp = hp; // 그 외 속성
}
attack(target) {
super.attack(target); // 부모 클래스의 attack() 메서드 호출
console.log('부모 클래스의 attack() 외 추가 동작');
}
heal() { // 부모 클래스 메서드 외 동작
this.hp = this.maxHp;
}
}
class Monster extends Unit {
constructor(name, hp, att) {
super(name, hp, att);
}
attack(target) {
super.attack(target);
}
}
Hero와 Monster 클래스의 constructor() 메서드를 보면 둘 다 super() 라는 함수를 호출하고 있다. super() 함수는 부모 클래스(Unit)를 의미한다. 즉, 부모 클래스의 constructor() 메서드에 인수를 전달하는 함수이다. super() 함수가 호출되면 부모 클래스에서 자식 클래스 대신 name, hp, att 속성을 this에 입력한다. 이때 Hero 클래스의 maxHp 속성은 부모 클래스에는 존재하지 않는 속성이므로 super() 함수 아래에 따로 적는다. 공통 속성을 super() 함수로 처리한다고 보면 된다.
Hero 클래스의 attack() 메서드에서 super.attack()으로 호출하는데, 이것은 부모 클래스의 attack() 메서드를 호출하는 것과 같다. attack() 메서드 내부에서 super.attack()으로 호출한 뒤 다른 코드를 작성하면 부모 클래스의 메서드를 호출한 후 자신만의 작업을 할 수 있다. super.attack()을 호출하지 않고 다른 코드를 작성하면 부모 클래스와는 전혀 다른 작업을 할 수 있다.
Monster 클래스는 Unit 클래스를 상속하지만 Unit 클래스와 차이가 전혀 없다. constructor() 메서드도 부모 클래스의 메서드를 그대로 사용하고, attack() 메서드를 그대로 호출한다. 이처럼 부모와 하는 일이 같은 경우에는 메서드를 생략할 수 있습니다.
생성자 메서드와 attack() 메서드가 사라졌지만, 여전히 생성자 메서드와 attack() 메서드를 호출할 수 있다. 자식 클래스에 메서드를 생성하지 않은 경우, 부모 클래스에 메서드가 존재한다면 부모 클래스의 메서드를 대신 호출한다.
class Monster extends Unit {}
new Monster('슬라임', 29, 8); // 가능
'제로초님 자바스크립트 강의' 카테고리의 다른 글
[2주차] 스코프와 클로저 (0) | 2025.01.16 |
---|---|
[2주차] 비동기와 타이머 (0) | 2025.01.16 |
[2주차] 객체와 메서드 (0) | 2025.01.13 |
[1주차] 함수 정리 (0) | 2025.01.11 |
[1주차] 자바스크립트 배열 문법 정리 (0) | 2025.01.09 |