javaScript/jsDeepDive

getter, setter 프로퍼티

부엉이사장 2023. 6. 23. 16:34

# 왜 포스팅하니?

getter setter 프로퍼티를 접하고 신기해서 써봄.. 이 문법에대해서 사실 몰랐다.

근데 생각보다 꽤나 유용할것 같아서 포스팅하려고 함.

 

 

# 내가 문제로 삼았던것.

const obj = {
  dog1: "무찌",
  dog2: "도리",
  teamName: obj.dog1 + obj.dog2+'듀오',
};

위 코드는 에러가 뜬다

요렇게 참조에러가 뜬다. teamName프로퍼티가 dog1과 dog2를 참조하는건데 teamName프로퍼티가 자기자신 obj를 참조하는 시점이 실행컨텍스트의 코드평가할때 참조하려고해서 참조에러가 뜨는거다. 호이스팅이 발생하는거지.

그래서 이걸 억지로 만들어보려면..?

 

- 즉시실행함수 

const obj = {
  dog1: "무찌",
  dog2: "도리",
};

(function () {
  obj.teamName = obj.dog1 + obj.dog2 + "듀오";
})();

console.log(obj.teamName); // 무찌도리듀오 출력

즉시실행함수로 프로퍼티를 추가해주었다.

다만 이건 dog1과 dog2가 바뀔때 즉시실행함수가 또 호출되지않아서 초기설정만 가능하다.

뭐 굳이 즉시실행함수까지도 필요없고 걍 obj.teamName = obj.dog1+obj.dog2+'듀오'로 해줘도 되긴함..

 

const obj = {
  dog1: "무찌",
  dog2: "도리",
  teamName() {
    return this.dog1 + this.dog2 + "듀오";
  },
};

console.log(obj.teamName());

뭐 이런식으로 메서드호출방식으로 해도 된다.

이건 dog1과, dog2가 바뀌더라도 teamName()을 호출할때마다 동적으로 리턴값이 바뀐다.

다만 호출을 해야하기때문에 프로퍼티라기보단 메서드로 보는게 맞다.

 

 

const obj = {
  dog1: "무찌",
  dog2: "도리",
  teamName: (function () {
    return this.dog1 + this.dog2 + "듀오";
  })(),
};

console.log(obj.teamName);

teamName에 즉시실행함수로 값을 리턴하는 방식은 역시나 obj를 실행컨텍스트 평가단계에서 프로퍼티 teamName을 바인딩하는과정에서 이 obj를 참조해야하기때문에 호이스팅에러가 뜨게 된다.

 

 

# 위 방식으로 억지로 접근자프로퍼티같은 프로퍼티 만들기?

위에서 봤을때 메서드로 하는방법만이 유일하다..

그렇다면 이 방식으로 억지로 접근자프로퍼티같은 메서드?를 만들어보자.

const obj = {
  dog1: "무찌",
  dog2: "도리",
  teamName(how, value) {
    if (how == "get") {
      return this.dog1 + this.dog2 + "듀오";
    } else if (how == "set") {
      this.dog1 = value[0] + value[1];
      this.dog2 = value[2] + value[3];
    } else {
      return;
    }
  },
};

console.log(obj.teamName("get")); // 무찌도리듀오
obj.teamName("set", "바보똥개듀오"); // 프로퍼티 바꿈
console.log(obj.teamName("get")); // 바보똥개듀오

console.log(obj.dog1); // 바보
console.log(obj.dog2); // 똥개

뭐이런식으로 단순하게 만들어보았다.

호출을 해야하고.. set을 인수로 넣어 또 따로 호출해줘야하고.. 짜증나게 생겼다.

 

때문에 이건 그냥 접근자 프로퍼티로 만드는게 속편하다.

const obj = {
  dog1: "무찌",
  dog2: "도리",
  get teamName() {
    return this.dog1 + this.dog2 + "듀오";
  },
  set teamName(newTeamName) {
    this.dog1 = newTeamName[0] + newTeamName[1];
    this.dog2 = newTeamName[2] + newTeamName[3];
  },
};

console.log(obj.teamName); // 무찌도리듀오
obj.teamName = "바보똥개듀오"; // 이렇게하면 obj의 dog1과 dog2가 전부 새로 세팅된다.
console.log(obj.dog1); // 바보 
console.log(obj.dog2); // 똥개
console.log(obj.teamName); // 바보똥개듀오

내가 중요하게 생각하는 점은 접근자프로퍼티는 현재 자신이 속한 객체를 참조할수 있다는것이다.

하지만 이 접근자 프로퍼티는 정확히 프로퍼티가 아니라 메서드로 이루어져있다고 봐야할것이다.

teamName이라는 프로퍼티를 참조할때는 get teamName메서드가 호출되고, 할당할떄는 set teamName이라는 메서드가 호출된다고 보면 된다.

 

이걸로 프로젝트에서 다크모드를 구현할때 쓰면될듯..?

아래 대충 만들어서 써놨음.

 

 

 

# 검사해주는 접근자 프로퍼티 만들기

위에까지는 한 프로퍼티가 동일객체 다른 프로퍼티를 참조할때를 가정한것이다.

그러나 한 프로퍼티만을 참조, 할당할때 함수가 동작하게 하려면 예시가 있다.

모던 js 에서 본 내용이다.

 

https://ko.javascript.info/property-accessors

 

프로퍼티 getter와 setter

 

ko.javascript.info

 

let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 4) {
      alert("입력하신 값이 너무 짧습니다. 네 글자 이상으로 구성된 이름을 입력하세요.");
      return;
    }
    this._name = value;
  }
};

user.name = "Pete";
alert(user.name); // Pete

user.name = ""; // 너무 짧은 이름을 할당하려 함

요 코드인데 오직 user이란 객체안에서는 name이라는 프로퍼티밖에없다.

다만 이렇게 한다면 name프로퍼티를 할당할때 setter메서드가 호출되어서 길이를 조절해준다.

대신 _name프로퍼티는 접근자프로퍼티 메서드내에서만 쓰는게 적절하단다.

이런 코딩방식은 약간 패턴으로 생각해도 되는듯..?

 

 

 

# 다크모드 예시코드

let mode = "dark";

const obj = {
  get backgroundColor() {
    if (mode == "dark") {
      return "black";
    } else {
      return "white";
    }
  },
  set backgroundColor(newColor) {
    if (newColor == "dark") {
      mode = "dark";
    } else {
      mode = "light";
    }
  },
};

console.log(obj.backgroundColor); // black 출력
obj.backgroundColor = "white";
console.log(obj.backgroundColor); // white 출력
console.log(mode); // light 출력

대충 다크모드 만들어봤다.

 

이건 get한정해서 삼항연산자로 쓸 수도 있다.

let mode = "dark";

const obj = {
  backgroundColor: mode == "dark" ? "black" : "white",
};

console.log(obj.backgroundColor); // black
let mode = "light";

const obj = {
  backgroundColor: mode == "dark" ? "black" : "white",
};

console.log(obj.backgroundColor); // white

다만 삼항연산자는 프로퍼티를 참조할때 작동하는것이 아니라, 처음 프로퍼티들이 할당될때 한번만 동작한다.

따라서 아래와같은 오류?아닌 병신같은 결과가 나올 수 있다.

let mode = "dark";

const obj = {
  backgroundColor: mode == "dark" ? "black" : "white",
};

console.log(obj.backgroundColor); // black

mode = "light";
console.log(obj.backgroundColor);//black출력. mode는 바뀌었지만 삼항연산자 코드는 동작하지 않음.

이렇게 삼항연산자는 처음 obj.backgroundColor가 할당될때 한번만 작동한다.

 

이걸 어떻게 해결하냐면,

let mode = "dark";

const obj = {
  backgroundColor: mode == "dark" ? "black" : "white",
};

console.log(obj.backgroundColor); // black

const changeLight = () => {
  if (mode == "dark") {
    mode = "light";
    obj.backgroundColor = "white";
  } else {
    mode = "dark";
    obj.backgroundColor = "black";
  }
};

changeLight();
console.log(mode); //light
console.log(obj.backgroundColor); // white

changeLight();
console.log(mode); //dark
console.log(obj.backgroundColor); // black

이렇게 changeLight라는 함수를 만들어서 다크모드 바꾸는 버튼의 클릭이벤트로 넣었다.

뭐 이런식으로도 되긴 된다만 접근자프로퍼티가 더 좋겠지?

 

 

 

# 클래스에서 접근자 프로퍼티

const User = class {
  constructor(name) {
    this.name = name;
    this.level = "normal";
  }
  get 존칭() {
    return this.name + "님" + `(${this.level})`;
  }
  set 존칭(name) {
    this.name = name.slice(0, -1);
  }
};

유저 클래스를 만들었따.

그리고 접근자 프로퍼티로 존칭이라는 프로퍼티를 만들었다.

 

const user1 = new User("무찌");
console.log(user1.존칭); // 무찌님(normal)
console.log(user1); //User { name: '무찌', level: 'normal' }

user1.존칭 = "도리님";
console.log(user1.존칭); // 도리님(normal)
console.log(user1); //User { name: '도리', level: 'normal' }

요클래스로 user1을 만들었고 잘뜬다.

그리고 user1의 존칭을 '도리님'으로 바꾸니 이 클래스의 모든 프로퍼티가 잘 바뀌었다.

근데 user1을 봤는데 '존칭'으로 지정한 접근자프로퍼티가 없다. 이건 프로토타입 메서드로 정해줬기 때문이다.

 

여기서 Vip라는 확장클래스를 만들어봤다.

class Vip extends User {
  constructor(para) {
    super(para);
    this.level = "vip";
  }
}
const user2 = new Vip("도리");
console.log(user2); //Vip { name: '도리', level: 'vip' }
user2.존칭 = "똥개님";
console.log(user2); //Vip { name: '똥개', level: 'vip' }

console.log(user2.존칭); //똥개님(vip)

위와같이 User의 프로토타입메서드인 '존칭'접근자 프로퍼티가 Vip클래스로 생성된 user2에서도 잘 동작한다.

 

console.log(User.prototype); //여기엔 존칭이라는 접근자 프로퍼티로 존재
console.log(Vip.prototype); // 여기는 존칭이 있긴한데 접근자 프로퍼티가 아님

console.log(Object.getOwnPropertyDescriptor(User.prototype, "존칭")); // 접근자프로퍼티 잘뜸
console.log(Object.getOwnPropertyDescriptor(Vip.prototype, "존칭")); // undefined뜸

하지만 이 존칭 접근자 프로퍼티는 User.prototype 것이지 Vip.prototype꺼는 아니다.

 

그럼 오버라이딩을 테스트하기위해 새로운 클래스 Banned를 만들어봤다.

class Banned extends User {
  constructor(para) {
    super(para);
    this.level = "강퇴당한고객";
  }
  get 존칭() {
    return this.name + "새끼" + `(${this.level})`;
  }
  set 존칭(name) {
    this.name = name.slice(0, -2);
  }
}
const user3 = new Banned("무찌");
console.log(user3); //Banned { name: '무찌', level: '강퇴당한고객' }
// User의 존칭 접근자 프로퍼티를 덮어쓴걸 알 수 있다.
console.log(user3.존칭); // 무찌새끼(강퇴당한고객)

console.log(Object.getOwnPropertyDescriptor(User.prototype, "존칭")); // 접근자프로퍼티 잘뜸
console.log(Object.getOwnPropertyDescriptor(Banned.prototype, "존칭")); // 덮어쓴 접근자프로퍼티 잘뜸

 

결과적으로 프로토타입 메서드로 접근자 프로퍼티를 만들었지만 하위클래스에서도 다 잘 쓰고 있다.

동적으로 접근자 프로퍼티를 클래스마다 변경 및 부여하려면 오버라이딩을 하면 된다.

 

그럼 직접적으로 접근자 프로퍼티를 클래스에 넣고 싶다면?

 

const User = class {
  constructor(name) {
    this.name = name;
    this.level = "normal";
    Object.defineProperty(this, "존칭", {
      get() {
        return this.name + "님" + `(${this.level})`;
      },
      set(name) {
        this.name = name.slice(0, -1);
      },
    });
  }
};

const user1 = new User("무찌");

console.log(user1); //User { name: '무찌', level: 'normal' } 여기엔 존칭 프로퍼티가 안뜸
console.log(user1.존칭); // 무찌님(normal) 하지만 동작됨

user1.존칭 = "도리님";
console.log(user1); //User { name: '도리', level: 'normal' } set함수가 잘 동작함

console.log(Object.getOwnPropertyDescriptors(user1)); // 여기에선 존칭 프로퍼티가 잘 뜸
console.log(User.prototype); // 여기엔 '존칭'프로퍼티가 없다. 즉 프로토타입 메서드가 아니라, constuctor에 직접적으로 들어가는 프로퍼티라는 뜻.

요렇게 defineProperty메서드로 직접적으로 프로퍼티를 지정해주면된다.

그러면 User에서 만든 인스턴스에서 직접적으로 쓸 수 있는 접근자프로퍼티가 된다.

User의 하위 프로퍼티에선 super로 넘겨주지않는한 이 접근자프로퍼티를 사용할 수 없다.

 

 

 

 

++ 왜 user1을 참조해서 콘솔로 찍을때 접근자프로퍼티가 안떴을까?

const User = class {
  constructor(name) {
    this.name = name;
    this.level = "normal";
    Object.defineProperty(this, "존칭", {
      get() {
        return this.name + "님" + `(${this.level})`;
      },
      set(name) {
        this.name = name.slice(0, -1);
      },
      enumerable: true,
      configurable: false,
    });
  }
};

const user1 = new User("무찌");

console.log(user1); //User { name: '무찌', level: 'normal', '존칭': [Getter/Setter] }
//잘뜬다

enumerable을 true로 해주면 잘뜬다. ㅅㄱ