[JavaScript] 객체 지향 프로그래밍 가이드
KUKJIN LEE • 2개월 전 작성
1. 클래스와 객체의 기본 개념
1.1과 1.2를 같이 보셔야 합니다. class
는 객체를 생성하기 위한 양식이라고 생각하면 쉽습니다.
직접 객체를 생성할 수 있다 그럼 왜 class
를 사용할까요? 객체를 직접 생성하는 것보다 class
를 사용했을 때 가독성이 좋고, 유지보수가 쉽기 때문이다.
1.1 클래스 정의하기
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
return `안녕하세요, 제 이름은 ${this.name}입니다.`;
}
}
1.2 객체 생성과 사용
const person = new Person('GG', 20);
console.log(person.sayHello()); // "안녕하세요, 제 이름은 GG입니다."
이어서 살펴보면 좋은 내용이 상속과 확장입니다. 이해를 돕기 위해 예시를 준비했습니다. Add-on이나 Plugin을 생각하면 이해하기 쉽습니다. 기존 기능 위에 새로운 기능을 추가하거나 기존 기능을 확장하는 것입니다. 개발하면서 "바퀴를 다시 발명하지 말라"는 말을 들어보셨을 겁니다. 이미 존재하는 클래스를 사용할 수 있다면, 새로운 클래스를 만들어 사용하기보다 상속과 확장을 사용하는 것이 좋습니다.
1.3 클래스 상속
class Employee extends Person {
constructor(name, age, employeeId) {
super(name, age);
this.employeeId = employeeId;
}
getEmployeeInfo() {
return `${this.name} (사번: ${this.employeeId})`;
}
}
1.4 클래스 상속 사용 예시
const employee = new Employee('GG', 20, 'E12345');
console.log(employee.getEmployeeInfo()); // "GG (사번: E12345)"
2. 생성자와 인스턴스 생성
2.1 생성자 패턴
생성자 패턴은 "양식이 있는 걸 사용한다"고 생각하면 편합니다. 예를 들어 설문조사를 할 때, 질문이 있어야 답을 할 수 있듯이, 생성자 패턴은 객체를 만들기 위한 기본 틀을 제공합니다. 생성자를 통해 기본적인 값을 설정하고 객체를 생성하게 됩니다.
class User {
constructor(options = {}) {
this.name = options.name || '익명';
this.age = options.age || 0;
this.email = options.email || '';
this.initialize();
}
initialize() {
// 초기화 로직
console.log(`${this.name} 유저가 초기화되었습니다.`);
}
}
2.2 팩토리 패턴
팩토리 패턴은 키오스크와 같습니다. 예를 들어, 맥도날드에서 햄버거 주문을 생각해봅시다. 키오스크에서 1번을 누르면 빅맥, 2번을 누르면 불고기 버거가 나오는 것처럼, 팩토리 패턴도 특정 조건에 따라 객체를 생성하는 방식입니다.
class UserFactory {
static createUser(type, props) {
switch (type) {
case 'bigmac':
return new BigmacUser(props);
case 'chicken':
return new ChickenUser(props);
default:
return new ShrimpUser(props);
}
}
}
3. 메서드 오버라이딩
3.1 메서드 오버라이딩 예시 (JavaScript)
class Employee extends Person {
constructor(name, age, employeeId) {
super(name, age);
this.employeeId = employeeId;
}
sayHello() {
return `안녕하세요, 제 이름은 ${this.name}입니다. (사번: ${this.employeeId})`;
}
}
class Manager extends Employee {
sayHello() {
return `${super.sayHello()} 저는 매니저입니다.`;
}
}
const manager = new Manager('GG', 20, 'M1234');
console.log(manager.sayHello()); // "안녕하세요, 제 이름은 GG입니다. (사번: M1234) 저는 매니저입니다."
3.2 React를 오버라이딩 예시
import React from 'react';
// Employee 컴포넌트 정의
const Employee = ({ name, age, employeeId }) => {
// sayHello 함수는 이름과 사번을 출력합니다.
const sayHello = () => {
return `안녕하세요, 제 이름은 ${name}입니다. (사번: ${employeeId})`;
};
return (
<div>
<p>{sayHello()}</p>
</div>
);
};
// Manager 컴포넌트 정의 (Employee 컴포넌트를 확장하여 사용)
const Manager = ({ name, age, employeeId }) => {
// Employee 컴포넌트에서 정의한 sayHello 메서드를 확장합니다.
const sayHello = () => {
return `안녕하세요, 제 이름은 ${name}입니다. (사번: ${employeeId}) 저는 매니저입니다.`;
};
return (
<div>
<p>{sayHello()}</p>
</div>
);
};
// App 컴포넌트에서 Employee와 Manager를 사용
const App = () => {
return (
<div>
<h1>직원 및 매니저 정보</h1>
{/* Employee 컴포넌트 */}
<Employee name="KAKAO" age={30} employeeId="E5678" />
{/* Manager 컴포넌트 */}
<Manager name="GG" age={20} employeeId="M1234" />
</div>
);
};
export default App;
4. 캡슐화와 접근 제어
4.1 프라이빗 필드 사용
캡슐화는 클래스 내부의 데이터를 외부에서 직접 접근하지 못하게 하는 것입니다. JavaScript에서는 #
을 사용하여 프라이빗 필드를 정의할 수 있습니다. 그럼 왜 접근을 제한할까? 데이터 보호와 무결성 유지를 위해서입니다. 외부에서 데이터를 수정해 잘못된 값이 할당되거나 예기치 못한 동작을 초래하면 문제가 발생할 수 있습니다. 따라서 프라이빗 설정을 통해 정해진 메서드 안에서만 이루어지도록 하는 것이 좋습니다.
class BankAccount {
#balance = 0; // 프라이빗 필드
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return true;
}
return false;
}
getBalance() {
return this.#balance;
}
}
4.2 React 캡슐화, 프라이빗 필드 예시
핵심은 무결성을 유지하고, 안전하게 접근할 수 있어야 한다는 것입니다. 외부에서 직접 접근할 수 없고, Component가 제공하는 메서드를 통해서만 값을 변경하거나 조회할 수 있어야 합니다.
import React, { useState } from 'react';
// BankAccount 컴포넌트 정의
const BankAccount = () => {
// balance는 컴포넌트 내부의 프라이빗 상태로 관리됩니다.
const [balance, setBalance] = useState(0);
// 입금 함수: 외부에서 직접 balance를 변경하지 못하고, 이 함수만을 통해 수정 가능합니다.
const deposit = (amount) => {
if (amount > 0) {
setBalance((prevBalance) => prevBalance + amount);
return true;
}
return false;
};
// 잔액 조회 함수
const getBalance = () => {
return balance;
};
return (
<div>
<h2>은행 계좌</h2>
<p>잔액: {getBalance()}원</p>
<button onClick={() => deposit(100)}>100원 입금</button>
<button onClick={() => deposit(-50)}>잘못된 금액 입금 (테스트용)</button>
</div>
);
};
// App 컴포넌트에서 BankAccount 사용
const App = () => {
return (
<div>
<h1>은행 계좌 예시</h1>
<BankAccount />
</div>
);
};
export default App;
4.2 게터와 세터
게터(getter)와 세터(setter)는 클래스의 프라이빗 필드에 접근하거나 수정할 수 있는 메서드입니다.
setter
는 상태를 업데이트 하는 함수, getter
는 상태 값을 포맷팅하거나 필요한 형식으로 가공하는 함수로 이해하시면 됩니다.
class Product {
#price = 0;
get price() {
return `${this.#price}원`;
}
set price(value) {
if (value < 0) throw new Error('가격은 0보다 작을 수 없습니다.');
this.#price = value;
}
}
5. 다형성 구현하기
5.1 인터페이스 구현
JavaScript에는 인터페이스라는 개념이 명확히 존재하지 않지만, 추상 클래스나 메서드 강제 구현을 통해 비슷하게 구현할 수 있습니다. 추상 클래스는 반드시 하위 클래스에서 구현되어야 하는 메서드를 정의합니다.
(다형성을 통해 새로운 종류의 동물이 추가되더라도 확장할 수 있습니다.)
class Animal {
makeSound() {
throw new Error('이 메서드는 반드시 구현되어야 합니다.');
}
}
class Dog extends Animal {
makeSound() {
return '멍멍!';
}
}
class Cat extends Animal {
makeSound() {
return '야옹!';
}
}
const animals = [new Dog(), new Cat()];
animals.forEach(animal => console.log(animal.makeSound()));
// "멍멍!"
// "야옹!"