게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌54 - JavaScript - 7년 전 등록 / 6년 전 수정

변수 변경 탐지하기

setter, getter, proxy

안녕하세요. 2018년의 첫 번째 강좌로 변수 변경을 탐지하는 방법에 대해 알아보겠습니다.

자바스크립트로 코딩할 때, 변수의 값이 변경될 때마다 특정 동작을 실행하도록 한 적이 있을 겁니다. 간단한 예로, 계산기를 만들 때 결괏값을 DOM에 반영한다거나, 카운터의 숫자가 올라갈 때 DOM을 업데이트하는 경우가 있겠죠.

보통은 이벤트 리스너에 콜백 함수를 연결하는 방법으로 해결하곤 하는데요. 이 방법 외에도 다음과 같이 할 수 있습니다. 과거에 Object.observe라는 기능이 이 역할을 했습니다. 하지만 이 방법은 deprecated되었고요 ES2015에 와서는 getter, setter, proxy로 구현합니다.

ES2015(ES6) 코드이므로 크롬, 파이어폭스 같은 에버그린 브라우저에만 동작합니다. ES5에서 사용하고 싶다면 간단히 proxy-polyfill이나 Object.defineProperty를 사용하면 됩니다.

counter.html

<div id="count">0</div>
<button id="up">+1</button>
<button id="down">-1</button>
<script>
  var count = 0;
  var counter = document.querySelector('#count');
  document.querySelector('#up').addEventListener('click', function() {
    count++;
    counter.textContent = count; // 중복됨
  });
  document.querySelector('#down').addEventListener('click', function() {
    count--;
    counter.textContent = count; // 중복됨
  });
</script>

버튼을 눌렀을 때 1이 올라가거나 내려가는, 흔하고 간단한 카운터 예제입니다. 아마 대부분 다 이렇게 구현할 겁니다. 코드를
보면 중복되는 부분이 있습니다. count를 변경할 때마다 DOM도 같이 변경하는 부분이 따라서 나옵니다. 앞으로 이벤트 리스너를 추가할 때마다 count를 변경 후 DOM을 변경하는 부분도 추가해야 합니다. 중복이 계속 발생하는 구조인 것이죠.

getter와 setter를 알면 이렇게도 짤 수 있습니다. script 부분만 바뀝니다.

var count = {};
Object.defineProperty(count, 'number', {
  get: function() {
    return this._num || 0;
  },
  set: function(num) {
    this._num = num;
    console.log(this._num); // 이렇게 일괄적으로 디버깅 가능.
    document.querySelector('#count').textContent = this._num; // 중복 제거
  },
});
document.querySelector('#up').addEventListener('click', function() {
  count.number++;
});
document.querySelector('#down').addEventListener('click', function() {
  count.number--;
});

먼저 변수 대신 객체를 사용합니다. 객체가 제공하는 기능을 사용할 것이기 때문입니다. Object 강좌 때 배웠던 defineProperty를 사용하였습니다. count 객체에 number라는 속성을 추가하는데, number 속성을 조회할 때(get)는 _num이라는 비밀 속성을 가져오고, number 속성에 값을 대입할 때(set)는 _num에 새로운 값을 대입한 후 DOM에 반영합니다. 즉, 실제 카운트는 _num 속성에 되지만, getter와 setter를 사용하기 위해 number 속성을 대신 사용하는 것입니다. _num 속성을 접근 불가능한 비밀변수로 만들고 싶다면 클로저를 활용한 IIFE 패턴을 사용하면 되겠죠?

잠깐, 코드가 더 길어졌으니 쓸 데 없는 짓을 한 거 아니냐고요? set 부분을 보시면 됩니다. number 속성에 값을 대입할 때 자동으로 DOM에도 반영하고, console.log 등의 디버깅도 할 수 있습니다. 예전 코드였다면 이벤트 리스너 안에 일일이 추가해줘야 했던 것이죠. 중복이 발생하는 것입니다. 물론 지금과 같은 짧은 코드는 이렇게 짜면 시간 낭비입니다만, 코드가 복잡해지면 중복을 피하는 것이 매우 중요해집니다. 앞으로는 count.number만 변경하면 알아서 DOM도 같이 변경하기 때문에 따로 코드를 추가할 필요가 없습니다. 속성값과 한 세트로 동작하는 동작들을 setter를 이용해서 정의할 수 있는 것입니다. getter도 비슷하게 활용할 수 있고요.

ES2015 코드를 사용하면 좀 더 간단해집니다. defineProperty를 사용하지 않아도 되기 때문이죠.

const count = {
  get number() {
    return this._num || 0;
  },
  set number(num) {
    this._num = num;
    console.log(num); // 이렇게 일괄적으로 디버깅 가능.
    document.querySelector('#count').textContent = this._num;
  }
};
document.querySelector('#up').addEventListener('click', () => {
  count.number++;
});
document.querySelector('#down').addEventListener('click', () => {
  count.number--;
});

더 강력한 기능을 구현하고 싶다면 Proxy를 사용할 수 있습니다. ES2015에 추가된 기능입니다. Proxy 강좌 에 자세한 설명이 있지만, 간단하게 설명드리겠습니다. Proxy는 기존 객체를 건드리지 않고도 새 기능을 추가하거나 기존 기능을 수정할 수 있는 기능입니다. 기존 객체를 두고, 새로운 기능(handler)를 추가한 프록시 객체를 그 위에 덮어 씌우는 개념입니다. 현재 예제가 너무 간단하여 Proxy를 100% 활용하기가 힘든데요.

const count = {};
const handler = {
  get: (obj, name) => {
    if (name === 'number') {
      return this._num || 0;
    }
  },
  set: (obj, name, value) => {
    if (name === 'number') {
      this._num = value;
      console.log(count);
      document.querySelector('#count').textContent = this._num;
    }
  }
};
const proxy = new Proxy(count, handler);
document.querySelector('#up').addEventListener('click', () => {
  proxy.number++;
});
document.querySelector('#down').addEventListener('click', () => {
  proxy.number--;
});

위의 예제는 Proxy를 사용해서 카운터를 구현한 예제입니다. set 부분에 console.log(count)를 했는데 실제로 돌려보면 count는 빈 객체로 뜹니다. 카운터는 멀쩡히 작동하고 있는데 말이죠. 이렇게 실제 객체와 프록시 객체를 분리하여 다른 동작을 하게끔 할 수도 있습니다. 보통 프록시를 사용할 때는 단순히 getter와 setter만 쓸 때가 아니라, 핸들러의 다른 기능들을 같이 쓰고자 할 때 사용합니다. 더 많은 활용 방법은 여러분의 상상력에 맡기겠습니다.

주의할 점은 getter와 setter를 잘못 사용하면 무한 루프에 빠져 스택 오버플로우가 일어날 수도 있습니다. getter에서 속성에 대입을 한다거나 하면 getter가 setter를 호출하고, setter가 다시 getter를 호출하고... 무한 반복 끝에 스택 오버플로우가 터집니다. 이 점 조심하셔야 합니다.

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

3개의 댓글이 있습니다.
4년 전
asdf
6년 전
비공개 변수.이렇게 만들면 되나요?
(() => {
const count = {
get number() {
return this._num || 0;
},
set number(num) {
this._num = num;
document.querySelector("#count").textContent = this.number;
}
};
document.querySelector('#up').addEventListener('click', () => {
count.number++;
});
document.querySelector('#down').addEventListener('click', () => {
count.number--;
});
})();
6년 전
네 그게 본문에 언급된 IIFE 패턴입니다.
6년 전
어차피 count를 임시처럼 쓸바에는 아예 count조차 숨겨버리는건 어떤가요??
(count => {
Object.defineProperty(count, 'number', {
get() {
return this._num || 0;
},
set(num) {
this._num = num;
console.log(this._num);
document.querySelector('#count').textContent = this.number;
}
});
document.querySelector('#up').addEventListener('click', () => {
count.number++;
});
document.querySelector('#down').addEventListener('click', () => {
count.number--;
});
})({});
6년 전
네네 즉시실행함수에서 return으로 Proxy를 해주면 더 좋겠네요.