je개발 회고

[ je 개발 회고 ] L 프로젝트 (5) - Vue 로 방대한 양의 데이터 다룰 때 주의할 점

Je-chan 2024. 3. 6. 22:18

1. 배경

  L 프로젝트는 Vue3 프레임워크를 기반으로 만들었다. 해당 도메인은 굉장히 방대한 양의 데이터(10초마다 데이터를 수집 후 저장)를 활용해 정보를 제공한다. 이는 화면에서 그려야할 게 많다는 것을 의미하기도 한다. 예를 들어, 우리 도메인 중에는 10초 단위의 데이터를 하루치 보여준다거나, 1분 단위의 데이터를 한 달 ~ 두 달의 기간으로 보여주기도 한다. 이게 단순히 tick 을 계산하면 10 * 6 * 1440 개, 1440 * 30 개 정도의 데이터지만, 이는 단순히 Tick 일뿐, 그려야 하는 범례나 데이터의 depth 도 존재해서 우리가 Max Range 를 걸어주지 않으면, 데이터를 받아와서 그리다가 브라우저가 다운되는 경우가 있었다.

단순 JSON 파일로 다 받아들이는데 460Mbps 속도의 네트워크에서 6초가 걸린다. 위의 API 호출은 총 208,812 개의 row 데이터를 가져왔다.

 

  부문장님은 "컨텐츠가 좋으면 느리게 렌더링되더라도 사람들은 기다리고 본다" 라고 말씀하시기 팀 전체적인 분위기로 브라우저 렌더링 속도에 대해 크게 민감하게 반응하지 않지만, 프론트엔드 개발자인 이상 이런 문제는 결코 간과할 수 없다고 생각한다. 그래서 렌더링 속도 개선을 위해 크게 뭔가 변화를 주지 않지만 나혼자서 할 수 있는 선에서 개선할 수 있는 방법이 뭐인지 생각해봤다.

 

2. Vue 의 ref 를 주의하자

(2-1) React 의 State :  useState()

  마침 위의 고민을 하고 있을 때 React 의 공식 문서를 읽으면서 React 동작 원리에 대해 공부하고 있었다. 그때, 내가 참신하게 봤던 것은 React 에서는 "얕은 복사" 를 한다는 것이다. 예를 들어서, 가상 DOM 을 캐싱할 때도 얕은 복사를 하기 때문에 props 로 넘겨주는 값이 객체고, 그 객체가 동일한 key-value 를 갖고 있다 하더라도 주소가 다르기에 다른 값으로 판단해 자식 컴포넌트를 리렌더링한다. 때문에 자식이 리렌더링 되는 것을 방지하기 위해서는 React.memo 나 memoization Hook(useMemo, useCallback) 을 활용해서 리렌더링을 방지한다. 

 

  이는 React 의 상태관리 Hook 인 useState 도 마찬가지다. useState 는 객체에 대한 직접적인 mutation 이 불가능하다. 반드시 상태 업데이터 함수를 통해서만 상태를 수정할 수 있으며, 동일한 주소의 객체를 업데이트 해서는 안 된다. 때문에 객체의 depth 가 깊어지면 깊어질 수록, 상태를 업데이트하기 어려우며 이를 방지하기 위해서 lodash 의 deepCopy 를 사용하든가, Immer 라는 것을 활용하기도 한다. 

 

https://react.dev/learn/updating-objects-in-state

 

Updating Objects in State – React

The library for web and native user interfaces

react.dev

 

(2-2) Vue 의 State :  ref()

  그러나 Vue 에서는 무엇을 캐싱해야할지, 그리고 상태를 deepCopy 해야할지 고민할 필요가 없다. Vue 는 깊이 있게 캐싱하기 때문이다. Vue 의 모든 컴포넌트는 객체로 props 를 넘겨 받더라도 모든 depth 를 다 감시하기 때문에 위의 상황을 고민할 필요가 없다. 또, ref 라고 하는 Vue 의 상태관리 함수는 객체를 넘겨 받으면 몇 depth 를 갖든 객체 상태에 직접적인 Mutation 을 가해도 반응성 있게 바뀐다.

 

https://ko.vuejs.org/api/reactivity-core.html#ref

 

Vue.js

Vue.js - The Progressive JavaScript Framework

vuejs.org

 

  그러나 모든 것은 trade-off 다. 모든 것을 다 반응성 있게 관리한다는 것은 그만큼 메모리를 많이 잡아 먹는다는 것을 의미한다. 우리가 받아오는 대용량의 데이터처럼 굉장히 양이 많고, depth 가 깊은 데이터는, 그 깊이 만큼 모두 반응성 있도록 만들어야 하기에 메모리를 굉장히 많이 잡아 먹는다. 이러면서 성능이 저하되는 것이다.

 

(2-3) Vue 의 State : shallowRef()

  이런 문제점을 Vue 도 알고 있었는지, shallowRef() 라는 함수를 만들었다. 이건, useState 와 비슷하게 깊이 있게 반응성을 체크하지 않고, 오직 " 상태.value = 새로운 상태 " 방식으로만 상태를 업데이트 할 수 있다. 공식문서에도 나와 있지만 대규모 데이터 구조의 성능 최적화에 사용된다고 명시돼 있다.

 

https://ko.vuejs.org/api/reactivity-advanced.html#shallowref

 

Vue.js

Vue.js - The Progressive JavaScript Framework

vuejs.org

 

  사수와 나는 이 L 프로젝트에서 이 shallowRef 를 활용해 useState() 처럼 사용하고 있다.

const useState = <T>(initState: T): [ShallowRef<T>, (newState: T) => void ] => {
  const state = shallowRef<T>(initState)
  const setState = (newState: T) => {
    state.value = newState
  }

  return [state, setState]
}

  

  (  다음에 또 포스팅 하겠지만... 지금 저 useState 를 그냥 사용하고 있지는 않고, 이슈가 있어 고도화 해서 활용하고 있다. )

 

 

3. 그래서 성능은 개선됐는가?

  결론부터 말하면 성능 최적화에 성공했다. 약간, 아쉬운 점이 있다면 실제 내가 사용하고 있는 곳은 비교해볼 생각을 못하고 바로 적용해버렸기 때문에 비교해볼 수 있는 자료를 내가 따로 만들지 못했다. 그런데 최근, 우리 회사 도메인 중 한 곳에 위와 같은 방법(shallowRef)을 소개해드렸는데 성능 최적화에 성공했다고 해서 어느정도의 개선을 이뤄냈는지 여쭤봤다. console.time 을 활용하셨는데, 11326.30 ms 걸리던 건 4642.36 ms로 단축됐고, 16100.97 ms 걸리던 건 4756.76 ms, 11690.72 ms 걸리는 건 5220.70 ms로 단축됐다. 결론적으로 절반 가량 시간을 단축했다.

 

 

4. Vue 는 왜 ref 를 고집할까? 

  이렇게 개선하고 나니 드는 생각은... 왜 Vue 는 ref 를 고수하는지에 대한 의문이다. 성능이 최적화 되는 좋은 shallowRef 가 있음에도 불구하고 왜 아직 Vue 는 ref 를 메인 상태 관리 함수로 내세울까?

 

  개인적인 생각이지만, 그 이유는 Vue 가 태생적으로 양방향 데이터 흐름이기 때문일 것이다.

 

  HTML 에서 사용자로부터 입력 받은 값이 실제 상태를 조작할 수 있어야 한다. 그런데 만약, shallowRef 처럼 객체를 깊이 있게 반응성을 주지 못한다면, 양방향이 깨지게 될 상황이 생길 것이다. 그래서 Vue 는 ref 처럼 깊이 있게 반응성을 줄 수밖에 없지 않나 싶다. 그런 면에서 React 는 데이터 흐름이 단방향이기에 오히려 사용자로부터 입력 받은 값이 바로 상태를 조작할 수 없게 얕은 복사로 방지한 건 아닐까 싶기도 하다. (물론 애초에 상태를 조작할 수 있는 건 업데이터 함수뿐이지만)