-
[JavaScript] ReactivityFront-End/JavaScript 2024. 1. 19. 04:27
🍀 목차
글의 목적
Reactivity
사례글의 목적
"React, Angular, Vue 같은 프론트엔드 프레임워크를 사용하려는 이유가 무엇인가?"에 대해서는 여러 이유가 나올 수 있다. 컴포넌트 기반으로 아키텍처를 구성하고 싶으므로, Vanilla JS로 개발하는 것보다 성능도 개선되어 있고 생산성도 높아지므로 등등...
이유 중 "Reactivity 때문"도 있을 수 있다. React, Vue와 같은 프론트엔드 프레임워크의 주요 이점 중 하나인 Reactivity는 무엇이고, 어떻게 적용되어 있을까?
해당 글은 추상적인 개념이었던 Reactivity를 조금이나마 이해해보기 위해 작성되었다.
ReactivityReactivity는
JavaScript 애플리케이션의 메모리에 있는 내용을 DOM에 HTML로 반영하는 기능
이다.SPA 프레임워크에서 컴포넌트의 상태가 변경되면 렌더링이 일어나 UI를 자동으로 업데이트한다거나,
데이터의 변화에 따라 어떤 코드가 자동으로 실행되게 한다면 그 애플리케이션은 Reactivity 하다고 할 수 있다.
JavaScript에서의 예시를 알아보자.
const data = {}; // https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty // Object.defineProperty(obj, prop, descriptor) // obj(속성을 정의할 객체), prop(새로 정의하거나 수정하려는 속성이 이름), descriptor(속성 서술자) Object.defineProperty(data, 'message', { get() { console.log('Getting the value!'); return data._message; }, set(value) { console.log('Setting the value!'); data._message = value; }, enumerable: true, // true : 전개 연산자가 속성을 볼 수 있음(열거 가능). configurable: true // true : 해당 속성을 재정의 가능. }); console.log(data.message); // getter 함수 호출, 'Getting the value!', undefined 출력 data.message = 'Updated Message!'; // setter 호출, 'Setting the value!', console.log(data.message); // getter 함수 호출, 'Getting the value!', 'Updated Message!' 출력
Object.definedProperty
를 사용하여 data 객체의 message 속성을 정의, get, set 메소드를 활용하여 값을 읽고 쓸 때마다 자동으로 message 속성을 업데이트한다.Object.definedProperty를 쓰지 않더라도 data.message를 변경시킬 수는 있지만 set, get 시에 부가적인 로직이 들어간다면 Object.definedProperty를 사용하면 유용하다.
각 속성에 대한 정의를 개별적으로 하기 싫다면
Proxy
를 사용할 수 있다. Proxy는 단일 핸들러로 전체 객체에 대한 핸들러를 동적으로 정의할 수 있다.두 번째 예시로는 Proxy를 사용한 화면 갱신을 알아보자.
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Proxy Reactivity Example</title> </head> <body id="app"> <p></p> <button onClick="updateMessage()">Message</button> </body> <script> let count = 0; const data = { message: 'First Message!' }; const reactiveData = new Proxy(data, { set(target, key, value) { target[key] = value; updateView(); return true; }, }); function updateView() { document.querySelector('#app p').innerHTML = reactiveData.message; } updateView(); function updateMessage() { reactiveData.message = 'Updated Message!' + count++; } </script> </html>
Proxy로 원본 객체를 감싸고, 핸들러에 set을 정의한다. 객체의 상태를 변경시킬 때 관련 코드 블록이 자동으로 실행된다. 위의 코드에서는
updateMessage()
로 data의 message가 변경되면 p 태그 안의 내용을Updated Message!+count
로 변경시킨다.애플리케이션을 개발하다 목록에서 항목을 제거해야 할 일이 발생했다면, 그에 따라 총 항목 개수(ex. total)도 -1 해줘야 할 것이고 장바구니에 해당 항목이 담겨있었다면 업데이트시키는 등 굉장히 복잡해질 수 있다.
그렇기에 "항목이 제거되면 관련 요소들을 업데이트 시키는 코드"를 기록해 두고 Object.defineProperty의 setter 같은 곳에 state의 업데이트 후에 그 코드들을 실행시키게 할 수 있다. 기록된 코드를 재실행할 수 있는 trigger가 되도록 만드는 것이다. 정적으로 속성을 정의하기 힘들 경우 Proxy를 이용하여 객체에 대한 핸들러를 동적으로 정의할 것이고...
그런데 너무 복잡하지 않은가...?
데이터 한 개에 의존된 데이터까지 신경을 써야 하고 그러다 보니 코드베이스가 점점 복잡해진다. 사실 개발자가 신경 쓰고 싶은 것만 남기면 아래 코드와 비슷해질 것이다.
<main> <button id="add-button">Add one to: 0</button> <button id="remove-button">Remove one from: 0</button> <ul id="list"></ul> </main> <script> // `count`가 변경되면 DOM에 자동 업데이트되면 Magical land! let count = 0; addBtn.addEventListener('click', () => { count++; }); removeBtn.addEventListener('click', () => { count--; }); </script>
이러한 Magical land를 개발자가 신경 쓸 필요 없이 라이브러리, 프레임워크가 해준다면 사용하지 않을 이유가 없다. 글의 목적에서도 서술했지만 React, Vue와 같은 프론트엔드 프레임워크의 주요 이점 중 하나는 Reactivity라고 했다. 어떻게 적용되어 있을까?
사례
React, Vue 같은 SPA 프레임워크들은 이러한 Reactivity를 더 개선된 형식으로 제공하여 개발자가 JavaScript 데이터에만 집중하여 개발할 수 있도록 도와준다. React, Vue는
Virtual DOM
을 활용하여 데이터의 변화를 DOM에 HTML로 반영시킨다(Reactivity). 이 과정에서 바뀔 때마다 DOM을 업데이트시키는 것이 아닌, 실제 DOM의 복제본 같은 Virtual DOM을 이용하여 Virtual DOM을 수정시키고 최종적으로 실제 DOM과 비교하여 바뀌어야 할 부분만을 업데이트시킬 수 있도록 도운다.아래의 코드를 확인하면 위의 사례들처럼 객체 속성 하나하나에 getter/setter를 달거나, 객체 전체에 전달할 핸들러를 작성할 필요 없이 간단히 데이터의 변경과 반영을 작성할 수 있는 것을 알 수 있다. 결과적으로 위에서 '개발자가 신경 써야 할 부분'만 깔끔하게 남겨놓은 것이다.
React
const App = () => { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(count + 1)}>Add one to: {count}</button> <button onClick={() => setCount(count - 1)}> Remove one from: {count} </button> <ul> {Array.from({ length: count }).map((_, i) => ( <li>List item {i}</li> ))} </ul> </div> ); }
Vue
<script setup> import { ref } from "vue"; const count = ref(0); </script> <template> <button @click="count++">Add one to: {{ count }}</button> <button @click="count--">Remove one from: {{ count }}</button> <ul id="list"> <li v-for="(_, i) of [].constructor(count)">List item {{ i }}</li> </ul> </template>
참고자료
https://blog.rhostem.com/posts/2018-09-12-javascript-reactivity