인스턴스 (Instance)
혹시 이런 코드를 본 적 있나요?
const date1 = new Date();
const date2 = new Date();
date1.setFullYear(2025);
console.log(date2.getFullYear()); // 왜 2025가 아니라 현재 연도가 나올까?
처음 JavaScript를 배울 때 저도 이런 의문이 들었습니다. “같은 Date인데 왜 따로 동작하지?” 이것이 바로 인스턴스(Instance)의 핵심 개념입니다. 각각이 독립적인 존재라는 것이죠.
상상해보세요. 건축가가 설계도를 그렸습니다. 그 설계도를 바탕으로 서울에 하나, 부산에 하나, 제주도에 하나, 똑같은 구조의 집을 지었습니다. 설계도는 하나지만 실제 집은 세 채입니다. 서울 집의 벽을 파란색으로 칠한다고 해서 부산 집 벽이 파란색으로 변하지 않습니다. 각 집은 독립적인 실체니까요.
JavaScript의 인스턴스도 정확히 이렇게 동작합니다. 클래스나 생성자 함수는 설계도이고, new 키워드로 만든 각각의 객체는 그 설계도를 바탕으로 만들어진 독립적인 실체입니다.
왜 인스턴스를 이해해야 할까요?
1. 현대 JavaScript 개발의 기초
React, Vue, Angular 같은 프레임워크를 사용하다 보면 이런 코드를 자주 만나게 됩니다.
// React 컴포넌트
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = { isLoading: true };
}
}
// Vue 인스턴스
const app = new Vue({
data: { message: 'Hello' }
});
// Axios 인스턴스
const api = axios.create({
baseURL: 'https://api.example.com'
});
이 모든 것이 인스턴스 개념을 활용합니다. 인스턴스를 이해하지 못하면 이 코드들이 어떻게 동작하는지, 왜 이렇게 작성하는지 알 수 없습니다.
2. 메모리와 성능 최적화
인스턴스를 이해하면 불필요한 객체 생성을 피할 수 있습니다.
// ❌ 나쁜 예: 매번 새로운 Date 인스턴스 생성
function formatCurrentTime() {
return new Date().toLocaleTimeString();
}
// 1초에 60번 호출하면 60개의 Date 인스턴스 생성!
setInterval(() => {
console.log(formatCurrentTime());
}, 16);
// ✅ 좋은 예: 필요할 때만 생성
let lastTime = null;
function formatCurrentTime() {
const now = Date.now();
// 1초에 한 번만 업데이트
if (!lastTime || now - lastTime > 1000) {
lastTime = now;
}
return new Date(lastTime).toLocaleTimeString();
}
3. 디버깅과 문제 해결
실무에서 이런 버그를 만났을 때:
const user1 = { name: 'Alice', scores: [85, 90] };
const user2 = user1;
user2.scores.push(95);
console.log(user1.scores); // [85, 90, 95] - 왜?!
인스턴스와 참조의 개념을 이해해야 해결할 수 있습니다.
기본 개념: 인스턴스란 무엇인가?
객체 vs 인스턴스
모든 인스턴스는 객체이지만, 모든 객체가 인스턴스는 아닙니다.
// 그냥 객체 (리터럴)
const obj1 = { name: 'Alice' };
// 인스턴스 (생성자/클래스로 생성)
const obj2 = new Object({ name: 'Alice' });
console.log(obj1 instanceof Object); // true (객체는 Object의 인스턴스)
console.log(obj2 instanceof Object); // true
// 하지만 의미적으로 다름
// obj1: 단순 데이터 객체
// obj2: Object 생성자로 만든 인스턴스
인스턴스의 핵심 특징:
- 생성자 함수나 클래스로부터 만들어짐
- 프로토타입 체인을 통해 메서드를 상속받음
instanceof연산자로 타입 확인 가능- 독립적인 상태(데이터)를 가짐
생성자 함수와 인스턴스
// 생성자 함수 (관례적으로 대문자로 시작)
function Car(brand, color) {
// this = 새로 만들어질 인스턴스를 가리킴
this.brand = brand;
this.color = color;
this.mileage = 0;
}
// 프로토타입에 메서드 추가 (모든 인스턴스가 공유)
Car.prototype.drive = function(distance) {
this.mileage += distance;
return `${this.color} ${this.brand}가 ${distance}km 주행했습니다.`;
};
// 인스턴스 생성
const tesla = new Car('Tesla', 'red');
const bmw = new Car('BMW', 'blue');
tesla.drive(100);
bmw.drive(50);
console.log(tesla.mileage); // 100
console.log(bmw.mileage); // 50
// 메서드는 공유하지만 상태는 독립적
console.log(tesla.drive === bmw.drive); // true (같은 함수 참조)
new 키워드가 하는 일
new Car('Tesla', 'red')를 실행하면 내부적으로:
// 1. 빈 객체 생성
const instance = {};
// 2. 프로토타입 연결
Object.setPrototypeOf(instance, Car.prototype);
// 또는: instance.__proto__ = Car.prototype;
// 3. 생성자 함수 실행 (this를 새 객체에 바인딩)
Car.call(instance, 'Tesla', 'red');
// 4. 객체 반환 (생성자가 명시적으로 객체를 반환하지 않으면)
return instance;
클래스와 인스턴스 (ES6+)
클래스는 생성자 함수의 문법적 설탕(syntactic sugar)입니다.
class Animal {
// 생성자 메서드
constructor(name, age) {
this.name = name;
this.age = age;
}
// 인스턴스 메서드 (프로토타입에 추가됨)
speak() {
return `${this.name}가 소리를 냅니다.`;
}
// 정적 메서드 (클래스 자체의 메서드)
static compare(animal1, animal2) {
return animal1.age - animal2.age;
}
}
const dog = new Animal('멍멍이', 3);
const cat = new Animal('야옹이', 2);
console.log(dog.speak()); // "멍멍이가 소리를 냅니다."
console.log(Animal.compare(dog, cat)); // 1
// 정적 메서드는 인스턴스에서 호출 불가
console.log(dog.compare); // undefined
정적 메서드 vs 인스턴스 메서드: 왜 분리되어 있을까?
위 예제에서 dog.compare가 undefined인 이유가 궁금하셨나요? 정적 메서드와 인스턴스 메서드는 저장되는 위치가 완전히 다릅니다.
저장 위치의 차이
class Animal {
constructor(name) {
this.name = name;
}
// 인스턴스 메서드 → Animal.prototype에 저장
speak() {
return `${this.name}가 소리를 냅니다.`;
}
// 정적 메서드 → Animal 클래스 자체에 저장
static createPet(name) {
return new Animal(name);
}
}
// 확인해보기
console.log(Animal.prototype.speak); // function (프로토타입에 있음)
console.log(Animal.createPet); // function (클래스 자체에 있음)
const dog = new Animal('멍멍이');
// ✅ 인스턴스 메서드: 프로토타입 체인을 통해 접근
console.log(dog.speak()); // "멍멍이가 소리를 냅니다."
// ❌ 정적 메서드: 프로토타입 체인에 없음
console.log(dog.createPet); // undefined
프로토타입 체인과 정적 메서드:
┌─────────────────────────┐
│ Animal 클래스 자체 │
│ createPet: function │ ← 정적 메서드 (인스턴스 접근 불가)
└─────────────────────────┘
↑
│ constructor
│
┌─────────────────────────┐
│ dog 인스턴스 │
│ name: '멍멍이' │
└──────────┬──────────────┘
│ [[Prototype]]
↓
┌─────────────────────────┐
│ Animal.prototype │
│ speak: function │ ← 인스턴스 메서드 (인스턴스 접근 가능)
└─────────────────────────┘
인스턴스(dog)는 프로토타입 체인을 통해 Animal.prototype의 메서드는 찾을 수 있지만, Animal 클래스 자체에 있는 정적 메서드에는 접근할 수 없습니다.
왜 이렇게 분리했을까?
1. 개념적 차이
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 인스턴스 메서드: "이 특정 사용자"에 대한 동작
greet() {
return `안녕하세요, ${this.name}입니다.`;
}
// 정적 메서드: "사용자 클래스 전체"에 대한 유틸리티
static compareAge(user1, user2) {
return user1.age - user2.age;
}
static createAdmin(name) {
const user = new User(name, 0);
user.role = 'admin';
return user;
}
}
const alice = new User('Alice', 25);
const bob = new User('Bob', 30);
// ✅ 인스턴스 메서드: 특정 인스턴스에 대한 작업
alice.greet(); // "안녕하세요, Alice입니다."
// ✅ 정적 메서드: 클래스 레벨의 유틸리티
User.compareAge(alice, bob); // -5
User.createAdmin('Charlie'); // 팩토리 메서드
- 인스턴스 메서드: “이 특정 사용자가 인사한다”
- 정적 메서드: “사용자 클래스가 두 사용자를 비교한다” 또는 “새 관리자를 생성한다”
2. 메모리 효율성
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
// ✅ 정적 메서드: 딱 한 번만 메모리에 존재
static findCheapest(products) {
return products.reduce((min, p) => p.price < min.price ? p : min);
}
}
const products = [
new Product('A', 1000),
new Product('B', 500),
new Product('C', 1500)
];
// 1000개의 제품 인스턴스를 만들어도
// findCheapest 메서드는 메모리에 딱 한 번만 존재
console.log(Product.findCheapest(products)); // Product { name: 'B', price: 500 }
만약 findCheapest가 인스턴스 메서드였다면, 모든 제품 인스턴스가 이 메서드를 사용할 수 있지만 실제로는 필요하지 않은 기능입니다. 정적 메서드로 만들면 메모리를 절약할 수 있습니다.
3. 명확한 의도 전달
// 유틸리티 클래스 - 인스턴스를 만들 필요가 없음
class MathHelper {
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
}
// ✅ 명확: 이건 유틸리티 클래스다
MathHelper.add(1, 2); // 3
// ❌ 인스턴스를 만드는 건 의미 없음
const helper = new MathHelper();
// helper.add(1, 2); // undefined - 의도적으로 불가능
실제 JavaScript의 Math 객체도 정적 메서드만 제공합니다:
Math.max(1, 2, 3); // ✅ 정적 메서드
const m = new Math(); // ❌ TypeError: Math is not a constructor
정적 메서드를 인스턴스에서 사용하고 싶다면?
명시적으로 constructor를 통해 접근할 수 있습니다:
class Counter {
constructor(value) {
this.value = value;
}
static create(value) {
return new Counter(value);
}
// 인스턴스 메서드에서 정적 메서드 호출
reset() {
// this.constructor는 Counter를 가리킴
return this.constructor.create(0);
}
}
const counter = new Counter(10);
// ✅ constructor를 통해 정적 메서드 접근
console.log(counter.constructor.create(5)); // Counter { value: 5 }
// ✅ 인스턴스 메서드를 통해 간접 호출
console.log(counter.reset()); // Counter { value: 0 }
실전 활용: 정적 메서드의 일반적인 용도
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.createdAt = new Date();
}
// 1. 팩토리 메서드
static createGuest() {
return new User('Guest', 'guest@example.com');
}
static createFromData(data) {
return new User(data.name, data.email);
}
// 2. 유틸리티 함수
static isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// 3. 인스턴스 관리
static instances = [];
static addInstance(instance) {
User.instances.push(instance);
}
static getCount() {
return User.instances.length;
}
}
// 팩토리 메서드 사용
const guest = User.createGuest();
// 유효성 검사 (인스턴스 없이도 사용 가능)
console.log(User.isValidEmail('test@example.com')); // true
// 인스턴스 관리
User.addInstance(new User('Alice', 'alice@example.com'));
User.addInstance(new User('Bob', 'bob@example.com'));
console.log(User.getCount()); // 2
프로토타입 체인 시각화
┌─────────────────────┐
│ dog 인스턴스 │
│ name: '멍멍이' │
│ age: 3 │
└──────────┬──────────┘
│ [[Prototype]]
↓
┌─────────────────────┐
│ Animal.prototype │
│ speak: function │
│ constructor: Animal│
└──────────┬──────────┘
│ [[Prototype]]
↓
┌─────────────────────┐
│ Object.prototype │
│ toString: function │
│ hasOwnProperty: fn │
└──────────┬──────────┘
│
↓
null
실전 예제: 다양한 인스턴스 생성 방법
1. new 키워드 사용
가장 전통적이고 명확한 방법:
function User(name, email) {
this.name = name;
this.email = email;
this.createdAt = new Date();
}
User.prototype.sendEmail = function(subject) {
console.log(`${this.email}로 "${subject}" 전송`);
};
const user1 = new User('Alice', 'alice@example.com');
user1.sendEmail('환영합니다!');
// ❌ new 없이 호출하면?
const user2 = User('Bob', 'bob@example.com');
console.log(user2); // undefined
console.log(window.name); // 'Bob' - 전역 오염!
new 없는 호출 방지하기
function SafeUser(name, email) {
// new 없이 호출되었는지 체크
if (!(this instanceof SafeUser)) {
return new SafeUser(name, email);
}
this.name = name;
this.email = email;
}
// 두 방법 모두 동작
const user3 = new SafeUser('Charlie', 'charlie@example.com');
const user4 = SafeUser('David', 'david@example.com');
console.log(user3 instanceof SafeUser); // true
console.log(user4 instanceof SafeUser); // true
2. Object.create() 사용
프로토타입을 직접 지정하여 인스턴스 생성:
const personPrototype = {
greet() {
return `안녕하세요, ${this.name}입니다.`;
},
getAge() {
const today = new Date();
return today.getFullYear() - this.birthYear;
}
};
// 프로토타입을 지정하여 객체 생성
const person1 = Object.create(personPrototype);
person1.name = 'Alice';
person1.birthYear = 1990;
console.log(person1.greet()); // "안녕하세요, Alice입니다."
console.log(person1.getAge()); // 35 (2025년 기준)
// 프로토타입 체인 확인
console.log(Object.getPrototypeOf(person1) === personPrototype); // true
Object.create()의 활용: 상속 구현
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function() {
return `Hello, I'm ${this.name}`;
};
function Child(name, age) {
Parent.call(this, name); // 부모 생성자 호출
this.age = age;
}
// 프로토타입 체인 설정
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
return `I'm ${this.age} years old`;
};
const child = new Child('Alice', 10);
console.log(child.sayHello()); // "Hello, I'm Alice"
console.log(child.sayAge()); // "I'm 10 years old"
// 프로토타입 체인
console.log(child instanceof Child); // true
console.log(child instanceof Parent); // true
3. 클래스 문법 (ES6+)
현대적이고 가독성이 좋은 방법:
class Rectangle {
// 인스턴스 필드 (public)
width;
height;
// private 필드 (ES2022+)
#id = Math.random();
// static 필드
static count = 0;
constructor(width, height) {
this.width = width;
this.height = height;
Rectangle.count++;
}
// getter
get area() {
return this.width * this.height;
}
// setter
set area(value) {
// 정사각형으로 만들기
const side = Math.sqrt(value);
this.width = side;
this.height = side;
}
// 인스턴스 메서드
resize(factor) {
this.width *= factor;
this.height *= factor;
}
// private 메서드
#getId() {
return this.#id;
}
// static 메서드
static fromSquare(side) {
return new Rectangle(side, side);
}
}
const rect1 = new Rectangle(10, 20);
console.log(rect1.area); // 200
rect1.area = 400;
console.log(rect1.width); // 20
console.log(rect1.height); // 20
const square = Rectangle.fromSquare(15);
console.log(square.area); // 225
console.log(Rectangle.count); // 2
4. 팩토리 함수 패턴
클래스나 생성자 없이 객체 생성:
function createCounter(initialValue = 0) {
// 클로저를 활용한 private 변수
let count = initialValue;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getValue() {
return count;
},
reset() {
count = initialValue;
return count;
}
};
}
const counter1 = createCounter(10);
const counter2 = createCounter(20);
console.log(counter1.increment()); // 11
console.log(counter2.increment()); // 21
// 각각 독립적
console.log(counter1.getValue()); // 11
console.log(counter2.getValue()); // 21
// private 변수에 직접 접근 불가
console.log(counter1.count); // undefined
좋은 예 vs 나쁜 예
❌ 나쁜 예 1: 인스턴스 프로퍼티를 프로토타입에 정의
function BadUser(name) {
this.name = name;
}
// ❌ 배열을 프로토타입에 정의 (모든 인스턴스가 공유!)
BadUser.prototype.hobbies = [];
const user1 = new BadUser('Alice');
const user2 = new BadUser('Bob');
user1.hobbies.push('독서');
user2.hobbies.push('운동');
console.log(user1.hobbies); // ['독서', '운동'] - 오염됨!
console.log(user2.hobbies); // ['독서', '운동'] - 오염됨!
✅ 좋은 예 1: 인스턴스 프로퍼티는 생성자에서
function GoodUser(name) {
this.name = name;
this.hobbies = []; // 각 인스턴스마다 새 배열
}
const user3 = new GoodUser('Charlie');
const user4 = new GoodUser('David');
user3.hobbies.push('독서');
user4.hobbies.push('운동');
console.log(user3.hobbies); // ['독서']
console.log(user4.hobbies); // ['운동']
❌ 나쁜 예 2: 메서드를 생성자 안에서 정의
function BadAnimal(name) {
this.name = name;
// ❌ 인스턴스마다 새로운 함수 생성 (메모리 낭비)
this.speak = function() {
console.log(`${this.name}가 소리를 냅니다.`);
};
}
const animal1 = new BadAnimal('멍멍이');
const animal2 = new BadAnimal('야옹이');
console.log(animal1.speak === animal2.speak); // false (다른 함수!)
✅ 좋은 예 2: 메서드는 프로토타입에
function GoodAnimal(name) {
this.name = name;
}
// ✅ 프로토타입에 메서드 정의 (모든 인스턴스가 공유)
GoodAnimal.prototype.speak = function() {
console.log(`${this.name}가 소리를 냅니다.`);
};
const animal3 = new GoodAnimal('멍멍이');
const animal4 = new GoodAnimal('야옹이');
console.log(animal3.speak === animal4.speak); // true (같은 함수!)
❌ 나쁜 예 3: this 바인딩 실수
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
}
}
const counter = new Counter();
// ❌ 메서드를 참조만 전달하면 this가 사라짐
const incrementFn = counter.increment;
incrementFn(); // TypeError: Cannot read property 'count' of undefined
✅ 좋은 예 3: this 바인딩 명확히 하기
class Counter {
constructor() {
this.count = 0;
// 방법 1: 생성자에서 바인딩
this.increment = this.increment.bind(this);
}
increment() {
this.count++;
}
// 방법 2: 화살표 함수 사용 (class field)
decrement = () => {
this.count--;
}
}
const counter = new Counter();
const incrementFn = counter.increment;
const decrementFn = counter.decrement;
incrementFn(); // 동작!
decrementFn(); // 동작!
console.log(counter.count); // 0
활용
1. instanceof 연산자의 동작 원리
instanceof는 프로토타입 체인을 따라 올라가며 확인합니다.
function MyClass() {}
const instance = new MyClass();
// instanceof의 내부 동작 구현
function customInstanceOf(obj, constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto !== null) {
if (proto === constructor.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
console.log(customInstanceOf(instance, MyClass)); // true
console.log(customInstanceOf(instance, Object)); // true
console.log(customInstanceOf(instance, Array)); // false
2. Symbol.hasInstance로 커스터마이징
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true
console.log({} instanceof MyArray); // false
// 실용적인 예: 타입 체커
class TypeChecker {
static [Symbol.hasInstance](instance) {
return (
typeof instance === 'object' &&
instance !== null &&
'validate' in instance &&
typeof instance.validate === 'function'
);
}
}
const validator = {
validate() { return true; }
};
console.log(validator instanceof TypeChecker); // true
3. 인스턴스 vs 프로토타입 프로퍼티
class Person {
constructor(name) {
// 인스턴스 프로퍼티
this.name = name;
this.id = Math.random();
}
// 프로토타입 메서드
greet() {
return `안녕하세요, ${this.name}입니다.`;
}
}
// 프로토타입에 동적으로 추가
Person.prototype.species = 'Human';
const person1 = new Person('Alice');
const person2 = new Person('Bob');
// 인스턴스 프로퍼티는 각자 다름
console.log(person1.id !== person2.id); // true
// 프로토타입 프로퍼티는 공유
console.log(person1.species === person2.species); // true
// 인스턴스에서 덮어쓰기
person1.species = 'Robot';
console.log(person1.species); // 'Robot' (인스턴스 프로퍼티)
console.log(person2.species); // 'Human' (프로토타입 프로퍼티)
console.log(Person.prototype.species); // 'Human' (영향 없음)
// 프로퍼티 확인
console.log(person1.hasOwnProperty('species')); // true
console.log(person2.hasOwnProperty('species')); // false
4. 메모리 최적화: 여러 인스턴스의 효율적 관리
class GameCharacter {
// 모든 캐릭터가 사용하는 스프라이트 시트 (static)
static spriteSheet = null;
// 인스턴스 카운터
static instanceCount = 0;
static maxInstances = 100;
constructor(x, y, type) {
// 인스턴스 제한
if (GameCharacter.instanceCount >= GameCharacter.maxInstances) {
throw new Error('최대 인스턴스 수를 초과했습니다.');
}
this.x = x;
this.y = y;
this.type = type;
this.health = 100;
GameCharacter.instanceCount++;
}
destroy() {
GameCharacter.instanceCount--;
// 정리 작업...
}
// 프로토타입 메서드 (공유됨)
move(dx, dy) {
this.x += dx;
this.y += dy;
}
// 무거운 리소스는 static으로 공유
static loadSpriteSheet(url) {
if (!GameCharacter.spriteSheet) {
GameCharacter.spriteSheet = loadImage(url);
}
return GameCharacter.spriteSheet;
}
}
function loadImage(url) {
return { url, loaded: true };
}
// 스프라이트 한 번만 로드
GameCharacter.loadSpriteSheet('/sprites.png');
// 여러 캐릭터 생성
const characters = [];
for (let i = 0; i < 50; i++) {
characters.push(new GameCharacter(i * 10, 0, 'enemy'));
}
console.log(GameCharacter.instanceCount); // 50
console.log(characters[0].move === characters[1].move); // true (메서드 공유)
5. WeakMap을 활용한 Private 데이터
const privateData = new WeakMap();
class BankAccount {
constructor(owner, balance) {
// public 프로퍼티
this.owner = owner;
// private 데이터 (WeakMap에 저장)
privateData.set(this, {
balance: balance,
pin: Math.floor(Math.random() * 10000)
});
}
deposit(amount) {
const data = privateData.get(this);
data.balance += amount;
return data.balance;
}
withdraw(amount, pin) {
const data = privateData.get(this);
if (data.pin !== pin) {
throw new Error('PIN이 일치하지 않습니다.');
}
if (data.balance < amount) {
throw new Error('잔액이 부족합니다.');
}
data.balance -= amount;
return data.balance;
}
getBalance(pin) {
const data = privateData.get(this);
if (data.pin !== pin) {
throw new Error('PIN이 일치하지 않습니다.');
}
return data.balance;
}
}
const account = new BankAccount('Alice', 1000);
account.deposit(500);
console.log(account.owner); // 'Alice'
console.log(account.balance); // undefined (접근 불가)
// PIN을 모르면 조회 불가
try {
account.getBalance(1234);
} catch (e) {
console.log(e.message); // "PIN이 일치하지 않습니다."
}
함정과 주의사항
1. this 바인딩 문제
class Button {
constructor(label) {
this.label = label;
this.clickCount = 0;
}
handleClick() {
this.clickCount++;
console.log(`${this.label} 버튼이 ${this.clickCount}번 클릭됨`);
}
}
const button = new Button('제출');
// ❌ 문제: 이벤트 리스너에서 this가 바뀜
document.querySelector('#submit').addEventListener('click', button.handleClick);
// TypeError: Cannot read property 'clickCount' of undefined
// ✅ 해결 1: bind 사용
document.querySelector('#submit').addEventListener('click',
button.handleClick.bind(button)
);
// ✅ 해결 2: 화살표 함수로 래핑
document.querySelector('#submit').addEventListener('click',
() => button.handleClick()
);
// ✅ 해결 3: 클래스 필드 (자동 바인딩)
class BetterButton {
constructor(label) {
this.label = label;
this.clickCount = 0;
}
handleClick = () => {
this.clickCount++;
console.log(`${this.label} 버튼이 ${this.clickCount}번 클릭됨`);
}
}
2. 프로토타입 오염
// ❌ 위험: 내장 프로토타입 수정
Array.prototype.first = function() {
return this[0];
};
const arr = [1, 2, 3];
console.log(arr.first()); // 1
// 문제: 모든 배열이 영향받음
const otherArr = [];
console.log('first' in otherArr); // true
// for...in에서도 나타남
for (let key in arr) {
console.log(key); // '0', '1', '2', 'first'
}
// ✅ 더 나은 방법: 헬퍼 함수
function first(array) {
return array[0];
}
console.log(first([1, 2, 3])); // 1
3. 생성자 반환값 주의
function WeirdConstructor() {
this.value = 42;
// ❌ 객체를 명시적으로 반환하면 this가 무시됨
return { value: 100 };
}
const weird = new WeirdConstructor();
console.log(weird.value); // 100 (this.value가 아님!)
// ✅ 원시값 반환은 무시됨 (정상 동작)
function NormalConstructor() {
this.value = 42;
return 100; // 무시됨
}
const normal = new NormalConstructor();
console.log(normal.value); // 42
4. instanceof의 한계
// 문제 1: iframe에서 생성된 배열
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
const arr = new iframeArray(1, 2, 3);
console.log(arr instanceof Array); // false!
console.log(Array.isArray(arr)); // true ✅
// 문제 2: Object.create(null)
const obj = Object.create(null);
console.log(obj instanceof Object); // false
console.log(typeof obj); // 'object'
// ✅ 더 안전한 타입 체크
function isArray(value) {
return Array.isArray(value);
}
function isObject(value) {
return value !== null && typeof value === 'object';
}
5. 클래스 필드의 this
class Counter {
count = 0;
// ❌ 화살표 함수는 인스턴스마다 새로 생성됨
increment = () => {
this.count++;
}
// ✅ 일반 메서드는 프로토타입에 공유됨
decrement() {
this.count--;
}
}
const c1 = new Counter();
const c2 = new Counter();
console.log(c1.increment === c2.increment); // false (각자 생성)
console.log(c1.decrement === c2.decrement); // true (공유)
// 트레이드오프:
// - 화살표 함수: this 바인딩 자동, 메모리 많이 사용
// - 일반 메서드: 메모리 효율적, this 바인딩 수동 처리
실전 활용
1. UI 컴포넌트 인스턴스
class Modal {
static activeModals = new Set();
constructor(options) {
this.id = options.id || `modal-${Date.now()}`;
this.title = options.title;
this.content = options.content;
this.isOpen = false;
this.element = null;
this.onOpen = options.onOpen || (() => {});
this.onClose = options.onClose || (() => {});
this.create();
}
create() {
this.element = document.createElement('div');
this.element.className = 'modal';
this.element.innerHTML = `
<div class="modal-overlay"></div>
<div class="modal-content">
<h2>${this.title}</h2>
<div class="modal-body">${this.content}</div>
<button class="modal-close">닫기</button>
</div>
`;
// 이벤트 리스너 (this 바인딩 주의!)
this.element.querySelector('.modal-close')
.addEventListener('click', () => this.close());
document.body.appendChild(this.element);
}
open() {
if (this.isOpen) return;
this.isOpen = true;
this.element.classList.add('open');
Modal.activeModals.add(this);
this.onOpen();
// ESC 키로 닫기
this.handleKeyDown = (e) => {
if (e.key === 'Escape') this.close();
};
document.addEventListener('keydown', this.handleKeyDown);
}
close() {
if (!this.isOpen) return;
this.isOpen = false;
this.element.classList.remove('open');
Modal.activeModals.delete(this);
this.onClose();
document.removeEventListener('keydown', this.handleKeyDown);
}
destroy() {
this.close();
this.element.remove();
}
static closeAll() {
Modal.activeModals.forEach(modal => modal.close());
}
}
// 사용 예
const loginModal = new Modal({
id: 'login',
title: '로그인',
content: '<form>...</form>',
onOpen: () => console.log('로그인 모달 열림'),
onClose: () => console.log('로그인 모달 닫힘')
});
const signupModal = new Modal({
id: 'signup',
title: '회원가입',
content: '<form>...</form>'
});
loginModal.open();
signupModal.open();
console.log(Modal.activeModals.size); // 2
// 모든 모달 닫기
Modal.closeAll();
2. 게임 오브젝트 관리
class GameObject {
static allObjects = [];
static nextId = 1;
constructor(x, y, type) {
this.id = GameObject.nextId++;
this.x = x;
this.y = y;
this.type = type;
this.isActive = true;
GameObject.allObjects.push(this);
}
update(deltaTime) {
// 각 타입마다 오버라이드
}
destroy() {
this.isActive = false;
const index = GameObject.allObjects.indexOf(this);
if (index > -1) {
GameObject.allObjects.splice(index, 1);
}
}
static updateAll(deltaTime) {
GameObject.allObjects.forEach(obj => {
if (obj.isActive) {
obj.update(deltaTime);
}
});
}
static findById(id) {
return GameObject.allObjects.find(obj => obj.id === id);
}
static findByType(type) {
return GameObject.allObjects.filter(obj => obj.type === type);
}
}
class Player extends GameObject {
constructor(x, y) {
super(x, y, 'player');
this.health = 100;
this.speed = 5;
}
update(deltaTime) {
// 플레이어 로직
}
takeDamage(amount) {
this.health -= amount;
if (this.health <= 0) {
this.destroy();
}
}
}
class Enemy extends GameObject {
constructor(x, y) {
super(x, y, 'enemy');
this.health = 50;
}
update(deltaTime) {
// 적 AI
const player = GameObject.findByType('player')[0];
if (player) {
// 플레이어를 향해 이동
const dx = player.x - this.x;
const dy = player.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
this.x += (dx / distance) * 2 * deltaTime;
this.y += (dy / distance) * 2 * deltaTime;
}
}
}
}
// 게임 실행
const player = new Player(400, 300);
const enemy1 = new Enemy(100, 100);
const enemy2 = new Enemy(700, 100);
const enemy3 = new Enemy(100, 500);
console.log(GameObject.allObjects.length); // 4
// 게임 루프
function gameLoop() {
const deltaTime = 0.016; // 60fps
GameObject.updateAll(deltaTime);
requestAnimationFrame(gameLoop);
}
// gameLoop();
3. 데이터 모델과 상태 관리
class Store {
constructor(initialState = {}) {
this.state = { ...initialState };
this.listeners = new Set();
}
getState() {
return { ...this.state };
}
setState(updater) {
const newState = typeof updater === 'function'
? updater(this.state)
: updater;
this.state = { ...this.state, ...newState };
this.notify();
}
subscribe(listener) {
this.listeners.add(listener);
// 구독 취소 함수 반환
return () => {
this.listeners.delete(listener);
};
}
notify() {
this.listeners.forEach(listener => {
listener(this.state);
});
}
}
class TodoStore extends Store {
constructor() {
super({ todos: [], filter: 'all' });
}
addTodo(text) {
const todo = {
id: Date.now(),
text,
completed: false
};
this.setState(state => ({
todos: [...state.todos, todo]
}));
}
toggleTodo(id) {
this.setState(state => ({
todos: state.todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
}));
}
setFilter(filter) {
this.setState({ filter });
}
getVisibleTodos() {
const { todos, filter } = this.getState();
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}
}
// 사용
const todoStore = new TodoStore();
// 상태 변경 구독
const unsubscribe = todoStore.subscribe(state => {
console.log('상태 업데이트:', state);
});
todoStore.addTodo('JavaScript 공부하기');
todoStore.addTodo('운동하기');
todoStore.toggleTodo(todoStore.getState().todos[0].id);
console.log(todoStore.getVisibleTodos());
// 구독 취소
unsubscribe();
4. 싱글톤 패턴: API 클라이언트
class ApiClient {
static instance = null;
constructor(baseURL, options = {}) {
// 싱글톤: 이미 인스턴스가 있으면 반환
if (ApiClient.instance) {
return ApiClient.instance;
}
this.baseURL = baseURL;
this.defaultHeaders = options.headers || {};
this.timeout = options.timeout || 5000;
this.interceptors = {
request: [],
response: []
};
ApiClient.instance = this;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers
}
};
// Request 인터셉터 실행
for (const interceptor of this.interceptors.request) {
await interceptor(config);
}
try {
const response = await fetch(url, config);
// Response 인터셉터 실행
for (const interceptor of this.interceptors.response) {
await interceptor(response);
}
return await response.json();
} catch (error) {
console.error('API 요청 실패:', error);
throw error;
}
}
get(endpoint, options) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, data, options) {
return this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
}
addRequestInterceptor(fn) {
this.interceptors.request.push(fn);
}
addResponseInterceptor(fn) {
this.interceptors.response.push(fn);
}
static getInstance() {
if (!ApiClient.instance) {
throw new Error('ApiClient가 초기화되지 않았습니다.');
}
return ApiClient.instance;
}
}
// 앱 초기화 시 한 번만 생성
const api = new ApiClient('https://api.example.com', {
headers: {
'Authorization': 'Bearer token123'
}
});
// 인터셉터 추가
api.addRequestInterceptor(async (config) => {
console.log('요청 시작:', config);
});
// 다른 모듈에서도 같은 인스턴스 사용
const api2 = new ApiClient('https://different-url.com');
console.log(api === api2); // true (같은 인스턴스!)
// 또는 getInstance()로 가져오기
const api3 = ApiClient.getInstance();
console.log(api === api3); // true
5. 풀링(Pooling) 패턴: 객체 재사용
class ObjectPool {
constructor(createFn, resetFn, initialSize = 10) {
this.createFn = createFn;
this.resetFn = resetFn;
this.available = [];
this.inUse = new Set();
// 초기 객체 생성
for (let i = 0; i < initialSize; i++) {
this.available.push(this.createFn());
}
}
acquire() {
let obj;
if (this.available.length > 0) {
obj = this.available.pop();
} else {
obj = this.createFn();
}
this.inUse.add(obj);
return obj;
}
release(obj) {
if (!this.inUse.has(obj)) {
console.warn('이 객체는 풀에서 가져온 것이 아닙니다.');
return;
}
this.inUse.delete(obj);
this.resetFn(obj);
this.available.push(obj);
}
getStats() {
return {
available: this.available.length,
inUse: this.inUse.size,
total: this.available.length + this.inUse.size
};
}
}
// 파티클 시스템에서 활용
class Particle {
constructor() {
this.x = 0;
this.y = 0;
this.vx = 0;
this.vy = 0;
this.life = 1.0;
this.isActive = false;
}
init(x, y, vx, vy) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.life = 1.0;
this.isActive = true;
}
update(deltaTime) {
if (!this.isActive) return;
this.x += this.vx * deltaTime;
this.y += this.vy * deltaTime;
this.life -= deltaTime;
if (this.life <= 0) {
this.isActive = false;
}
}
}
// 파티클 풀 생성
const particlePool = new ObjectPool(
() => new Particle(),
(particle) => {
particle.isActive = false;
particle.life = 1.0;
},
100
);
// 사용
function createExplosion(x, y) {
const particles = [];
for (let i = 0; i < 50; i++) {
const particle = particlePool.acquire();
const angle = (Math.PI * 2 * i) / 50;
const speed = 100 + Math.random() * 100;
particle.init(
x,
y,
Math.cos(angle) * speed,
Math.sin(angle) * speed
);
particles.push(particle);
}
return particles;
}
// 업데이트 루프
function update(deltaTime) {
particlePool.inUse.forEach(particle => {
particle.update(deltaTime);
if (!particle.isActive) {
particlePool.release(particle);
}
});
}
console.log(particlePool.getStats()); // { available: 100, inUse: 0, total: 100 }
const explosion = createExplosion(400, 300);
console.log(particlePool.getStats()); // { available: 50, inUse: 50, total: 100 }
타입 체크: instanceof vs typeof vs Object.prototype.toString
// typeof: 원시 타입 체크
console.log(typeof 42); // 'number'
console.log(typeof 'hello'); // 'string'
console.log(typeof true); // 'boolean'
console.log(typeof undefined); // 'undefined'
console.log(typeof null); // 'object' (역사적 버그)
console.log(typeof {}); // 'object'
console.log(typeof []); // 'object' (구분 안됨)
console.log(typeof function(){}); // 'function'
// instanceof: 인스턴스와 생성자 관계 체크
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
// 한계: 다른 컨텍스트(iframe)에서는 실패
// instanceof는 원시값에 사용 불가
console.log(42 instanceof Number); // false
console.log(new Number(42) instanceof Number); // true
// Object.prototype.toString: 가장 정확한 타입 체크
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1);
}
console.log(getType(42)); // 'Number'
console.log(getType('hello')); // 'String'
console.log(getType(true)); // 'Boolean'
console.log(getType(null)); // 'Null'
console.log(getType(undefined)); // 'Undefined'
console.log(getType({})); // 'Object'
console.log(getType([])); // 'Array'
console.log(getType(new Date())); // 'Date'
console.log(getType(/regex/)); // 'RegExp'
console.log(getType(function(){})); // 'Function'
// 커스텀 타입도 체크 가능
class MyClass {
get [Symbol.toStringTag]() {
return 'MyClass';
}
}
console.log(getType(new MyClass())); // 'MyClass'
// 실용적인 타입 체크 유틸리티
const is = {
array: value => Array.isArray(value),
object: value => value !== null && typeof value === 'object' && !Array.isArray(value),
string: value => typeof value === 'string',
number: value => typeof value === 'number' && !isNaN(value),
function: value => typeof value === 'function',
null: value => value === null,
undefined: value => value === undefined,
date: value => value instanceof Date && !isNaN(value),
regexp: value => value instanceof RegExp,
promise: value => value instanceof Promise || (value && typeof value.then === 'function')
};
console.log(is.array([1, 2, 3])); // true
console.log(is.object({ a: 1 })); // true
console.log(is.number(NaN)); // false
console.log(is.promise(Promise.resolve())); // true
참고 자료
MDN 공식 문서
-
[Classes - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) -
[new operator - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new) -
[instanceof - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) -
[Object.create() - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) -
[Inheritance and the prototype chain - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) -
[this - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this)
심화 학습
- You Don’t Know JS: this & Object Prototypes - Kyle Simpson
- JavaScript.info - Class basic syntax
- JavaScript.info - Prototypes
- Understanding JavaScript Constructors
디자인 패턴
- JavaScript Design Patterns - Patterns.dev
- Singleton Pattern in JavaScript
- Object Pool Pattern - Game Programming Patterns
성능 최적화
-
[Memory Management - JavaScript MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management) - JavaScript Performance Optimization Tips
- V8 Blog: Understanding Classes and Prototypes
댓글