newclass
2025. 3. 30. 22:35
JavaScript에서 객체와 배열은 데이터를 구조화하고 관리하는 핵심 데이터 타입입니다. 이 챕터에서는 객체와 배열의 생성, 접근, 조작 방법과 주요 개념들을 살펴보겠습니다.
객체
객체는 관련된 데이터와 함수(메서드)를 하나의 단위로 그룹화합니다. JavaScript는 객체 지향 언어이며, 거의 모든 것이 객체로 취급됩니다.
객체 리터럴
객체 리터럴은 중괄호 {}를 사용하여 객체를 생성하는 가장 간단한 방법입니다.
// 기본 객체 생성
const person = {
name: "홍길동",
age: 25,
job: "개발자",
isMarried: false
};
// 빈 객체 생성 후 속성 추가
const car = {};
car.brand = "현대";
car.model = "소나타";
car.year = 2023;
// 중첩 객체
const student = {
name: "김철수",
age: 20,
grades: {
math: 90,
english: 85,
science: 95
},
hobbies: ["독서", "게임", "수영"]
};
객체 속성 접근
객체 속성에 접근하는 두 가지 방법이 있습니다: 점 표기법과 대괄호 표기법
const user = {
firstName: "홍",
lastName: "길동",
"user-age": 25, // 하이픈을 포함한 속성명
"1stPlace": true // 숫자로 시작하는 속성명
};
// 점 표기법 (dot notation)
console.log(user.firstName); // "홍"
console.log(user.lastName); // "길동"
// 대괄호 표기법 (bracket notation)
console.log(user["firstName"]); // "홍"
console.log(user["lastName"]); // "길동"
// 특수 문자나 공백이 있는 속성명은 대괄호 표기법만 가능
console.log(user["user-age"]); // 25
// console.log(user.user-age); // 오류 발생
console.log(user["1stPlace"]); // true
// console.log(user.1stPlace); // 오류 발생
// 변수를 사용한 동적 속성 접근
const propertyName = "firstName";
console.log(user[propertyName]); // "홍"
객체 속성 수정 및 삭제
const product = {
name: "노트북",
price: 1500000,
brand: "삼성"
};
// 속성 수정
product.price = 1400000;
product["brand"] = "LG";
console.log(product);
// { name: "노트북", price: 1400000, brand: "LG" }
// 새 속성 추가
product.inStock = true;
product["discount"] = 0.1;
console.log(product);
// { name: "노트북", price: 1400000, brand: "LG", inStock: true, discount: 0.1 }
// 속성 삭제
delete product.discount;
console.log(product);
// { name: "노트북", price: 1400000, brand: "LG", inStock: true }
// 속성 존재 여부 확인
console.log("price" in product); // true
console.log("discount" in product); // false
console.log(product.hasOwnProperty("brand")); // true
메서드
객체의 속성으로 할당된 함수를 메서드라고 합니다.
// 메서드가 있는 객체
const calculator = {
result: 0,
add: function(num) {
this.result += num;
},
subtract: function(num) {
this.result -= num;
},
multiply: function(num) {
this.result *= num;
},
divide: function(num) {
if (num !== 0) {
this.result /= num;
} else {
console.error("0으로 나눌 수 없습니다.");
}
},
clear: function() {
this.result = 0;
},
getResult: function() {
return this.result;
}
};
calculator.add(5);
calculator.multiply(2);
calculator.subtract(3);
console.log(calculator.getResult()); // 7
// ES6 단축 구문 (메서드 정의 간소화)
const modernCalculator = {
result: 0,
add(num) {
this.result += num;
return this; // 메서드 체이닝을 위한 this 반환
},
subtract(num) {
this.result -= num;
return this;
},
getResult() {
return this.result;
}
};
// 메서드 체이닝
console.log(modernCalculator.add(10).subtract(4).getResult()); // 6
this 키워드
메서드 내에서 this 키워드는 해당 메서드를 호출한 객체를 가리킵니다.
// 기본 this 동작
const person = {
name: "홍길동",
sayHello: function() {
console.log(`안녕하세요, ${this.name}입니다!`);
}
};
person.sayHello(); // "안녕하세요, 홍길동입니다!" 출력
// this의 동적 바인딩
const greeting = person.sayHello;
// greeting(); // "안녕하세요, undefined입니다!" 출력 (this가 전역 객체를 가리킴)
// call, apply, bind를 사용한 this 바인딩
const anotherPerson = { name: "김철수" };
// call 사용
person.sayHello.call(anotherPerson); // "안녕하세요, 김철수입니다!" 출력
// apply 사용 (배열로 인자 전달)
function introduce(age, job) {
console.log(`안녕하세요, ${this.name}입니다. ${age}살이고, ${job}입니다.`);
}
introduce.apply(person, [25, "개발자"]); // "안녕하세요, 홍길동입니다. 25살이고, 개발자입니다." 출력
// bind 사용 (새 함수 생성)
const introducePerson = introduce.bind(person);
introducePerson(25, "개발자"); // "안녕하세요, 홍길동입니다. 25살이고, 개발자입니다." 출력
// 화살표 함수에서의 this
const user = {
name: "이영희",
regularFunction: function() {
console.log(`일반 함수: ${this.name}`);
},
arrowFunction: () => {
console.log(`화살표 함수: ${this.name}`);
},
nestedFunctions: function() {
// 일반 함수의 내부 함수에서 this 문제
function innerRegular() {
console.log(`중첩 일반 함수: ${this.name}`); // this는 전역 객체 (또는 undefined)
}
// 화살표 함수로 문제 해결
const innerArrow = () => {
console.log(`중첩 화살표 함수: ${this.name}`); // this는 nestedFunctions의 this와 동일
};
innerRegular();
innerArrow();
}
};
user.regularFunction(); // "일반 함수: 이영희" 출력
user.arrowFunction(); // "화살표 함수: " 출력 (화살표 함수는 자신의 this를 가지지 않음)
user.nestedFunctions();
// "중첩 일반 함수: " 출력
// "중첩 화살표 함수: 이영희" 출력
객체 생성 패턴
1. 생성자 함수
생성자 함수는 객체를 생성하고 초기화하는 특별한 함수입니다. 관례적으로 대문자로 시작합니다.
// 생성자 함수
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayHello = function() {
console.log(`안녕하세요, ${this.name}입니다!`);
};
}
// new 연산자로 객체 생성
const person1 = new Person("홍길동", 25, "개발자");
const person2 = new Person("김철수", 30, "디자이너");
console.log(person1.name); // "홍길동"
person2.sayHello(); // "안녕하세요, 김철수입니다!" 출력
// 프로토타입을 사용한 메서드 정의
// (모든 인스턴스가 공유하므로 메모리 효율적)
Person.prototype.introduce = function() {
console.log(`제 이름은 ${this.name}이고, ${this.age}살이며, ${this.job}입니다.`);
};
person1.introduce(); // "제 이름은 홍길동이고, 25살이며, 개발자입니다." 출력
person2.introduce(); // "제 이름은 김철수이고, 30살이며, 디자이너입니다." 출력
2. 클래스 (ES6)
ES6에서 도입된 클래스 문법은 기존 프로토타입 기반 상속을 더 명확하게 표현합니다.
// 클래스 선언
class Animal {
// 생성자
constructor(name, species) {
this.name = name;
this.species = species;
}
// 메서드
makeSound() {
console.log("Some generic sound");
}
// 정적 메서드
static isAnimal(obj) {
return obj instanceof Animal;
}
}
// 클래스 사용
const dog = new Animal("바둑이", "개");
console.log(dog.name); // "바둑이"
dog.makeSound(); // "Some generic sound" 출력
// 상속
class Dog extends Animal {
constructor(name, breed) {
super(name, "개"); // 부모 클래스의 생성자 호출
this.breed = breed;
}
// 메서드 오버라이딩
makeSound() {
console.log("멍멍!");
}
// 추가 메서드
fetch() {
console.log(`${this.name}이(가) 공을 물어옵니다.`);
}
}
const myDog = new Dog("초코", "리트리버");
console.log(myDog.name); // "초코"
console.log(myDog.species); // "개"
myDog.makeSound(); // "멍멍!" 출력
myDog.fetch(); // "초코이(가) 공을 물어옵니다." 출력
// 정적 메서드 사용
console.log(Animal.isAnimal(myDog)); // true
3. 객체 생성 메서드
Object.create()를 사용하면 지정된 프로토타입 객체와 속성을 가진 새 객체를 생성할 수 있습니다.
// 프로토타입 객체
const personProto = {
sayHello: function() {
console.log(`안녕하세요, ${this.name}입니다!`);
},
introduce: function() {
console.log(`저는 ${this.age}살이고, ${this.job}입니다.`);
}
};
// Object.create()로 객체 생성
const person1 = Object.create(personProto);
person1.name = "홍길동";
person1.age = 25;
person1.job = "개발자";
person1.sayHello(); // "안녕하세요, 홍길동입니다!" 출력
person1.introduce(); // "저는 25살이고, 개발자입니다." 출력
// 속성 정의와 함께 생성
const person2 = Object.create(personProto, {
name: { value: "김철수", writable: true, enumerable: true },
age: { value: 30, writable: true, enumerable: true },
job: { value: "디자이너", writable: true, enumerable: true }
});
person2.sayHello(); // "안녕하세요, 김철수입니다!" 출력
객체 속성 심화
속성 설명자(Property Descriptor)
Object.defineProperty() 메서드를 사용하면 속성의 특성을 세밀하게 제어할 수 있습니다.
const product = {};
// 속성 정의
Object.defineProperty(product, "name", {
value: "스마트폰",
writable: true, // 값 변경 가능 여부
enumerable: true, // for...in 등에서 열거 가능 여부
configurable: true // 속성 삭제 및 속성 설명자 변경 가능 여부
});
// 읽기 전용 속성
Object.defineProperty(product, "id", {
value: "P12345",
writable: false, // 읽기 전용
enumerable: true,
configurable: false // 삭제 불가
});
product.name = "태블릿"; // 변경 가능
console.log(product.name); // "태블릿"
product.id = "P67890"; // 변경 시도해도 무시됨 (엄격 모드에서는 오류)
console.log(product.id); // "P12345"
// 열거되지 않는 속성
Object.defineProperty(product, "secretCode", {
value: "XYZ789",
enumerable: false // for...in 등에서 보이지 않음
});
for (const key in product) {
console.log(key); // "name", "id" 출력 (secretCode는 출력되지 않음)
}
// 여러 속성 한번에 정의
Object.defineProperties(product, {
price: {
value: 1000000,
writable: true,
enumerable: true,
configurable: true
},
brand: {
value: "삼성",
writable: true,
enumerable: true,
configurable: true
}
});
접근자 속성(Getter/Setter)
접근자 속성은 값을 가져오거나 설정할 때 함수를 호출합니다.
const person = {
firstName: "홍",
lastName: "길동",
// getter
get fullName() {
return `${this.lastName}${this.firstName}`;
},
// setter
set fullName(name) {
const parts = name.split(" ");
if (parts.length === 2) {
this.lastName = parts[0];
this.firstName = parts[1];
} else {
throw new Error("잘못된 이름 형식입니다.");
}
}
};
// getter 사용
console.log(person.fullName); // "길동홍" 출력
// setter 사용
person.fullName = "김 철수";
console.log(person.firstName); // "철수"
console.log(person.lastName); // "김"
console.log(person.fullName); // "김철수" 출력
// defineProperty로 접근자 속성 추가
const product = {
price: 1000,
discount: 0.1
};
Object.defineProperty(product, "discountedPrice", {
get: function() {
return this.price * (1 - this.discount);
},
set: function(value) {
this.discount = 1 - (value / this.price);
}
});
console.log(product.discountedPrice); // 900 출력
product.discountedPrice = 800;
console.log(product.discount); // 0.2 출력
객체 순회 및 조작
const person = {
name: "홍길동",
age: 25,
job: "개발자",
hobbies: ["독서", "여행"]
};
// for...in 루프로 속성 순회
for (const key in person) {
console.log(`${key}: ${person[key]}`);
}
// Object.keys() - 속성 이름 배열 반환
const keys = Object.keys(person);
console.log(keys); // ["name", "age", "job", "hobbies"]
// Object.values() - 속성 값 배열 반환
const values = Object.values(person);
console.log(values); // ["홍길동", 25, "개발자", ["독서", "여행"]]
// Object.entries() - [키, 값] 쌍의 배열 반환
const entries = Object.entries(person);
console.log(entries);
// [["name", "홍길동"], ["age", 25], ["job", "개발자"], ["hobbies", ["독서", "여행"]]]
// Object.assign() - 객체 복사 및 병합
const defaults = { age: 18, country: "한국" };
const newPerson = Object.assign({}, defaults, person);
console.log(newPerson);
// { age: 25, country: "한국", name: "홍길동", job: "개발자", hobbies: ["독서", "여행"] }
// 전개 연산자(Spread Operator) 사용 (ES6)
const spreadPerson = { ...defaults, ...person };
console.log(spreadPerson);
// { age: 25, country: "한국", name: "홍길동", job: "개발자", hobbies: ["독서", "여행"] }
객체 불변성
객체의 불변성을 보장하는 몇 가지 방법을 알아보겠습니다.
const user = {
name: "홍길동",
age: 25,
address: {
city: "서울",
district: "강남구"
}
};
// Object.freeze() - 객체를 완전히 동결 (변경 불가)
Object.freeze(user);
user.name = "김철수"; // 변경되지 않음
user.newProp = "새 속성"; // 추가되지 않음
delete user.age; // 삭제되지 않음
console.log(user.name); // "홍길동"
// 중첩 객체는 동결되지 않음
user.address.city = "부산"; // 변경됨
console.log(user.address.city); // "부산"
// 깊은 동결 함수 구현
function deepFreeze(obj) {
// 객체가 아니거나 이미 동결된 경우 바로 반환
if (typeof obj !== 'object' || obj === null || Object.isFrozen(obj)) {
return obj;
}
// 객체의 속성들을 재귀적으로 동결
for (const prop of Object.getOwnPropertyNames(obj)) {
const value = obj[prop];
obj[prop] = deepFreeze(value);
}
return Object.freeze(obj);
}
const user2 = {
name: "김철수",
age: 30,
address: {
city: "서울",
district: "서초구"
}
};
deepFreeze(user2);
user2.address.city = "부산"; // 변경되지 않음
console.log(user2.address.city); // "서울"
// Object.seal() - 객체를 봉인 (속성 추가/삭제 불가, 값 변경은 가능)
const product = { name: "노트북", price: 1500000 };
Object.seal(product);
product.brand = "삼성"; // 추가되지 않음
delete product.name; // 삭제되지 않음
product.price = 1400000; // 변경됨
console.log(product); // { name: "노트북", price: 1400000 }
// Object.preventExtensions() - 객체 확장 금지 (속성 추가 불가, 삭제/변경은 가능)
const car = { brand: "현대", model: "아반떼" };
Object.preventExtensions(car);
car.year = 2023; // 추가되지 않음
delete car.model; // 삭제됨
car.brand = "기아"; // 변경됨
console.log(car); // { brand: "기아" }