React 응용 프로그램을 최적화하는 방법 > 시티즌 인사이트

본문 바로가기

IT&개발 정보 React 응용 프로그램을 최적화하는 방법

페이지 정보

작성자 GCK딜런 작성일 22-02-28 10:25 조회 1,031회 댓글 0건

본문

최적화는 인터넷 응용 프로그램을 구축할 때 디자인 프로세스의 중요한 부분입니다. 연구에 따르면 웹사이트에서 페이지를 로드하는 데 걸리는 이상적인 시간은 2~3초라고 합니다. 이 시간보다 더 길어지면 사용자가 페이지 로드 전에 해당 페이지를 떠날 가능성이 많이 늘어나고 더 빨리 로드되는 페이지에서는 사용자 기반의 광고 수익이 늘어난다고 합니다.


모바일 장치에서 페이지를 방문하는 경우 최적화는 훨씬 중요한 문제가 됩니다. 로드 시간이 길어지면 데스크톱에서 로드하는 페이지의 경우보다 이탈률이 훨씬 높아집니다. 모바일 장치에서 사이트에 방문하는 사용자 수가 늘어남에 따라 Google은 모바일 우선 인덱싱 선호로 방향을 전환했습니다.


즉, Google이 Googlebot의 스마트폰 에이전트 기반의 웹사이트 인덱싱을 더 선호하게 됨에 따라 최적화가 훨씬 더 중요해졌다는 의미입니다.


오늘날의 대형 응용 프로그램은 대용량 데이터 집합과 번들을 취급하고 복잡한 계산을 수행합니다. 다행히 React와 Wijmo의 FlexGrid에서는 React DataGrid 응용 프로그램을 최적화할 수 있는 옵션을 제공합니다. 이 블로그에서는 다음과 같은 주제를 다루겠습니다.


  • Observable에서 구독 취소

  • 컴포넌트 상태를 계속 로컬로 유지하여 다시 렌더링 방지

  • 코드를 분할하여 번들 크기를 작게 유지

  • FlexGrid 가상화로 DOM 요소 감소

  • React 컴포넌트 기억



Observable에서 구독 취소


Observable은 말하는 그대로 합니다. 즉, 사용자가 관찰하고 해당 조치를 취하려는 것입니다. 개발자는 Observable을 사용하여 데이터를 응용 프로그램과 응용 프로그램 주위로 쉽게 전달할 수 있습니다. React에서는 이벤트 처리, 비동기 프로그래밍 그리고 다양한 데이터 집합 관리에 사용됩니다. 그러나 Observable을 잘못 사용하면 성능이 저하되고 심각한 메모리 관리 문제가 발생할 수 있습니다.


Observable을 사용할 때는 데이터를 얻고 데이터에 변동이 있는지 확인하기 위해 구독 메서드를 사용하는데, 이 모두가 비동기로 이루어집니다. 일반적으로 사용자는 데이터를 얻기 위해 HTTP 호출을 수행하는 서비스의 메서드를 구독하며, 대부분의 경우 이 작업은 컴포넌트의 componentDidMount() 메서드 내에서 수행됩니다.


Mock 컴포넌트

import React, { Component } from 'react';  
import { BrowserRouter as Router, Route } from 'react-router-dom';
​
import { mockService } from '@/services';
​
class Mock extends React.Component {  
  constructor(props) {  
      super(props);  
      this.state = {  
          mockData: []  
      };  
  }
​
  componentDidMount() {  
      this.mockSubscription = mockService.getAPIData().subscribe(data => {  
          this.setState({ mockData: data });  
      }  
  }
​
  render() { ... }  
}



완료되면 이제 API에서 데이터에 비동기식으로 액세스하고 구독을 통해 데이터에 대한 변경 내용을 모두 수신할 수 있습니다. 그러나 unsubscribe() 메서드라는 중요한 세부 정보가 누락되었습니다. 컴포넌트를 그대로 설정하면 연결에서 벗어나더라도 구독 연결이 계속 유지됩니다. 이는 메모리 누수로 이어지고 심각한 성능 문제를 일으킬 수 있습니다.


이 컴포넌트가 로드된 상태에서는 이 연결이 계속 유지되기를 원하므로 우리는 DOM에서 컴포넌트가 제거되어 구독이 취소될 때까지 계속 기다릴 것입니다. React에서는 이렇게 할 수 있는 componentWillUnmount()라고 하는 라이프사이클 후크를 제공합니다.

import React, { Component } from 'react';  
import { BrowserRouter as Router, Route } from 'react-router-dom';
​
import { mockService } from './services';
​
class Mock extends React.Component {  
  constructor(props) {  
      super(props);  
      this.state = {  
          mockData: []  
      };  
  }
​
  componentDidMount() {  
      this.mockSubscription = mockService.getAPIData().subscribe(data => {  
          this.setState({ mockData: data });  
      }  
  }
​
  componentWillUnmount {  
      this.mockSubscription.unsubscribe();  
  }
​
  render() { ... }  
}


이제 사용자가 이 컴포넌트에서 나가면 응용 프로그램은 이 Observable에서 구독을 취소하여 연결을 닫고 메모리 누수를 방지합니다. Observable에 대해 자세히 알아보려면 추가 정보를 제공하는 RxJS 문서를 참조하세요.



컴포넌트 상태를 계속 로컬로 유지하여 다시 렌더링 방지


상태는 컴포넌트 전체에서 액세스할 수 있는 컴포넌트 내에 데이터를 저장하도록 합니다. 상태가 수정되면 컴포넌트가 다시 렌더링될 뿐만 아니라 자식 컴포넌트도 다시 렌더링됩니다.

컴포넌트를 쪼개고, 데이터를 자식 컴포넌트에 대해 로컬로 유지하고, 상태 변경 시 추가 컴포넌트가 렌더링되지 않도록 방지하여 대규모 다시 렌더링을 방지할 수 있습니다.


부모 컴포넌트

import React, { Component } from 'react';  
import { ChildComponent } from './childcomponent';
​
class ParentComponent extends React.Component {  
  constructor(props) {  
      this.state = {  
          count: 0;  
      }  
  }  
  incrementCount(event) {  
      this.setState({ count: ++this.state.counter });  
  }
​
  render() {  
      return <div>  
              <div>Count: </div>  
              <button onClick={this.incrementCount.bind(this)}>Increment Count</button>  
              <ChildComponent />  
          </div>  
  }  
}


자식 컴포넌트

import React, { Component } from 'react';  
import { ChildComponent } from './childcomponent';
​
class ChildComponent extends React.Component {  
  constructor(props) {  
      this.state = {}  
  }
​
  render() {  
      return <div><p>This is our Child Component</p></div>  
  }  
}


이제 버튼을 클릭할 때마다 카운트가 1씩 증분하는데 ParentComponent 및 ChildComponent가 둘 다 다시 렌더링됩니다. 컴포넌트가 상대적으로 작기 때문에 이 시나리오에서는 크게 느껴지지 않을 수 있지만 컴포넌트가 커지면 각 컴포넌트를 다시 렌더링하면 응용 프로그램의 성능 측면에서 비용이 발생합니다.

여기에서는 ButtonIncrement라고 하는 새 컴포넌트를 만들어 모든 컴포넌트가 다시 렌더링되지 않도록 방지하고 버튼의 기능을 해당 컴포넌트로 이전할 수 있습니다.


부모 컴포넌트

import React, { Component } from 'react';  
import { ChildComponent } from './childcomponent';  
import { ButtonIncrement } from './buttonincrement';
​
class ParentComponent extends React.Component {  
  constructor(props) {  
      this.state = {}  
  }
​
  render() {  
      return <div>  
          <ButtonIncrement />  
          <ChildComponent />  
      </div>  
  }  
}


ButtonIncrement 컴포넌트

import React, { Component } from 'react';
​
class ButtonIncrement extends React.Component {  
  constructor(props) {  
      this.state = {  
          count: 0;  
      }  
  }  
  incrementCount(event) {  
      this.setState({ count: ++this.state.counter });  
  }
​
  render() {  
      return <div>  
          <div>Count: </div>  
          <button onClick={this.incrementCount.bind(this)}>Increment Count</button>  
      </div>  
  }  
}



이제 버튼과 부모 컴포넌트의 증분 부분을 컴포넌트로 나누었으므로 버튼 클릭 시 더 이상 부모 컴포넌트와 자식 컴포넌트가 둘 다 다시 렌더링되지 않고 대신 ButtonIncrement 컴포넌트만 다시 렌더링됩니다. 이렇게 하면 DOM이 과도하게 렌더링되지 않도록 할 수 있습니다.



코드를 분할하여 번들 크기를 작게 유지


기본적으로 React 응용 프로그램이 브라우저로 로드되면 응용 프로그램의 모든 코드가 포함된 번들 파일이 로드되어 한 번에 사용자에게 제공됩니다. React에서는 응용 프로그램 실행에 필요해 작성한 모든 파일을 병합하여 번들을 생성합니다.


이는 페이지에서 수행해야 하는 요청 수를 줄여 페이지 로드 속도를 높이기 위해 수행됩니다. 그러나 응용 프로그램이 커질수록 번들 파일 크기 역시 늘어나고 특정 페이지에서 사용자에게 필요 없는 콘텐츠를 번들 파일에 로드하는 경우가 발생합니다.


이로 인해 페이지의 로드 속도가 줄고 사용자가 현재 사용하지 않는 응용 프로그램의 부분과 관련된 불필요한 코드를 브라우저에 로드하게 됩니다.


React는 코드 분할이라고 하는 프로세스를 통해 번들 파일을 더 작은 여러 개의 번들 크기로 나눌 수 있는 방법을 제공합니다. 코드 분할은 동적 import() 메서드를 사용한 다음 React.lazy를 사용하여 요청 시 컴포넌트를 지연 로드하여 수행합니다. 그러면 크고 복잡한 React 응용 프로그램을 로드할 때 성능이 크게 향상됩니다. 예를 들어, 자식 컴포넌트에 대한 가져오기 문이 있다고 가정해 보겠습니다.


import ChildComponent from './childcomponent';</td>


이 문은 브라우저에서 응용 프로그램을 렌더링할 때 로드되는 기본 번들에 ChildComponent를 자동으로 포함합니다. 그러나 이 ChildComponent를 고유한 번들로 분할하고 컴포넌트 호출 시 해당 번들을 로드하고자 합니다. 새 가져오기 문은 다음과 같습니다.


const ChildComponent = React.lazy(() => import('./childcomponent'));</td>


React.lazy는 동적 import()를 사용하는 함수를 가져옵니다. 이 호출은 React 컴포넌트를 포함하는 기본 내보내기를 사용하여 모듈로 확인되는 Promise를 반환합니다. 이제 이 컴포넌트를 렌더링하려고 하면 Suspense 컴포넌트 내에서 렌더링해야 합니다. 그러면 지연 컴포넌트가 로드되기를 기다리는 동안 몇 가지 대체 콘텐츠(예: 로드 중 아이콘 또는 진행률 표시줄)를 표시할 수 있습니다.


import React, { Suspense } from 'react';  
const ChildComponent = React.lazy(() => import('./childcomponent'));
​
function ParentComponent() {  
  return (  
      <div>  
          <Suspense fallback={<div>Loading...</div>}  
              <ChildComponent />  
          </Suspense>  
      </div>  
  )  
}


ChildComponent가 브라우저로 로드될 때까지 기다리는 동안 "로드 중..."이라는 텍스트를 표시합니다.


코드 분할에 대해 자세히 알아볼 수 있도록 React에서는 관련 문서를 다양하게 제공하고 있습니다. 이러한 문서는 여기서 찾을 수 있습니다.



React 컴포넌트 기억


이 글의 이전 섹션에서는 ParentComponent가 자신과 ChildComponent를 다시 렌더링하지 않도록 하기 위한 코드 리팩터링에 대해 살펴보았습니다. 이제 메모이제이션(Memoization)을 사용하여 응용 프로그램의 성능을 높이는 방법에 관해 알아보겠습니다.


메모이제이션은 컴포넌트를 캐싱하도록 허용하는 최적화 전략으로, 결과를 메모리에 저장한 다음 동일한 입력에 대해 캐시된 결과를 반환합니다. 기본적으로, 자식 컴포넌트가 속성을 수신하면 기억된 컴포넌트가 속성에 대해 얕은 비교를 수행하고 속성이 변경되지 않은 경우 자식 컴포넌트 다시 렌더링하기를 건너뜁니다. 이 작업은 React.memo를 사용하여 수행합니다. 다음 컴포넌트를 예로 들어보겠습니다.


export function Car({ brand, model, year }) {  
  return (  
      <div>  
          <div>Car Brand: { brand }</div>  
          <div>Car Model: { model }</div>  
          <div>Year Released: { year }</div>  
      </div>  
  );  
}
​
export const MemoizedCar = React.memo(Car);



여기에서는 Car 컴포넌트와 정확히 동일한 내용을 제공하는 MemoizedCar라는 컴포넌트를 반환하는데, MemoizedCar 컴포넌트는 렌더링을 기억하고 있다는 한 가지 차이점이 있습니다. 이제 React에서는 렌더링 간에 모델 및 연도 속성이 동일하기만 하면 기억된 내용을 재사용합니다. 예를 들어, 다음 코드를 사용해 보세요.


<MemoizedCar brand="Toyota" model="Camrey" year="2022" />


이 코드를 처음 실행하면 응용 프로그램이 MemoizedCar 컴포넌트 렌더링 프로세스를 거칩니다. React는 해당 컴포넌트에 대해 제공된 속성에 대한 얕은 비교를 수행하여 변경되지 않은 경우 DOM을 렌더링할 때 이 컴포넌트를 건너뜁니다. 따라서 이미 렌더링한 내용을 재사용하여 성능을 높입니다. 응용 프로그램이 컴포넌트를 기억하도록 할지 여부를 선택할 때 몇 가지 고려 사항이 있습니다. React.memo를 사용할 때 다음 내용을 확인해야 합니다.


  • 컴포넌트가 잘 작동하고, 동일한 속성이 주어졌을 때 항상 같은 출력을 렌더링합니다.

  • 컴포넌트가 자주 렌더링합니다.

  • 렌더링 중에는 (일반적으로) 컴포넌트에 동일한 속성이 제공됩니다.

  • 컴포넌트에 속성 동등성 검사를 판단할 수 있는 적절한 양의 UI 요소가 포함되어 있습니다.


이제 이 모든 도구를 마음대로 사용함으로써 사용자가 응용 프로그램에서 작업할 때 React가 수행해야 할 작업량을 줄이면서 컴포넌트를 렌더링하고 제거할 때 브라우저의 DOM 부하를 줄일 수도 있습니다.



WIJMO FlexGrid 가상화로 DOM 요소 감소

GrapeCity는 위와 같이 고성능의 React 애플리케이션 개발을 원하시는 React 개발자를 위한 업계 최고 성능의 DataGrid 및 UI 컨트롤인 Wijmo(위즈모) 제품을 제공합니다.

특히, 대량의 데이터를 위한 DataGrid 컨트롤인 FlexGrid는 국내 기업들에서 많은 사랑을 받고, 그 안정성을 인정 받았습니다.

 Wijmo FlexGrid의 기본 목적은 JavaScript 개체를 사용자가 조작할 수 있는 DOM 요소로 변환하는 것입니다. 많은 인스턴스에서 이 데이터는 수백, 수천 또는 수백만 행의 데이터로 구성됩니다. 따라서 이러한 각 항목에 대해 DOM 요소를 생성할 경우 리소스를 많이 소모하게 되어 페이지 속도가 느리고 비대해질 수 있습니다.

가상화는 사용자에게 보이는 데이터 부분을 추적하고 해당 섹션만 DOM에서 렌더링하는 프로세스입니다. 이렇게 하면 특히 방대한 데이터 집합으로 작업할 때 문서 트리의 DOM 요소 수가 많이 감소하고 성능이 훨씬 향상됩니다.

Wijmo는 viewRange 속성을 통해 데이터의 보이는 부분을 노출합니다. 사용자가 화면 크기를 변경하거나 그리드를 스크롤할 때마다 viewRange도 업데이트되어 결국 DOM을 업데이트하게 됩니다.

DOM의 요소 수가 증가하는 것을 방지하기 위해 FlexGrid는 viewRange에서 벗어나는 셀을 가져와 재활용함으로써 저장하고 있던 데이터에서 제거하고 viewRange로 들어오는 새 데이터로 다시 채웁니다. 이를 통해 DOM을 간단하게 만들고 응용 프로그램을 빠르고 가볍게 유지합니다.

이 샘플에서 관련 내용을 확인할 수 있습니다.

dom 요소

보다시피 현재 그리드에는 100개의 데이터 행이 있고 60개의 셀 요소만 DOM에 의해 렌더링되고 있습니다. 다음 코드를 사용하여 이 수치를 얻을 수 있습니다.

flexGrid.updatedView.addHandler((s, e) => {  
  this.setState({  
      rowCount: s.rows.length.toString(),  
      cellCount: s.hostElement.querySelectorAll(".wj-cell").length.toString()  
  });  
});</td>


s.hostElement.querySelectorAll('.wj-cell') 메서드는 .wj-cell 클래스가 추가되어 DOM에서 렌더링된 요소의 배열을 반환합니다. 그리드를 아래로 스크롤하면 FlexGrid 내의 데이터 행 수는 늘어나고 셀 요소 수는 그대로인 것을 확인할 수 있습니다.

dom 요소

그럼 즐거운 코딩하세요!

  • 페이스북으로 공유
  • 트위터로  공유
  • 구글플러스로 공유
  • 카카오톡으로 보내기

댓글목록

등록된 댓글이 없습니다.

그레이프시티 홈페이지를 통해 제품에 대해서 더 자세히 알아 보세요!
홈페이지 바로가기

인기글

더보기
  • 인기 게시물이 없습니다.
그레이프시티 홈페이지를 통해 제품에 대해서 더 자세히 알아 보세요!
홈페이지 바로가기
이메일 : sales-kor@grapecity.com | 전화 : 1670-0583 | 경기도 안양시 동안구 시민대로 230, B-703(관양동, 아크로타워) 그레이프시티(주) 대표자 : 허경명 | 사업자등록번호 : 123-84-00981 | 통신판매업신고번호 : 2013-경기안양-00331 Copyright ⓒ 2022 GrapeCity inc.