> 이 글은 [원문](https://blog.bitsrc.io/10-ways-to-optimize-your-react-apps-performance-e5e437c9abce)을 번역한 글 입니다. 최적화는 모든 소프트웨어, 특히 웹 앱을 개발할때 가장 신경을 써야 하는 것 중 하나이다. Angular, React 또는 다른 자바스크립트 프레임워크는 이와 관련해서 좋은 설정과 기능을 가지고 있다. 이 포스터에서는 앱의 성능을 최적화하는 데 도움이 되는 기능과 방법들에 대해서 알아보도록 하겠다. 코드를 최적화하기 위해 사용하는 특정 패턴과 방법에 상관 없이 **코드를 DRY하게 유지**하는 것은 매우 중요하다. DRY하게 유지하기 위해 최적화된 코드를 작성하는데 도움이 되는 구성 요소는 항상 재사용하도록 해야 한다. 만약 개발을 하면서 더 많은 시간을 훌룡한 코드를 개발하는데 쓰고 더 적은 시간을 평범한 코드를 개발하는데 (실수할 가능성이 훨씬 더 큰) 쓴다면 놀라운 일들이 일어날 것이다. 위와 같이 말하기는 했지만, 코드를 재사용하는 것은 큰 코드베이스나 다른 리포지토리를 다룰 때 어려운 일이 될 수 있는데, 크게 두가지 이유가 있다. - 재사용하기 유용한 코드 부분을 알지 못한다. - 리파지토리 간에 코드를 공유하는 전통적인 방식은 패키지를 통해 이루어지며, 이것은 복잡한 설정이 필요하다. 위와 같은 두가지 문제를 해결하기 위하여 **[Bit](https://bit.dev/)** ([Github](https://github.com/teambit/bit)) 과 같은 툴을 사용하는 것을 권장한다. Bit은 코드베이스에서 구성요소를 분리하여 bit.dev에 공유할 수 있도록 도와준다. bir.dev의 인상 깊은 검색 엔진으로 쉽게 관련 Component들을 찾을 수 있다. - 좋은 코드는 좋은 업무 습관에서 시작한다. ## 1. useMemo() 이 함수는 React Hook 중 하나로서 React에서 CPU 소모가 심한 함수들을 캐싱하기 위해 사용된다. 예시를 보도록 하자: ``` jsx function App() { const [count, setCount] = useState(0) const expFunc = (count)=> { waitSync(3000); return count * 90; } const resCount = expFunc(count) return ( <> Count: {resCount} setCount(e.target.value)} placeholder="Set Count" /> > ) } ``` 위 코드에 `expFunc`은 3분후 실행되는 비싼 함수이다. 이 함수는 `count`를 입력받아 3분을 기다린 후 90을 곱하여 리턴한다. 또한 `useState` hook에서 `count` 변수를 받아 `expFunc`을 실행하는 `resCount` 또한 확인할 수 있다. 여기서 `count`는 입력할 때마다 값이 변경되어야 한다. 아무거나 입력할 때마다 앱 컴포넌트가 다시 렌더링 되어 `expFunc` 함수가 호출 된다. 계속 입력을 하면 대규모 성능 병목 현상을 유발하는 기능이 실행되고 있다는 것을 알게 될 것이다. 각각의 입력마다 렌더링하는 데 최소 3분이 소요된다. 만약 `3`을 입력하게 되면 expFunc은 3분동안 실행되고 다시 `3`을 입력하면 또 3분간 실행이 된다. 하지만 이전 입력과 같이 때문에 두번째 입력에서는 다시 실행되어서는 안되고 결과를 어딘가 저장한 후 `expFunc`이 실행되지 않고 값을 리턴해야 한다. 위와 같은 문제는 `useMemo`를 통해 expFunc을 최적화 함으로써 해결할 수 있다. useMemo는 아래와 같은 구조를 가진다. ``` jsx useMemo(()=> func, [input_dependency]) ``` `func`은 캐시하고 싶은 함수이고, `input_dependency`는 useMemo가 캐시할 func에 대한 입력의 배열로서 해당 값들이 변경되면 func이 호출된다. 이제, useMemo를 이용하여 위의 코드를 최적화 해보도록 하자: ```jsx function App() { const [count, setCount] = useState(0) const expFunc = (count)=> { waitSync(3000); return count * 90; } const resCount = useMemo(()=> { return expFunc(count) }, [count]) return ( <> Count: {resCount} setCount(e.target.value)} placeholder="Set Count" /> > ) } ``` 이제 expFunc은 입력에 대해 캐싱되며 동일한 입력이 다시 발생할 때 useMemo는 expFunc을 호출하지 않고 입력에 대해 캐시된 결과값을 리턴한다. 이것은 앱 컴포넌트를 최적화 시키는 방법 중 하나이다. useMemo 캐싱 기술을 이용해 성능을 향상시키고, 함수형 컴포넌트에서는 prop 값들 또한 캐싱하는 것 또한 도움이 된다. ## 2. 가상화된 List 만약 거대한 list data를 렌더링 한다면 브라우저의 viewport에 보여지는 부분만 렌더링하고 나머지는 스크롤 할때 보여지도록 하는 것을 권장한다. 이것은 "windowing" 이라 부르며, 많은 React 라이브러리들이 존재한다. 이 중 Brian Vaughn이 개발한 [react-window](https://github.com/bvaughn/react-window)와 [react-virtualized](https://github.com/bvaughn/react-virtualized)가 있다. ## 3. React.PureComponent Class Component에서 ShouldComponentUpdate의 역활과 비슷한 React.PureComponent가 있다. React.PureComponent는 기본 component 바탕에서 state와 prop값을 체크하여 component가 업데이트 되어야 하는지 확인한다. 예제에서 shouldComponent 부분을 변환해 보도록 하겠다. ``` jsx class ReactComponent extends Component { constructor(props, context) { super(props, context) this.state = { data: null } this.inputValue = null } handleClick = () => { this.setState({data: this.inputValue}) } onChange = (evt) => { this.inputValue = evt.target.value } shouldComponentUpdate( nextProps,nextState) { if(nextState.data === this.state.data) return false return true } render() { l("rendering App") return (
) } } ``` React.PureComponent를 사용한다면: ```jsx class ReactComponent extends React.PureComponent { constructor(props, context) { super(props, context) this.state = { data: null } this.inputValue = null } handleClick = () => { this.setState({data: this.inputValue}) } onChange = (evt) => { this.inputValue = evt.target.value } render() { l("rendering App") return (
) } } ``` 위와 같이 shouldComponentUpdate를 지우고 React.PureComponent를 상속받도록 하였다. text 창에 2를 입력하고 `Click Me`를 연속으로 누르면 ReactComponent는 한번 렌더링 된 후 다시는 렌더링 되지 않는 것을 볼 수 있다. 이것은 단순히 객체 참조만 비교하는 것이 아니라 이전 props와 state 객체들과 변경할 state와 props를 얇게 비교한다. React.PureComponent는 component가 렌더링 되는 횟수를 줄임으로서 최적화를 하게 해준다. ## 4. Caching functions 함수는 JSX 컴포넌트 render 메소드 안에서 호출 될 수 있다. ``` jsx function expensiveFunc(input) { ... return output } class ReactCompo extends Component { render() { return (
) } } ``` 함수 실행이 오래 걸릴 경우, 렌더링 하는 나머지 부분이 대기하게 되어 사용자들의 경험에 방해가 된다. ReactCompo를 보면, expensiveFunc은 JSX 안에서 렌더링 되어 리렌더링 될때마다 실행되어 값을 리턴하여 DOM에 렌더링 시킨다. 함수는 CPU 집약적으로 리렌더링될때마다 실행되며 React는 해당 실행이 끝날때 까지 남은 리렌더링 알고리즘을 실행하기 위해 기다려야 한다. 위 상황에서 최고의 선택은 입력되는 값이 같다면 캐시 처리하여 같은 값을 리턴하도록 하는 것이다. 따라서 같은 입력이 다시 발생할 경우 함수의 연속 실행이 더 빨라지게 된다. ``` jsx function expensiveFunc(input) { ... return output } const memoizedExpensiveFunc = memoize(expensiveFunc) class ReactCompo extends Component { render() { return (
) } } ``` ## 5. Reselect selectors 사용하기 [reselect](https://github.com/reduxjs/reselect)를 사용하면 Redux 상태 관리가 최적화 된다. 리덕스 이뷰터블하게 동작한다는 것은 action이 dispatch 할때마다 새로운 객체 참조가 생성 된다는 뜻이다. 이것은 컴포넌트에서는 변경 되지 않았지만 오브젝트 참조가 변경될 경우 리렌더링이 되므로 성능에 방해가 되는 요소가 될 수도 있다. Reselect 라이브러리는 Redux state를 캡슐화하여 component를 확인하고 렌더링 할지 안할지 여부를 알려준다. 따라서 reselect는 메모리 참조가 서로 다르지만 변경되었는지 확인하기 위해 이전 및 현재 Redux 상태를 얕게 체크함으로써 시간을 절약하게 한다. 필드가 변경된 경우 React에게 리렌더링 해야 한다고 알려주고 필드가 변경되지 않을 경우 새로운 상태 객체가 생성되었음에도 불구하고 리렌더를 취소한다. ## 6. Web worker 자바스크립트 코드는 싱글 쓰레드에서 동작한다. 동일한 쓰레드에서 오래걸리는 프로세스를 실행하면 UI 렌더링 코드에도 심각한 영향을 미치므로 최선의 방책은 프로세스를 다른 쓰레드로 옮기는 것이다. 이것은 Web worker들이 하는 역활이다. 이것들은 UI 흐름을 방해하지 않고 메인 쓰레드와 동시에 실행할 수 있는 게이트웨이 이다. React에서 공식적으로 지원하지는 않지만 Web worker를 다양한 방식으로 사용할 수 있다. 그 중 한가지는 아래와 같다: ``` jsx // webWorker.js const worker = (self) => { function generateBigArray() { let arr = [] arr.length = 1000000 for (let i = 0; i < arr.length; i++) arr[i] = i return arr } function sum(arr) { return arr.reduce((e, prev) => e + prev, 0) } function factorial(num) { if (num == 1) return 1 return num * factorial(num - 1) } self.addEventListener("message", (evt) => { const num = evt.data const arr = generateBigArray() postMessage(sum(arr)) }) } export default worker // App.js import worker from "./webWorker" import React, { Component } from 'react'; import './index.css'; class App extends Component { constructor() { super() this.state = { result: null } } calc = () => { this.webWorker.postMessage(null) } componentDidMount() { let code = worker.toString() code = code.substring(code.indexOf("{") + 1, code.lastIndexOf("}")) const bb = new Blob([code], { type: "application/javascript" }); this.webWorker = new Worker(URL.createObjectURL(bb)) this.webWorker.addEventListener("message", (evt) => { const data = evt.data this.setState({ result: data }) }) } render() { return (
) } } ``` 이 앱은 10만개의 요소가 들어있는 배열의 합을 계산하는데, 만약 메인 쓰레드에서 작업을 했다면 메인쓰레드는 10만개의 요소들을 통과하고 그 합계를 계산할 때까지 다른 작업은 할 수 없을 것이다. 자, 이제 Web worker로 옮겼다. 메인 쓰레드는 web worker와 원할하게 병렬로 실행될 것이고, 10만개의 요소가 담긴 배열의 합은 계산될 것이다. 결과는 완료되었을 때 전달되며 메인 쓰레드는 결과만 제공하면 된다. 빠르고, 간단하며 성능 또한 뛰어나다. ## 7. Lazy Loading Lazy loading은 부하를 단축하기 위해 자주 사용되는 최적화 기법 중 하나이다. **Lazy Loading**은 일부 웹 앱 성능 문제의 위험을 최소화 하는데 도움이 된다. React에서 component를 lazy load를 이용하기 위해 React.lazy() API를 사용한다. React.lazy는 React v16.6에 새로 추가된 기능이다. 이것은 React Component를 쉽고 직관적으로 lazy-loading과 코드 스플리팅을 사용하기 위해 제안된 방법이다. > React.lazy 함수는 동적 import를 사용하며 일반 component처럼 렌더링할 수 있게 해준다. - [React blog](https://gist.github.com/philipszdavido/0d5d92a688ca9e1bbca65ba72de90f53) React.lazy는 동적 import를 사용하여 component를 생성하고 렌더링 하는 걸 쉽게 만들어준다. React.lazy는 파라미터로 함수를 받는다. ``` jsx React.lazy(()=>{}) // or function cb () {} React.lazy(cb) ``` 이 콜백 기능은 반드시 동적 import 구문을 이용하여 컴포넌트 파일을 불러와야 한다. ``` jsx // MyComponent.js class MyComponent extends Component{ render() { return
} } const MyComponent = React.lazy(()=>{import('./MyComponent.js')}) function AppComponent() { return
} // or function cb () { return import('./MyComponent.js') } const MyComponent = React.lazy(cb) function AppComponent() { return
} ``` React.lazy의 콜백 기능은 `import()` 호출을 통해 Promise를 반환한다. Promise는 모듈이 성공적으로 불러왔는지 여부를 확인하고 네트워크 오류, 잘못된 경로 확인, 파일 없음 등으로 인해 모듈을 로드하는 동안 오류가 발생했는지를 확인한다. webpack이 코드를 컴파일하고 번들링할 때 `React.lazy()`와 `import()`를 히트할때 별도의 번들을 만든다. 앱은 다음과 같이 될 것이다. ```jsx react-app dist/ - index.html - main.b1234.js (contains Appcomponent and bootstrap code) - mycomponent.bc4567.js (contains MyComponent) /** index.html **/
``` 이제 앱은 멀티 번들로 분리된다. AppComponent가 렌더링 될 때 mycomponent.bc4567.js 파일은 로드되어 MyComponent가 DOM에 보여진다. ## 8.React.memo() useMemo와 React.PureComponent와 같이 React.memo()는 함수 컴포넌트를 캐시하는데 사용된다. ```jsx function My(props) { return (
)
}
function App() {
const [state, setState] = useState(0)
return (
<>
)
}
const MemoedMy = React.memo(My)
function App() {
const [state, setState] = useState(0)
return (
<>