내가 보려고 만든 개발 공부 일지

자바스크립트 - 객체 프로퍼티(property) 파헤치기! 본문

Javascript

자바스크립트 - 객체 프로퍼티(property) 파헤치기!

kwangsunny 2022. 3. 30. 00:09

자바스크립트에는 8가지의 자료형이 존재한다.

그 종류로는 number, string, BigInt, boolean, null, undefined, Symbol, 마지막으로 객체가 존재한다.

이 중 객체를 제외한 나머지들은 딱 하나의 값만 가질 수 있는데, 이런 자료형을 원시타입(primitive type)이라 하고,

객체는 여러 값들을 포함할 수 있는 하나의 덩어리이며 참조타입(reference type) 이라 부른다.

 

객체가 포함할 수 있는 값들에 제한은 없으며 어떤 자료형이든 객체의 내부 값으로 할당할 수 있다.

프로퍼티는 키-값 쌍으로 구성되는데 키는 값을 나타내는 이름이라 생각하면 되고, 이 키로 해당 프로퍼티 값에

접근할 수 있다.

let obj = {
    name: 'sunny' // 키(name) + 값(sunny) 쌍 --> 프로퍼티 
}

 

객체의 선언 방식은 아래와 같다.

let obj = {}  // (A)
let obj2 = new Object();  // (B)

 (A) 와 (B) 는 같은 뜻이다.

객체는 (B)와 같이 자바스크립트의 내장 생성자 함수인 Object에 의해 만들어지는데 이를 축약해서 쓴 형태가 (A)이다.  

그리고 (A)처럼 객체를 중괄호 방식으로 나타낸것을 객체 리터럴 이라고 부른다. 

 

참고로 객체 리터럴에서 메서드를 선언하는 방식은 두 가지가 있다.

let obj = {
    goHome: function(){ // (A)
    	console.log('퇴근');
    },
    goToWork(){ // (B)
    	console.log('출근');
    },
}

(A)를 축약해서 쓴 형태가 (B)이고, 이런식으로 객체 안에서 좀더 깔끔하게 메서드를 선언해줄 수 있다. 

 

여기까지는 객체와 프로퍼티에 대해 누구나 아는 내용일 것이다.

그런데 객체 프로퍼티는 두 가지로 나눌 수 있고, 또 숨겨진 설정 기능이 존재해 프로퍼티를 더 디테일하게 다룰 수 있는데 이것들이 무엇인지 하나씩 알아보자.

 

 

객체 프로퍼티의 종류

객체 프로퍼티는 아래와 같이 두 가지로 나뉜다.  

 

데이터 프로퍼티 -  값을 저장하는 프로퍼티 (일반적으로 쓰는 방식) 

접근자 프로퍼티 -  프로퍼티 값에 접근하려고 할때 혹은 프로퍼티 값을 수정하려 할때 실행되는 메서드

 

데이터 프로퍼티는 객체에 값을 할당하고 접근하는 일반적인 방식으로 사용되는 프로퍼티를 말한다.

그러면 접근자 프로퍼티는 무엇인지 예제로 알아보자.

 

 

접근자 프로퍼티 - getter, setter 메서드

let obj = {
    get name(){ // getter (획득자)
    	return `name is ${this._name}`;
    },
    set name(value){ // setter (설정자)
    	this._name = value;
    }
}

getter setter를 가진 obj의 내부

위와 같은 코드를 본적이 있는가? 

get, set 뭔가 함수 같이 생겼는데..? 함수가 맞긴 한데 이 메서드들은 조금 특별하게 취급된다.

객체 리터럴 안에서 저런식으로 get somthing(){...} 혹은 set somthing(){...} 형태로 써주게 되면, 

외부에서 obj.something 을 할때 getter 함수가 실행되고 obj.something = 'test' 를 하면 setter 가 실행된다.

 

그래서 위 코드를 실행하면 아래와 같이 출력된다.

getter, setter 메서드 실행 결과

obj.name 을 실행하면 getter 함수가 실행되서 name is 가 붙어서 출력된다. 

처음엔 obj._name 이란 값이 없으므로 undefined가 출력되다가, 다음 줄에서 'test' 를 name 에 할당해주면

setter 메서드가 this._name = 'test' 를 수행하여 name is test 라고 출력된다.

 

(참고로 변수명에 언더바( _ )를 붙이면 '현재 스코프에서만 쓰는 로컬 변수' 라는 일종의 개발자들 사이의 약속인데, 

obj._name = 123 처럼 직접 값을 수정할 수도 있지만 지양하는것이 좋다.) 

 

- getter, setter 제한 사항

setter 함수는 인자를 하나만 받도록 강제되는데,

obj._name = 'test' 를 했을때 할당해주는 값(이 예제에선 'test')이 setter 함수의 첫번째 인자로 들어간다.

만약 setter에 인자가 하나가 아니면 에러가 발생한다.

반면 getter 는 setter 와 달리 인자를 하나도 받으면 안된다. 만약 인자를 하나라도 받게 되면 에러가 발생한다.

좌 : setter에 인자가 없을때  /  우 : getter에 인자가 있을때

 

또 getter, setter 는 메서드 축약 방식으로 선언해줘야 그 기능을 발휘한다.

만약 get: function name(){...} 같이 기본 방식을 사용하면 obj.name 을 해도 getter 가 작동하지 않는다.

 

- getter, setter 로 읽기전용, 쓰기전용 프로퍼티 만들기

getter 와 setter 를 같이 만들지 않고 하나씩만 만들어줌으로써 해당 프로퍼티를 읽기전용 혹은 쓰기전용 으로 만들 수도 있다.

좌 : getter만 존재  /  우 : setter만 존재

왼쪽처럼 getter 메서드만 있는 경우엔 o.name = 123 처럼 값을 수정해 주려고 해도 setter가 없기 때문에

값은 수정되지 않고 오로지 값을 읽는것 밖엔 할 수 없게된다. 

반대로 오른쪽처럼 setter만 있는 경우, 값을 수정해줄 수는 있어도 값을 읽지는 못한다.

이런식으로 제한을 걸어두어 객체의 상태값을 마음대로 수정하거나 읽어오는 행위를 방지하는 장치로 활용할 수 있다. 

 

- getter, setter 잘 활용하기

getter, setter를 이용하면 프로퍼티 값을 수정할때 간단한 유효성 체크도 할 수 있다.

let obj = {
    _userNames: [],
    get user(){
        return this._userNames;
    },
    set user(value){
        if(value.length > 9){
            console.error('이름이 너무 깁니다.');
            return;
        }
        this._userNames.push(value);
    }
}

실행 결과

위 예제는 getter, setter를 이용해 사용자 이름을 배열에 추가하는 코드인데 추가해줄 이름의 길이가 너무 길면

이름이 너무 길다는 에러 메시지를 출력해줘서 사용자 이름이 추가되지 않도록 유효성 체크를 해주는 코드이다.

'sunny' 는 9자를 넘지 않기 때문에 배열에 잘 추가된 모습을 볼 수 있다. 

 

- getter, setter 내장 프로퍼티 

getter, setter 를 사용하는 대표적인 내장 프로퍼티로 __proto__ 와 cookie 가 있다.

obj.__proto__ 프로퍼티는 점 앞의 객체(obj) 가 참조하는 프로토타입 객체에 접근할 수 있게 해주는

객체 내장 프로퍼티이고, 쿠키값을 저장하는 document.cookie 프로퍼티도 접근자 프로퍼티이다.

document.cookie = "name=sunny";
document.cookie = "age=20";
document.cookie = "nat=korea";

쿠키는 키-값 쌍으로 저장되는데 위 코드처럼 문자열로 "키=값" 형태로 값을 할당해주면 새롭게 쿠키가 추가된다.

그리고 다음 줄에서 cookie에 age 와 nat 값을 연달아 할당해주고 있는데, 이렇게 해도 cookie의 값은 덮어 씌워지지

않는다.

그 이유는 cookie가 접근자 프로퍼티라서 getter, setter 가 내부적으로 구현되어있고 새로운 값을 할당할때 덮어씌우지 않고 추가되도록 만들어져 있기 때문이다.

덮어씌워지지 않고 새롭게 추가된 모습

 

이런 식으로 접근자 프로퍼티를 활용하면 외부 코드에서는 그냥 일반 프로퍼티에 접근하는 것처럼 보이지만

내부적으로는 미리 정의된 getter 와 setter로 원하는 작업을 해줄 수 있으므로, 어떤 값에 대해 전처리 작업이

필요한 경우에 접근자 프로퍼티를 유용하게 사용할 수 있다.

 

   

프로퍼티 설정하기

지금까지는 객체의 접근자 프로퍼티에 대해 알아보았고, 이제부터는 프로퍼티 자체에 더 집중해 볼것이다.

프로퍼티는 값 외에 다른 숨겨진 속성들을 더 가지고 있다.

이 속성들은 평소엔 사용할 일이 거의 없긴 하지만, 알고 있으면 그만큼 자바스크립트의 이해도가 올라가는 일이니

공부해보는것도 좋을것 같다.

 

프로퍼티의 상세 속성들은 다음과 같다.

 

value - 우리가 평소에 사용하는 프로퍼티의 값이다.

writable - false 면 값을 수정할 수 없다. (기본값 = true)

enumarable - false 면 for...in 반복문으로 열거할 수 없다. (기본값 = true)

configurable - false 면 프로퍼티를 삭제할 수도 없고 writable, enumarable 을 수정할 수 없다. (기본값 = true)

 

위 속성들 중 value를 제외한 나머지 속성들을 플래그(flag) 라고 부른다.

객체에 프로퍼티가 만들어지면 그 플래그들의 값은 모두 true로 자동 세팅된다.

 

 

프로퍼티 descriptor (설명자)

- Object.getOwnPropertyDescriptor 메서드 

그럼 이 플래그들은 어떻게 확인 할 수 있을까?

// obj.key 의 플래그들을 얻을 수 있다.
Object.getOwnPropertyDescriptor(obj, 'key')

내장 함수 Object의 getOwnPropertyDescriptor 메서드를 이용하면 해당 객체의 프로퍼티 플래그들을 볼 수 있다.

첫 번째 인자에는 객체를 넘겨주고, 두 번째 인자는 확인하고픈 프로퍼티의 키를 넘겨준다.

 

obj.name 의 속성들

위 그림을 보면 getOwnPropertyDescriptor 메서드로 obj 객체의 name 프로퍼티의 값과 플래그들을 담고있는 객체가

반환되는데, 이렇게 반환된 객체를 descriptor 라고 부른다.  

 

보다시피 프로퍼티가 만들어질때 플래그들은 모두 기본값으로 true를 가지고 있다.  

그럼 이 플래그값을 수정하려면 어떻게 해야할까?

 

- Object.defineProperty 메서드

defineProperty 메서드를 사용하면 프로퍼티의 descriptor를 직접 세팅해줄 수 있다.

let obj = {};
Object.defineProperty(obj, 'name', {
    value: 'sunny',
    writable: true,
    configurable: true,
});

위 예시코드를 보면 defineProperty 의 첫번째 인자로 객체, 두번째 인자는 descriptor를 설정해줄 프로퍼티 이름,

마지막 세 번째 인자로 descriptor 객체를 넘겨주어 직접 obj.name의 값과 플래그들을 설정해주고 있다.

 

그런데 잘보면 enumerable 플래그는 값을 설정해주지 않고 생략했다.  

정의되지 않은 프로퍼티를 defineProperty로 새로 만들어줄때 이렇게 플래그를 명시해주지 않으면

그 플래그의 값은 자동으로 false가 된다. 한번 확인해 볼까?

defineProperty로 프로퍼티를 만든 후, getOwnPropertyDescriptor로 descriptor 객체 확인

enumerable 을 생략하고 프로퍼티를 정의한 후 getOwnPropertyDescriptor 메서드로 obj.name 의 descriptor 객체를

출력해보니, 정말 enumerable 플래그의 값이 false인 것을 확인할 수 있다.  

 

- 접근자 프로퍼티의 descriptor

그럼 접근자 프로퍼티인 getter, setter의 descriptor도 데이터 프로퍼티와 같은 형태일까? 확인해 보자.

접근자 프로퍼티의 descriptor

오호, 뭔가 다른데?

위 그림에서 볼 수 있듯이, 접근자 프로퍼티는 데이터 프로퍼티와는 달리 writable, value 가 없다.

대신 get, set 함수를 가지고 있는 모습이다.

 

글 초반에 getter, setter를 설정할때 객체 리터럴로 선언시에 바로 만들어주는 방식을 예제 코드로 설명했었다.

그럼 이미 선언되고난 이후에 getter, setter를 만들어주려면? defineProperty를 사용해서 만들어주면 된다.

let obj = {}
Object.defineProperty(obj, 'name', {
    get(){
        return '이름 : ' + this._name;
    },
    set(value){
        this._name = value + ' 입니다.';
    }
})

 

defineProperty로 만든 접근자 프로퍼티

위 예제처럼 이미 정의된 객체obj 에 defineProperty를 사용하여 getter와 setter를 만들어줄 수 있다.

 

 

플래그의 역할

이제 객체 프로퍼티는 value 와 세 가지의 플래그들로 이루어져 있다는걸 알게되었다.  

그럼 descriptor 객체의 각 플래그들의 역할은 무엇일까? 하나씩 알아보도록 하자.

 

1. writable 플래그

writable의 값이 true면 프로퍼티에 값을 쓸 수 있고 false면 쓸 수 없다.

writable == false 일때

writable 값이 false일때 프로퍼티 값을 수정해주는 예시이다.

분명 obj.name = 'kwang' 으로 값을 할당해줬지만, 아래에서 obj.name 을 출력해보면 값이 바뀌지 않았고

그대로 'sunny' 인걸 볼 수 있다.

이전 getter, setter 설명에서 setter 없이 getter만 만들어서 읽기전용 프로퍼티를 만든것과 같은 결과가 되었다.

 

참고로, 위 예시를 엄격모드에서 실행하면 Cannot assign 에러가 발생한다.

엄격모드에서 실행

 

2. enumerable 플래그

객체를 순회하기 위해서 가장 많이쓰는 방법은 for...in 반복문일 것이다.

for...in 반복문은 열거 가능한 객체 프로퍼티만 가져올 수 있다.

여기서 열거 가능하다는 것은 enumerable == true 인 프로퍼티를 말한다.

let obj = {
    name: 'sunny',
    age: 20,
    nat: 'Korea',
}

for(let key in obj){
    console.log(key);
}

출력 모습

obj의 프로퍼티들을 일반적인 방법으로 만들어졌기 때문에 enumerable 값은 true일 것이고 그래서 for...in 문에서

모든 프로퍼티에 접근 가능한 것이다.

그럼 enumerable 값을 false 로 바꾸면 어떻게 되는지 확인해보자.

enumerable == false 일때

obj.name 프로퍼티의 enumerable 플래그 값을 false로 만들고 for...in 을 실행해보니

정말 'name' 이 출력되지 않는 모습을 볼 수 있다.

 

for...in 문은 사실 객체가 상속받아 쓰는 프로퍼티들도 모두 출력해준다.

그런데 생각해보면 객체는 생성자 함수 Object를 통해 만들어지기 때문에 모든 객체는 Object의 프로토타입 객체를

참조하게 된다. 

 

그래서 Object.prototype에 정의되어있는 toStiring, hasOwnProperty, valueOf 같은 내장 메서드들을 그대로 

상속받듯이 사용할 수 있다.

그런데 신기하게도 for...in 문으로 객체를 순회할때 toString, hasOwnProperty 같은 메서드는 가져오지 않는다. 왜일까?

 

그건 바로 이 내장 메서드들의 enumerable은 false로 설정되어있기 때문이다.

그럼 바로 확인해보자.  

내장 메서드의 descriptor

위 그림에서 보이듯이 toString의 enumerable은 false 이다. 

그래서 일반 객체들을 for...in 으로 순회했을때 toString과 같은 내장 메서드들은 출력되지 않는것이다.

 

물론 obj.toString = function(){...} 이런식으로 객체에 직접 같은 이름의 메서드를 오버라이딩 해주면 obj.toString은

당연히 for...in 에서 접근가능 할 것이다.

그 이유는 객체 프로퍼티를 검색할때는 항상 제일 가까운곳에 있는 프로퍼티부터 탐색하기 때문이다.

 

3. configurable 플래그

configurable 은 플래그들을 수정할 수 있냐 없냐를 결정한다.

configurable 값을 false로 설정해주면, 기본적으로 프로퍼티를 삭제할 수 없게 되고,

enumerable, configurable, writable 값들을 수정할 수 없게된다. 

(writable 을 false -> true 로 변경할 순 없지만 true -> false는 가능하다.)

 

그리고 한 번 configurable값을 false 로 설정해주면 defineProperty를 사용해도 이를 다시 true로 되돌릴순 없다.

그래서 configurable값을 false로 설정해주는 경우는 어떤 속성이든 절대로 변경해서는 안되는 프로퍼티인 경우에만

사용해야 한다.

 

 

 

이렇게 객체 프로퍼티에 대해 알아보았는데, 정리해보니 내용이 꽤 길어졌다.

프로퍼티만 다뤄도 이정도인데.. 역시 JS잘알의 길은 멀고도 험한것같다.  

 

 

잘못된 내용이 있으면 댓글 주세요 :) 

 

 

 

 

 

Comments