최근 Three.js를 공부하면서 강의 코드를 따라 치다 보니
class문법이 계속 등장했다. 나는 주로 함수형으로 코드를 작성해왔기 때문에 클래스 문법이 낯설고 어색했다. "이거 꼭 클래스로 써야 하나?" 싶기도 했지만, Three.js 커뮤니티에서 클래스를 많이 사용하는 걸 보고 제대로 공부해보기로 했다. 오늘은 클래스 문법을 처음부터 다시 공부하면서 정리한 내용을 공유하고자 한다.
클래스를 이해하는 가장 쉬운 방법은 붕어빵 틀에 비유하는 것이다.
// 붕어빵 틀 만들기 (Class 정의)
class Bungeoppang {
constructor(filling) {
this.filling = filling;
this.temperature = 'hot';
}
}
// 실제 붕어빵 만들기 (Instance 생성)
const redBeanBread = new Bungeoppang('팥');
const creamBread = new Bungeoppang('슈크림');
console.log(redBeanBread.filling); // '팥'
console.log(creamBread.filling); // '슈크림'
같은 틀(Class)을 사용하지만, 각각 다른 속재료(데이터)를 가진 붕어빵(Instance)이 만들어진다. 이게 클래스의 핵심 개념이다 😎
constructor는 붕어빵이 만들어질 때 초기 설정을 해주는 특별한 함수다. 인스턴스가 생성되는 순간 자동으로 실행된다.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const person1 = new Person('철수', 25);
console.log(person1.name); // '철수'
this는 뭘까?
this는 "지금 만들어지고 있는 이 인스턴스"를 가리킨다. this.name은 "이 사람의 이름"이라는 뜻이다.
메서드는 인스턴스가 할 수 있는 행동이다. 클래스 안에 정의된 함수라고 생각하면 된다.
class Dog {
constructor(name, age) {
this.name = name;
this.age = age;
}
bark() {
console.log(`${this.name}이(가) 멍멍!`);
}
getAge() {
return `${this.age}살`;
}
}
const myDog = new Dog('바둑이', 3);
myDog.bark(); // '바둑이이(가) 멍멍!'
console.log(myDog.getAge()); // '3살'
메서드 안에서도 this를 사용하면 인스턴스의 속성에 접근할 수 있다.
"굳이 클래스를 써야 하나?"라는 의문이 들 수 있다. 실제로 같은 기능을 함수로도 구현할 수 있다.
function createCar(brand, color) {
return {
brand: brand,
color: color,
drive: function() {
console.log(`${brand} 자동차가 달립니다!`);
},
honk: function() {
console.log('빵빵!');
}
};
}
const car1 = createCar('현대', '흰색');
const car2 = createCar('기아', '검정');
car1.drive(); // '현대 자동차가 달립니다!'
car2.honk(); // '빵빵!'
class Car {
constructor(brand, color) {
this.brand = brand;
this.color = color;
}
drive() {
console.log(`${this.brand} 자동차가 달립니다!`);
}
honk() {
console.log('빵빵!');
}
}
const car1 = new Car('현대', '흰색');
const car2 = new Car('기아', '검정');
car1.drive(); // '현대 자동차가 달립니다!'
car2.honk(); // '빵빵!'
결과는 똑같다! 그렇다면 왜 클래스를 쓸까?
클래스는 관련된 데이터(속성)와 기능(메서드)을 하나로 묶어준다. 코드를 읽는 사람이 "아, 이건 Car에 관련된 모든 것들이구나"라고 바로 이해할 수 있다.
// 함수형: 어디까지가 자동차 관련 코드인지 불명확
const car = createCar('현대', '흰색');
function repairCar(car) { /* ... */ }
function washCar(car) { /* ... */ }
// 클래스: Car 클래스만 보면 자동차의 모든 것을 알 수 있음
class Car {
constructor(brand, color) {
this.brand = brand;
this.color = color;
}
repair() { /* ... */ }
wash() { /* ... */ }
}
복잡한 상태를 가진 객체를 다룰 때 클래스가 훨씬 편하다.
class BankAccount {
constructor(owner, balance) {
this.owner = owner;
this.balance = balance;
this.transactions = [];
}
deposit(amount) {
this.balance += amount;
this.transactions.push({ type: 'deposit', amount, date: new Date() });
}
withdraw(amount) {
if (this.balance >= amount) {
this.balance -= amount;
this.transactions.push({ type: 'withdraw', amount, date: new Date() });
return true;
}
return false;
}
getBalance() {
return this.balance;
}
getHistory() {
return this.transactions;
}
}
const account = new BankAccount('철수', 10000);
account.deposit(5000);
account.withdraw(3000);
console.log(account.getBalance()); // 12000
console.log(account.getHistory()); // 거래 내역 배열
이렇게 여러 메서드가 같은 상태(balance, transactions)를 공유하고 수정할 때 클래스가 빛을 발한다!
나중에 기능을 추가하거나 수정할 때 클래스 안에서만 작업하면 되니까 관리가 편하다.
class Calculator {
constructor() {
this.result = 0;
}
add(number) {
this.result += number;
return this;
}
subtract(number) {
this.result -= number;
return this;
}
multiply(number) {
this.result *= number;
return this;
}
// 나중에 기능 추가하기 쉬움
divide(number) {
if (number !== 0) {
this.result /= number;
}
return this;
}
getResult() {
return this.result;
}
clear() {
this.result = 0;
return this;
}
}
// 메서드 체이닝도 가능!
const calc = new Calculator();
calc.add(10).multiply(2).subtract(5).divide(3);
console.log(calc.getResult()); // 5
같은 패턴의 객체가 여러 개 필요할 때 클래스로 찍어내면 된다.
class TodoItem {
constructor(text) {
this.text = text;
this.completed = false;
this.createdAt = new Date();
}
toggle() {
this.completed = !this.completed;
}
edit(newText) {
this.text = newText;
}
getStatus() {
return this.completed ? '완료' : '미완료';
}
}
// 여러 개의 할 일 생성
const todos = [
new TodoItem('클래스 문법 공부하기'),
new TodoItem('블로그 글 쓰기'),
new TodoItem('Three.js 강의 보기')
];
todos[0].toggle();
console.log(todos[0].getStatus()); // '완료'
이론만 보면 와닿지 않을 수 있으니, 실제로 사용할 법한 예제를 만들어보자.
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(name, price, quantity = 1) {
const existingItem = this.items.find(item => item.name === name);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ name, price, quantity });
}
}
removeItem(name) {
this.items = this.items.filter(item => item.name !== name);
}
updateQuantity(name, quantity) {
const item = this.items.find(item => item.name === name);
if (item) {
item.quantity = quantity;
}
}
getTotal() {
return this.items.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
}
getItemCount() {
return this.items.reduce((count, item) => count + item.quantity, 0);
}
clear() {
this.items = [];
}
getItems() {
return this.items;
}
}
// 사용 예시
const cart = new ShoppingCart();
cart.addItem('사과', 2000, 3);
cart.addItem('바나나', 3000, 2);
cart.addItem('사과', 2000, 1); // 기존 사과 수량 증가
console.log(cart.getItems());
// [
// { name: '사과', price: 2000, quantity: 4 },
// { name: '바나나', price: 3000, quantity: 2 }
// ]
console.log(cart.getTotal()); // 14000
console.log(cart.getItemCount()); // 6
cart.removeItem('바나나');
console.log(cart.getTotal()); // 8000
이렇게 장바구니의 모든 기능이 ShoppingCart 클래스 안에 깔끔하게 정리되어 있다. 나중에 할인 기능을 추가하거나, 배송비를 계산하는 메서드를 추가하기도 쉽다!
ES2022부터는 #을 사용해서 외부에서 접근할 수 없는 private 필드를 만들 수 있다.
class User {
#password; // private 필드
constructor(username, password) {
this.username = username;
this.#password = password;
}
// Getter
get username() {
return this._username;
}
// Setter
set username(value) {
if (value.length < 3) {
console.log('사용자명은 3글자 이상이어야 합니다.');
return;
}
this._username = value;
}
checkPassword(input) {
return this.#password === input;
}
changePassword(oldPassword, newPassword) {
if (this.checkPassword(oldPassword)) {
this.#password = newPassword;
return true;
}
return false;
}
}
const user = new User('철수', 'secret123');
console.log(user.username); // '철수'
// console.log(user.#password); // 오류! private 필드는 외부에서 접근 불가
console.log(user.checkPassword('secret123')); // true
user.changePassword('secret123', 'newPassword456');
이렇게 하면 민감한 정보를 보호하고, 데이터를 안전하게 관리할 수 있다.
Three.js 강의를 보다 보면 클래스 문법이 자주 등장한다. 그 이유는 간단하다:
1. Three.js 자체가 클래스 기반이다
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera();
const renderer = new THREE.WebGLRenderer();
2. 3D 씬은 상태 관리가 복잡하다
카메라, 조명, 오브젝트, 애니메이션 등 여러 요소를 관리해야 하는데, 클래스로 묶으면 훨씬 깔끔하다.
class MyScene {
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera();
this.renderer = new THREE.WebGLRenderer();
this.cube = null;
}
init() {
this.createCube();
this.setupLights();
this.setupCamera();
}
createCube() {
// 큐브 생성
}
animate() {
// 애니메이션 루프
}
}
3. 재사용 가능한 3D 오브젝트를 만들기 좋다
class CustomCube {
constructor(size, color) {
this.size = size;
this.color = color;
this.mesh = this.create();
}
create() {
const geometry = new THREE.BoxGeometry(this.size, this.size, this.size);
const material = new THREE.MeshBasicMaterial({ color: this.color });
return new THREE.Mesh(geometry, material);
}
rotate(x, y) {
this.mesh.rotation.x += x;
this.mesh.rotation.y += y;
}
}
클래스 문법을 다시 공부하면서 "왜 쓰는지"에 대한 답을 찾을 수 있었다. 단순히 문법을 외우는 게 아니라, 언제 클래스가 유용한지 이해하니 코드를 볼 때 훨씬 편해졌다.
특히 Three.js 강의 코드를 보면서 "왜 이렇게 작성했을까?"를 이해할 수 있게 되었고, 복잡한 상태를 관리할 때는 클래스가 정말 유용하다는 걸 체감했다.
함수형으로 작성하는 게 익숙하더라도, 클래스 문법을 알아두면 남의 코드를 읽을 때나 협업할 때 큰 도움이 된다. 앞으로는 상황에 맞게 함수형과 클래스를 적절히 섞어 쓸 수 있을 것 같다!
"이거 클래스로 짜는 게 나을까, 함수로 짜는 게 나을까?" 고민하는 시간이 줄어들고, 자신있게 선택할 수 있게 되었다는 게 이번 공부의 가장 큰 수확이다 💪