본문 바로가기
자바스크립트(JavaScript)/자바스크립트

[JavaScript] 프로퍼티(property) 뿌수기

by yerica 2024. 12. 11.

프로퍼티(property)란?

자바스크립트에서 객체엔 프로퍼티(property)가 저장된다.

프로퍼티는 객체의 핵심 구성 요소로, 데이터(key, value)와 동작(함수)를 담을 수 있다.

객체는 여러 개의 프로퍼티를 가질 수 있으며, 각 프로퍼티는 객체의 특성을 나타낸다.


프로퍼티의 종류

1. 데이터 프로퍼티 (Data Property)

일반적인 키-값 형태의 프로퍼티로, 값을 읽거나 쓸 수 있다.
const obj = {
   name: "John",      // 키: "name", 값: "John"
   age: 30,           // 키: "age", 값: 30
   isStudent: false   // 키: "isStudent", 값: false
};

console.log(obj.name); // "John"
obj.age = 31;          // 값 변경
console.log(obj.age);  // 31

 

 

2. 접근자 프로퍼티(Accessor Property)

값을 저장하지 않고, 함수로 계산된 값을 반환하거나 설정할 때 사용한다.
get과 set 키워드로 정의한다.
const person = {
   firstName: "John",
   lastName: "Doe",
   get fullName() {
      return `${this.firstName} ${this.lastName}`;
   },
   set fullName(name) {
      const names = name.split(" ");
      this.firstName = names[0];
      this.lastName = names[1];
   }
};

console.log(person.fullName); // "John Doe" (getter 호출)
person.fullName = "Jane Smith"; // setter 호출
console.log(person.firstName); // "Jane"
console.log(person.lastName);  // "Smith"

프로퍼티의 구성 요소

1. 키(key)

프로퍼티의 이름으로 문자열 또는 심볼만 가능한데, 문자열로 작성된 키는 따옴표로 감쌀 수 있지만 생략도 가능하다.

 

2. 값(value)

프로퍼티에 할당된 데이터로, JavaScript의 모든 데이터 유형을 포함할 수 있다.
데이터 프로퍼티에만 존재하고, 접근자 프로퍼티에는 존재하지 않는다.

 

3. 플레그(flag)

객체 프로퍼티는 프로퍼티 플레그라고 불리는 특별한 속성 세가지를 갖는다.
writable(쓰기가능), enumerable(열거가능),  configurable(설정가능) 이 세가지 플레그는 기본적으로 true 값을 갖는다.

 

4. 획득(get)

인수가 없는 함수로, 프로퍼티를 읽을 때 동작한다.
접근자 프로퍼티에만 존재한다.

 

5. 설정(set)

인수가 하나인 함수로, 프로퍼티에 값을 쓸 때 호출된다.
접근자 프로퍼티에만 존재한다.

 

데이터 프로퍼티접근자 프로퍼티의 설명자 보유 여부 

설명자 데이터 프로퍼티(data property) 접근자 프로퍼티(accessor property)
키(key) 보유 O 보유 O
값(value) 보유 O 보유 X
플레그
(flag)
쓰기가능
(writable)
보유 O 보유 X
열거가능
(enumerable)
보유 O 보유 X
(설정가능)
configurable
보유 O 보유 X
획득(get) 보유 X 보유 O
설정(set) 보유 X 보유 O

프로퍼티 플레그

1. writable (쓰기가능)

프로퍼티의 writable이 true면 값을 수정할 수 있고, false면 읽기만 가능하다.
데이터 프로퍼티에만 존재하고, 접근자 프로퍼티에는 존재하지 않는다.
let user = {
  name: "John"
};

// writable을 false로 설정
Object.defineProperty(user, "name", {
  writable: false
});

user.name = "Pete"; // Error: Cannot assign to read only property 'name'​

user 객체에 defineProperty 메서드를 사용하여 writable을 false로 설정했다.
이후 user의 name 프로퍼티는 값의 수정이 불가능해졌기 때문에 에러를 발생한다.
(에러는 엄격보드에서만 발생한다. 비엄격모드에서는 값이 변경되지 않지만 에러없이 그냥 무시된다.)

 

2. enumerable (열거가능)

프로퍼티의 enumerable이 true 상태면 반복문을 사용해 나열할 수 있다. false면 반복문을 사용해 나열할 수 없다.
let user = {
  name: "John",
  toString() {
    return this.name;
  }
};

//커스텀 toString은 for...in을 사용해 열거할 수 있다.
for (let key in user) alert(key); // name, toString​

// enumerable 플래그 값을 false로 설정
Object.defineProperty(user, "toString", {
  enumerable: false
});

// 이제 for...in을 사용해 toString을 열거할 수 없게 되었다.
for (let key in user) alert(key); // name

// 열거가 불가능한 프로퍼티는 Object.keys에도 배제된다.
alert(Object.keys(user)); // name

user에 toString()이라는 메서드를 추가했다.
기본적으로 enumerable이 true 인 상태이기 때문에 열거하게 되면 name 과 toString이 모두 뜬다.
하지만 값을 false로 바꿀 경우 첫번째 프로퍼티만 출력되고, Object.key에서도 배제되는 것을 확인할 수 있다.

 

3. configurable (설정가능)

configurable이 true면 프로퍼티 삭제나 플래그 수정이 가능하고, false면 프로퍼티 삭제와 플래그 수정이 불가능하다.
보통 프로퍼티는 true가 기본 값이나 몇몇 내장 객체나 프로퍼티에는 false로 기본 설정되어있다.
어떤 프로퍼티의 configurable 플래그가 false로 설정되어 있다면 해당 프로퍼티는 객체에서 지울 수 없다.
(내장 객체 Math의 PI 프로퍼티가 대표적인 예이다. 이 프로퍼티는 쓰기와 열거, 구성이 불가능하다.)
또한, configurable 플래그는 false로 설정하면 이후 defineProperty를 써도 true로 되돌릴 수 없다.
이런 특징을 이용하면 "영원히 변경할 수 없는" 프로퍼티를 만들 수 있다.
  1. configurable 플래그를 수정할 수 없음
  2. enumerable 플래그를 수정할 수 없음.
  3. writable: false의 값을 true로 바꿀 수 없음(true를 false로 변경하는 것은 가능함).
  4. 접근자 프로퍼티 get/set을 변경할 수 없음(새롭게 만드는 것은 가능함).
let user = { };

Object.defineProperty(user, "name", {
  value: "John",
  writable: false,
  configurable: false
});

// user.name 프로퍼티의 값이나 플래그를 변경할 수 없다.
// 아래와 같이 변경하려고 하면 에러가 발생한다.
//   user.name = "Pete"
//   delete user.name
//   Object.defineProperty(user, "name", { value: "Pete" })
Object.defineProperty(user, "name", {writable: true}); // Error​

configurable 플래그가 false이더라도 writable 플래그가 true이면 프로퍼티 값을 변경할 수 있다.
configurable: false는 플래그 값 변경이나 프로퍼티 삭제를 막기 위해 만들어졌지, 프로퍼티 값 변경을 막기 위해 만들어진 게 아니다.

프로퍼티 조작 방법

1. 프로퍼티 추가

점 표기법(.) 또는 대괄호 표기법([ ])으로 새 키-값 쌍을 추가할 수 있다.
const obj = {};
obj.name = "Alice";         // 점 표기법
obj["age"] = 25;            // 대괄호 표기법
console.log(obj);           // { name: "Alice", age: 25 }

 

2. 프로퍼티 삭제

delete 연산자를 사용하여 프로퍼티를 제거할 수 있다.
const obj = { name: "Alice", age: 25 };
delete obj.age;
console.log(obj); // { name: "Alice" }

 

3. 프로퍼티 존재 확인

in 연산자 또는 hasOwnProperty 메서드로 특정 프로퍼티가 객체에 존재하는지 확인할 수 있다.
const obj = { name: "Alice" };
console.log("name" in obj);            // true
console.log(obj.hasOwnProperty("age"));// false

Getter / Setter

getter(획득자)와 setter(설정자)는 접근자 프로퍼티(accessor property)에서 사용되는 개념이다.

접근자 프로퍼티의 본질은 함수이다. 그렇기 때문에 함수에서 값을 받고 세팅하는 과정이 필요한데 이를 getter(획득자)와 setter(설정자)라는 역할을 부여해 실행한다.

객체리터럴 안에서 get과 set 키워드를 붙여 사용하는데, 외부 코드에서는 함수가 아닌 일반적인 프로퍼티처럼 보인다.

1) Getter
1. 선언 방법
getter 메서드는 객체 안에서 get 키워드를 붙여 사용한다.
get 키워드가 붙은 프로퍼티는 get이 붙어있는 해당 프로퍼티를 호출 했을 때 실행된다.

const 객체명 = {
	get 프로퍼티명 : function() {
    	   console.log("hello!");
	}
}​

객체명.프로퍼티명 // hello!​

객체명.프로퍼티명으로 getter 메서드로 설정된 함수를 불러왔다. 
함수 안에 console.log("hello!")라는 코드가 실행되어 콘솔에는 hello!가 출력된다.

2. 호출 방식
일반 함수를 호출할 때, 우리는 함수명과 소괄호를 붙여 함수를 호출한다.
하지만 접근자 프로퍼티는 함수 호출 없이 일반 프로퍼티처럼 호출한다.

// 일반 함수 정의
const obj = {
  myProperty() {
    return 'Hello!';
  }
};
obj.myProperty() // 일반 함수 호출

// 접근자 프로퍼티 정의
const obj = {
  get myProperty() {
    return 'Hello!';
  }
};
obj.myProperty // 접근자 프로퍼티 호출​

자동으로 get 키워드가 붙은 객체의 메서드를 불러와 실행하는 것이기 때문에 어떠한 인수, 값(value)을 받지 않는다.(get과 value를 동시에 설정하면 에러가 발생한다.)
2) Setter
1. 선언 방법
setter 메서드는 객체 안에서 set 키워드를 붙여 사용한다.
set  키워드가 붙으면 프로퍼티에 값을 할당하려고 할 때 실행된다.

const 객체명 = {
	set 프로퍼티명 : function(value) {
    	   console.log(value);
	}
}​

객체명.프로퍼티명 = "hello!" // hello!​​

객체명.프로퍼티명에 "hello!" 라는 값(value)를 넣었다.
setter 메서드로 설정된 함수는 무조건 값(value)인수를 한개를 받는다.
인수를 받으면 set 키워드로 정의된 메서드는 매개변수로 값을 받고, 함수 안의 내용을 실행한다.
결과적으로 value에 "hello!"가 들어가기 때문에 콘솔에 hello! 가 출력되는 것이다.
3) getter/setter 함께 사용
getter와 setter 메서드를 구현하면 객체엔 get 과 set 이 붙은 프로퍼티명으로 '가상'의 프로퍼티가 생성된다.
가상의 프로퍼티는 읽고 쓸 순 있지만 실제로는 존재하지 않는다.

getter와 setter를 함께 사용할 경우 순서는 다음과 같다.
set 키워드가 붙은 메서드의 value 설정 → set 메서드 실행 → get 메서드 호출 → get 메서드 실행

const BankAccount = {
   myMoney: 10000,

   get spend() {
      return this.myMoney
   },

   set spend(amount) {
      if (amount >= 1000) {
         if (this.myMoney >= amount) {
            this.myMoney -= amount // 잔액에서 출금액 차감
         } else {
            console.log('잔액이 부족합니다.')
         }
      } else {
         console.log('1,000원 부터 출금 가능합니다.')
      }
   },
}

// 초기 잔액 설정
BankAccount.spend = 1000

// 잔액 확인
console.log(BankAccount.spend)​

myMoney를 10,000으로 설정한뒤, getter/settter를 활용해 출금하는 객체를 만들어보았다.
BankAccount.spend = 1000으로 set 메서드가 실행되며 내부에서 if 문을 거쳐 결과값이 나온다.
그렇게 나온 결과값은 set spend 에 저장되어있는데, 이를 BankAccount.spend 를 통해 호출하면 get 메서드가 실행되어 좀전에 set 메서드에서 처리해놓은 결과물로 함수를 처리한다.

getter와 setter는 데이터 프로퍼티의 행동과 값을 원하는대로 조정할 수 있게 한다는 점에서 유용하다.

형식을 클래스로 변경할 경우 다음과 같다.

class BankAccount {
   constructor(myMoney) {
      this.myMoney = myMoney
   }
   get spend() {
      return this.myMoney
   }
   set spend(amount) {
      if (amount >= 1000) {
         if (this.myMoney >= amount) {
            this.myMoney -= amount // 잔액에서 출금액 차감
         } else {
            console.log('잔액이 부족합니다.')
         }
      } else {
         console.log('1,000원 부터 출금 가능합니다.')
      }
   }
}
const user = new BankAccount(10000)
user.spend = 1000
console.log(user.spend)​

 


속성 관련 메서드를 통해 프로퍼티 설정

1. Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor 메서드를 사용하면 특정 프로퍼티에 대한 정보를 모두 얻을 수 있다.
이 메서드는 주어진 객체의 모든 속성들의 설명자(descriptor)들을 반환한다.
Object.getOwnPropertyDescriptor(정보를 얻고자하는 객체, '정보를 얻고자 하는 객체 내 프로퍼티');
let user = {
   name: 'park'
}

console.log(Object.getOwnPropertyDescriptor(user, 'name'))
// { value: 'park', writable: true, enumerable: true, configurable: true }​

user의 name 이라는 프로퍼티로 예시를 들어보았다.
name이 가지고 있는 value와 세개의 프로퍼티 플래그가 출력된다.

 

2. Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors에서는 객체안의 모든 프로퍼티의 속성이 한번에 출력된다.
Object.getOwnPropertyDescriptor(정보를 얻고자하는 객체);
let user = {
   name: 'park',
   height: 180,
   weight: 70,
   성격: '밝음',
}

console.log(Object.getOwnPropertyDescriptors(user))

/*
{
  name: {
    value: 'park',
    writable: true,
    enumerable: true,
    configurable: true
  },
  height: { value: 180, writable: true, enumerable: true, configurable: true },
  weight: { value: 70, writable: true, enumerable: true, configurable: true },
  '성격': { value: '밝음', writable: true, enumerable: true, configurable: true }
}
*/

 

3. Object.defineProperty()

Object.defineProperty 메서드는 객체에 새로운 속성을 직접 정의하거나 이미 존재하는 속성을 수정 한 후, 해당 객체를 반환한다.

defineProperty메서드는 객체에 해당 프로퍼티가 있으면 플래그를 원하는 대로 변경해준다.
프로퍼티가 없으면 인수로 넘겨받은 정보를 이용해 새로운 프로퍼티를 만든다.
이때, 플래그 정보가 없으면 플래그 값은 자동으로 false가 된다.

Object.defineProperty(속성을 적용하고 싶은 객체, '적용하고 싶은 프로퍼티', {적용하고자 하는 속성: 값})

Object.defineProperty 메서드의 괄호 안에는 적용하고 싶은 객체와 프로퍼티, 속성이 들어간다.
적용하고싶은 객체명을 적고, 적용하고 싶은 객체 안의 프로퍼티명은 따옴표로 감싸서 작성한 뒤,
적용하고자 하는 속성을 배열로 감싸 입력하면된다.

let user = {
   name: 'park',
   height: 180,
   weight: 70,
   성격: '밝음',
}

// like 라는 없는 프로퍼티 생성
Object.defineProperty(user, 'like', { value: 'rice', writable: true })

// 현재 존재하는 프로퍼티인 height의 값 수정
Object.defineProperty(user, 'height', { value: 200 })

// 출력
console.log(Object.getOwnPropertyDescriptors(user))
/*
{
  name: {
    value: 'park',
    writable: true,
    enumerable: true,
    configurable: true
  },
  height: { value: 200, writable: true, enumerable: true, configurable: true },
  weight: { value: 70, writable: true, enumerable: true, configurable: true },
  '성격': { value: '밝음', writable: true, enumerable: true, configurable: true },
  like: {
    value: 'rice',
    writable: true,
    enumerable: false,
    configurable: false
  }
}
*/​


위에서 hegiht 는 기존에 존재하던 프로퍼티였기 때문에 value값만 수정되고 나머지는 모두 true이다.
하지만, like는 기존에 존재하지 않던 프로퍼티였기 때문에, defineProperty 메서드에서 입력해준 속성을 제외하고 모든 속성이 false 인 것을 확인할 수 있다.

defineProperty로 새로운 프로퍼티를 추가하고 속성 부여해보기
더보기
더보기

사용자에게 이름과 생일을 받았을 때, 나이를 계산하는 생성자 함수를 만들어보겠다.

 

function User(name, birthday) {
   this.name = name
   this.birthday = birthday
   this.age = function () {
      let todayYear = new Date().getFullYear()
      return todayYear - new Date(this.birthday).getFullYear()
   }
}

let john = new User('John', '1992-12-20')

console.log(john.birthday) // 1992-12-20
console.log(john.age()) // 32

 

 위의 예시처럼 john.age 에 익명함수를 넣어 

 

 

4. Object.defineProperties()

Object.defineProperties() 메서드는 객체에 새로운 속성을 정의하거나 기존의 속성을 수정하고, 그 객체를 반환한다.
Object.defineProperty(속성을 적용하고 싶은 객체, {
   적용하고자 하는 프로퍼티 : {
      속성 : 값,
      속성 : 값,
      ...
   },
   적용하고자 하는 프로퍼티 : {
      속성 : 값,
      속성 : 값,
      ...
   },
   ...
})
defineProperties() 메서드는 객체의 프로퍼티와 속성을 한번에 바꾸는데 유용하다.
내용은 배열 형태로 작성된다.

let user = {
   name: 'park',
   height: 180,
   weight: 70,
   성격: '밝음',
}
// name의 속성을 수정하고, like 프로퍼티와 속성을 추가하였다. 
Object.defineProperties(user, {
   name: {
      value: 'kim',
      writable: false,
   },
   like: {
      value: 'rice',
      writable: true,
   },
})
console.log(Object.getOwnPropertyDescriptors(user))
/*
{
  name: {
    value: 'kim',
    writable: false,
    enumerable: true,
    configurable: true
  },
  height: { value: 180, writable: true, enumerable: true, configurable: true },
  weight: { value: 70, writable: true, enumerable: true, configurable: true },
  '성격': { value: '밝음', writable: true, enumerable: true, configurable: true },
  like: {
    value: 'rice',
    writable: true,
    enumerable: false,
    configurable: false
  }
}
*/​

 

5. 객체와 속성을 함께 복사하는 방법

Object.getOwnPropertyDescriptors() 메서드와 Object.defineProperties() 메서드를 함께 사용하면 객체 복사 시 플래그도 함께 복사할 수 있다.
let user = {
   name: 'park',
   height: 180,
   weight: 70,
   성격: '밝음',
}

// 예전 방식 : 이 방식을 사용하면 플래그는 복사되지 않고, for..in을 사용하면 심볼형 프로퍼티도 놓치게됨.
let clone = {};
for (let key in user) {
  clone[key] = user[key];
}
// { name: 'park', height: 180, weight: 70, '성격': '밝음' }

// 메서드로 복사하는 방법
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(user));
// { name: 'park', height: 180, weight: 70, '성격': '밝음' }

 

객체 수정을 막아주는 다양한 메서드

프로퍼티 설명자는 특정 프로퍼티 하나를 대상으로 한다.

아래 메서드를 사용하면 한 객체 내 프로퍼티 전체를 대상으로 하는 제약사항을 만들 수 있다.

더보기
더보기

Object.preventExtensions(obj)

객체에 새로운 프로퍼티를 추가할 수 없게 한다.

 

Object.seal(obj)

새로운 프로퍼티 추가나 기존 프로퍼티 삭제를 막아준다.

프로퍼티 전체에 configurable: false를 설정하는 것과 동일한 효과이다.

 

Object.freeze(obj)

새로운 프로퍼티 추가나 기존 프로퍼티 삭제, 수정을 막아준다.

프로퍼티 전체에 configurable: false, writable: false를 설정하는 것과 동일한 효과다.

아래 메서드는 위 세 가지 메서드를 사용해서 설정한 제약사항을 확인할 때 사용할 수 있다.

 

Object.isExtensible(obj)

새로운 프로퍼티를 추가하는 게 불가능한 경우 false를, 그렇지 않은 경우 true를 반환한다.

 

Object.isSealed(obj)

프로퍼티 추가, 삭제가 불가능하고 모든 프로퍼티가 configurable: false이면 true를 반환한다.

 

Object.isFrozen(obj)

프로퍼티 추가, 삭제, 변경이 불가능하고 모든 프로퍼티가 configurable: false, writable: false이면 true를 반환한다.

 

위 메서드들은 실무에선 잘 사용되지 않는다.


+) 추가

1. 밑줄 프로퍼티(Underscore Property)

밑줄 프로퍼티는 변수명이나 속성명 앞에 밑줄( _ ) 을 붙여서 사용하는 관례를 말한다. 
이는 JavaScript 언어의 문법적인 요구 사항은 아니며, 코드 작성자들의 암묵적인 규칙(컨벤션)이다.
JavaScript 에서 ES6 이전에는 private 키워드가 없었기 때문에 밑줄 프로퍼티가 간단한 대안으로 쓰였다.
클래스나 객체 내부에서만 사용되는 속성임을 나타내기 위해 프로퍼티 앞에 밑줄을 붙인것이다.
"이 속성은 직접 수정하지 말고, getter / setter 등을 통해 간접적으로 접근하세요" 라는 의미이다.
밑줄 프로퍼티보단 Private Field가 더 확실하게 보호되기 때문에 현대에는 #을 쓰는 것을 더 권장한다.

1) 밑줄을 사용했을 경우
class Person {
  constructor(name) {
    this._name = name; // 밑줄 프로퍼티 (내부적으로만 사용)
  }

  // Getter: _name 값을 반환
  get name() {
    return this._name;
  }

  // Setter: _name 값을 설정
  set name(newName) {
    if (newName.length > 0) {
      this._name = newName;
    } else {
      console.log("이름은 비어 있을 수 없습니다.");
    }
  }
}

const person = new Person("John");

// Getter 호출
console.log(person.name); // "John"

// Setter 호출
person.name = "Doe";
console.log(person.name); // "Doe"

// _name에 직접 접근 (권장하지 않음)
person._name = "";
console.log(person.name); // "Doe" (Setter 검증 로직을 우회)​


2) Private Field를 사용했을 경우

class Person {
  #name; // 진정한 private 속성

  constructor(name) {
    this.#name = name;
  }

  get name() {
    return this.#name;
  }

  set name(newName) {
    if (newName.length > 0) {
      this.#name = newName;
    } else {
      console.log("이름은 비어 있을 수 없습니다.");
    }
  }
}

const person = new Person("John");

console.log(person.name); // "John"

// #name에 직접 접근 (오류 발생)
console.log(person.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class


3) 위의 getter/setter에서 들었던 BankAccount 예시에 적용해보기

class BankAccount {
   #myMoney

   constructor(myMoney) {
      this.#myMoney = myMoney
   }
   get spend() {
      return this.#myMoney
   }
   set spend(amount) {
      if (amount >= 1000) {
         if (this.#myMoney >= amount) {
            this.#myMoney -= amount // 잔액에서 출금액 차감
         } else {
            console.log('잔액이 부족합니다.')
         }
      } else {
         console.log('1,000원 부터 출금 가능합니다.')
      }
   }
}
const user = new BankAccount(10000)
user.spend = 1000
console.log(user.spend)



 

 

 

 

참고 : https://ko.javascript.info/property-descriptors