vanilla js에서 프록시 객체를 옵저버처럼 사용하여 상태관리하기

김서진·2021년 9월 5일
6

kimyo_javascript

목록 보기
3/5
post-thumbnail

글을 쓰게 된 동기와 글의 목표


위와 같이 간단한 필터링을 수행하는 코드를 짤 일이 생겼다.
(보안 상의 이유로 내용을 변형하고 간소화했다)

기존에 내가 코드를 작성하던 방법대로 각 버튼에 이벤트 리스너를 달아서 동작을 수행할까 하다가,
"버튼의 이미지를 바꾸는 역할"과 "리스트의 UI를 업데이트하는 역할"의 분리가 필요하겠다는 생각이 들어 새로운 방법으로 코드를 작성해보았다.

이 글에서 다루는 상태 관리는 비동기 통신의 결과로 받은 상태 관리가 아니라, 웹에 어떤 정보가 띄워질지에 대한 정보를 상태에서 다루고 상태가 바뀌는 시점에서 호출되는 콜백을 다루려고 한다.

예시 코드가 정석적이라고 볼수는 없지만, UI 내의 상태를 관리하는 하나의 방법으로 이렇게 시도할 수도 있다는 걸 후루룩 아시고 넘어가신다면 좋겠다.

Proxy란?

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Proxy 객체는 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 새로운 행동을 정의할 때 사용합니다.

Proxy를 사용하면 기존 객체의 속성이나 메서드를 덮어쓸 수 있고, 객체의 속성이나 메서드에 접근하는 행위에 대해 콜백을 먹여줄 수 있다. 다양한 예시는 위 mdn에서 확인할 수 있다.

https://stackoverflow.com/questions/36258502/why-has-object-observe-been-deprecated
https://ui.toast.com/weekly-pick/ko_20210413
proxy에 대해 더 알고 싶다면 위 자료들을 추천한다.

이 글에서는 Proxy의 덮어쓰기 기능보다는 Proxy가 갖고 있는 property들을 옵저빙되는 대상으로 삼고 set 메서드를 상태 변화 이후의 콜백 함수처럼 사용한 예시를 들 것이다.

Proxy의 set 메서드

set 메서드는 target, prop, value를 인자로 받는다.

target : set되기 전의 proxy가 갖고 있는 메소드와 프로퍼티
prop : set의 대상인 프로퍼티
value : set의 대상인 프로퍼티가 갖게 된 값

const proxyExample = new Proxy(
    {a:"",b:""}, 
    {
    set:function(target,prop,value){
        console.log(target, prop, value)
        Reflect.set(target, prop, value)
    }
})
proxyExample.a = "aa" // {a: "", b: ""} "a" "aa"
proxyExample.c = "c" // {a: "aa", b: ""} "c" "c"
console.log(proxyExample) // Proxy {a: "aa", b: "", c: "c"}

set 메서드는 위와 같이 proxy가 가진 값이 set될 때마다 이벤트를 발생시킬 수 있고, 어느 값이(prop) 어떻게(value) 바뀌는지도 받아올 수 있다.

소스 코드

Proxy를 사용하기 전까지는 버튼의 콜백 함수에서 hideItemsrevealItems를 작성했지만, 체크 여부를 판단하기에도 애매하고 코드가 중복되는 것 같아서 체크 여부를 저장하고 hideItemsrevealItems를 호출하는 역할을 Proxy의 setter에게 맡기는 방식으로 구현해 보았다.

<body>
    <div class="main-btn-wrapper" id="main-btn-nonmajor">
        <img class="main-btn-checkbox" src="./img/btn-checked-true.svg">
        <span>교양</span>
    </div>
    <div class="main-btn-wrapper" id="main-btn-major">
        <img class="main-btn-checkbox" src="./img/btn-checked-true.svg">
        <span>전공</span>
    </div>
    <div class="main-card-product" data-category="교양">선형대수학</div>
    <div class="main-card-product" data-category="교양">이산수학</div>
    <div class="main-card-product" data-category="전공">컴퓨터교육개론</div>
    <div class="main-card-product" data-category="교양">MBTI의 이해</div>
    <div class="main-card-product" data-category="전공">컴퓨터교재연구및지도법</div>
    <script src="./state-controller.js"></script>
</body>
const init = function(){
  	// 상수 이미지 URL 정의
    const BTN_CHECKED_TRUE = "./img/btn-checked-true.svg", BTN_CHECKED_FALSE = "./img/btn-checked-false.svg" 
    
  	// dom 요소들 담기
    const cards = document.querySelectorAll(".main-card-product")
    const nonmajorFilterButton = document.querySelector("#main-btn-nonmajor")
    const majorFilterButton = document.querySelector("#main-btn-major")
    
    const nonmajors = [...cards].filter(card => card.attributes["data-category"].value === "교양"), 
        majors = [...cards].filter(card => card.attributes["data-category"].value === "전공")
    
    // 함수 정의하기
    const hideItems = (elementList) => elementList.forEach(ele => ele.style.display="none")
    const revealItems = (elementList) => elementList.forEach(ele => ele.style.display="block")

    // Proxy 정의하기
    const proxiedState = new Proxy({
        nonmajor:true,
        major:true,
    }, {
        set: function(target, prop, value) {
            switch(prop) {
                case "nonmajor":
                    value ? revealItems(nonmajors) : hideItems(nonmajors)
                    break
                case "major":
                    value ? revealItems(majors) : hideItems(majors)
                    break
            }
            return Reflect.set(target, prop, value);
        }
    })

    // 버튼에 이벤트 콜백 붙이기
    nonmajorFilterButton.addEventListener("click", (e)=>{
        proxiedState.nonmajor = !proxiedState.nonmajor
        e.target.parentNode.querySelector(".main-btn-checkbox").src = proxiedState.nonmajor ? BTN_CHECKED_TRUE : BTN_CHECKED_FALSE
    })
    majorFilterButton.addEventListener("click", (e)=>{
        proxiedState.major = !proxiedState.major
        e.target.parentNode.querySelector(".main-btn-checkbox").src = proxiedState.major ? BTN_CHECKED_TRUE : BTN_CHECKED_FALSE
    })

    revealItems([...nonmajors,...majors])
}()

사실 범용성을 생각한다면 nonmajor와 major을 한 클래스에 넣기보다는 저런 기능을 수행하는 클래스를 하나 생성하여 nonmajor, major에 하나씩 붙여주는게 더 좋을 것 같다. 지금은 필터가 두가지라 괜찮지만, 더 많은 필터가 생길 때마다 switch case를 더 늘려줘야 하기 때문이다.
더 좋은 코드를 짠다면 이 포스팅에 덧붙이거나 새 포스팅을 올리도록 하겠다.

상태, proxy에 관한 좋은 의견이 있으시다면 언제든지 댓글 부탁드립니다. 감사합니다!

profile
뭐라도 더 하자~

1개의 댓글

comment-user-thumbnail
2022년 11월 24일

프록시를 공부하다보니 서진님의 블로그까지 왔네요~ 감사합니다~

답글 달기