Why?
개발자와 디자이너가 함께 협업하여 프로젝트를 완성하는 DND에서 사이드 프로젝트를 진행하고 있습니다. RN을 사용하면서 풀지 못할 숙제들을 풀어가는 일들이 굉장히 많았었는데, Global state를 관리하면서 느꼈던 여러 문제 중 한 가지를 공유하려고 합니다. useReducer와 useContext를 사용하여 Global state를 관리하였으나, context를 사용하여 가져다가 쓰는 구조 상 state 값이 업데이트될 때 re-rendering이 계속적으로 발생하는 상황을 마주하게 되었습니다. 이런 상황을 타개하기 위하여 최소한의 최적화가 필요 하였고, re-rendering에 관련된 글을 찾다 좋을 글을 발견하여 공유해 보려고 합니다.
만약 re-rendering에 발생하는 시점에 대한 공부가 필요하시다면, 아래의 글을 참고해보면 좋을 것 같습니다.
언제나 피드백은 환영이니, 주저하지 말고 댓글에 글을 남겨주시면 감사하겠습니다. 그럼 시작해 보겠습니다.
* 이 글은 5ways to avoid react component re-renderings 번역하였습니다.
Motivation
리액트 컴포넌트는 계속적으로 진화하고 발전해 왔습니다. 그렇기에 대다수의 개발자들은 최적화가 중요하다고 생각하였고, 필요없는 리렌더링을 지양하기 위한 방법들을 강구해왔습니다. 그리하여 이러한 이슈를 피할 수 있는 다양한 방법들을 찾게 되었습니다. 이 글은 리액트 컴포넌트의 리렌더링을 피할 수 있는 5가지 방법에 대해 논의할 것입니다.
1. Memoization using useMemo() and UseCallback() Hooks
메모이제이션은 props의 변경 사항이 있는 경우에만 컴포넌트를 다시 리렌더링할 수 있도록 만들어 줍니다. 이 기술을 통하여, 개발자들은 필요없는 리렌더링을 피할 수 있으며, 어플리케이션이 실행할 때의 부하를 줄일 수 있습니다.
리액트는 메모이제이션을 위한 2가지 hooks를 제공합니다.
1) useMemo()
2) useCallback()
위에 2가지 hooks는 입력의 계산 없이 동일할 경우 동일한 결과를 캐싱하고 반환하여 리렌더링을 줄입니다. 입력의 값이 바뀌없을 때 캐시는 초기화되고, 새로운 컴포넌트 값이 렌더됩니다.
1) useMeme()
useMemo() 함수를 사용하기 위해서는 아래의 예시를 참고해보세요.
const multiply = (x,y) => {
return x*y
}
위 함수는 입력과 관련없이 함수가 호출 될때마다 컴포넌트는 리렌더링되고 해당 결과물을 반환합니다. 그러나 useMemo() hook을 사용한다면, 입력이 같을 경우 캐시된 결과 값을 저장함으로써 컴포넌트의 리렌더링을 피할 수 있습니다. 아래와 같이 말이죠.
const cachedValue = useMemo(() => multiply(x, y), [x, y])
컴포넌트는 계산된 결과 값을 cachedValue의 값으로 저장하고 useMomo() 은 입력이 변경되지 않는 한 매번 저장된 값을 반환합니다.
2) useCallback()
useCallback() 은 메모이제이션을 실행하는 또 하나의 hook입니다. useMemo와는 다르게 계산된 결과 값을 캐싱하는 것이 아니라 제공된 callback 함수 자체를 기억합니다. 예를 들어 아래와 같이 리스트 안에 클릭할 수 있는 아이템 컴포넌트가 있다고 가정해봅시다.
import { useCallback } from 'react';
export function MyParent({ term }) {
const onClick = useCallback(event => {
console.log('Clicked Item : ', event.currentTarget);
}, [item]);
return (
<Listitem={item} onClick={onClick}
/>
);
}
useCallback()은 onClick callback 함수를 기억합니다. 따라서 사용자가 동일한 항목을 계속해서 클릭하면 구성요소를 리렌더링하지 않습니다.
2. API call optimization with React query
여러 hooks 중 하나인 비동기성 데이터를 가져오기 위한 작업으로 useEffect()를 사용하는 것은 당연지사입니다. 하지만 useEffect()는 각 렌더에서 데이터를 실행하고 가져오며 대다수의 상황에서 동일한 데이터를 계속 로드합니다.
이 문제를 해결할 수 있는 방법으로, 반환된 데이터를 캐시하는 React query 라이브러리를 사용합니다. API를 호출하게 되면 React query는 먼저 이전에 요청하여 반환된 캐시된 데이터를 반환합니다. 그 다음 서버에서 데이터를 검색하고, 사용할 만한 즉 변경되거나 새로운 데이터가 없다면 컴포넌트가 리렌더링 되지 않도록 합니다.
import React from 'react'
import {useQuery} from 'react-query'
import axios from 'axios'
async function fetchArticles(){
const {data} = await axios.get(URL)
return data
}
function Articles(){
const {data, error, isError, isLoading } = useQuery('articles', fetchArticles)
if(isLoading){
return <div>Loading...</div>
}
if(isError){
return <div>Error! {error.message}</div>
}
return(
<div>
...
</div>
)
}
export default Articles
3. Creating memoized selectors with Reselect
Reselect는 메모이제이션된 selector를 만들 수 있는 third-party 라이브러리입니다. 해당 라이브러리는 보통은 Redux와 함께 사용되며, 불필요한 리렌더링을 감소할 수 있는 놀랄만한 기능을 가지고 있습니다. 아래는 대표적인 특징입니다.
- Reselect는 파생된 데이터를 계산할 수 있습니다.
- Reselect는 인자값이 바뀌지 않는 한 다시 계산하지 않습니다.
Reselect는 createSelector라는 API를 제공하는데 메모이제이션된 섹터 기능을 생성할 수 있습니다. 이해를 돕기 위해, 아래의 예시를 참고 해주세요.
import { createSelector } from 'reselect'
...
const selectValue = createSelector(
state => state.values.value1,
state => state.values.value2,
(value1, value2) => value1 + value2
)
...
creactSelector는 2개의 선택자를 입력으로 받아 메모이제이션된 버전을 반환합니다. 메모이제이션 된 선택자들은 값이 변경되지 않는 한 다시 계산하지를 않습니다.
4. Replace useState() with useRef()
useState() hook은 react 어플리케이션에 상태를 변경시 컴포넌트를 리렌더링을 하기 위해서 많이 사용됩니다. 그러나 컴포넌트를 다시 렌더링하지 않고 상태 변경을 추적해야 하는 상황도 존재합니다.
만약 useRef() hook을 사용한다면, 컴포넌트의 리렌더링 없이 상태 변경을 추적할 수 있습니다.
function App() {
const [toggle, setToggle] = React.useState(false)
const counter = React.useRef(0)
console.log(counter.current++)
return (
<button onClick={() => setToggle(toggle => !toggle)} >
Click
</button>
)
}
ReactDOM.render(<React.StrictMode><App /></React.StrictMode>, document.getElementById('mydiv'))
위 예시를 살펴보면, 값이 변경될 때마다 리렌더링이 일어나는 toggle 컴포넌트입니다. 하지만 counter 변수는 ref값에 담겼기 때문에 값을 유지하게 됩니다. useState를 사용하게 되었을 때 각 toggle에서 2번의 렌더링이 일어나는 반면, useRef()를 사용하게 되면 한번의 렌더링만 일어나게 됩니다.
5. Using React Fragments
React를 사용한 경험이 존재한다면, 오직 하나의 부모 요소에 컴포넌트들을 위치시켜야 한다는 것을 아마도 알고 계실 것입니다. 리렌더링과는 직접적인 관련이 없더라도, 전체적으로 리렌더링되는 시간에 영향을 준다는 사실을 알고 계셨을까요?
이를 해결할 수 있는 방법으로는 컴포넌트를 Warpping 할 수 있는 React Fragments를 사용하는 것입니다. 이를 통해 DOM의 부하를 줄여줌으로써 렌더링 되는 시간을 단축시키고 메모리의 사용량을 감소시킬 수 있습니다.
const App= () => {
return (
<React.Fragment><p>Hello<p/><p>World<p/></React.Fragment>
);
};
Conclusion
이 글에서는 react 컴포넌트의 불필요한 리렌더링을 예방할 수 있는 5가지 방법을 제시하였습니다. 해당 방법은 대부분 캐싱을 활용하고, react hooks를 사용하고 third party 라이브러리를 사용하여 이행할 수 있습니다.
또한, 이러한 방법은 메모리 오버헤드를 줄이면서 불필요한 재렌더링을 방지하기 위해 애플리케이션 성능을 향상시킵니다.
< 참고자료 >
[사이트] #medium
https://blog.bitsrc.io/5-ways-to-avoid-react-component-re-renderings-90241e775b8c
5ways to avoid react component re-renderings end
'Language & Framework & Library > React' 카테고리의 다른 글
Design Patterns - Compound component pattern (0) | 2023.10.21 |
---|---|
Re-rendering과 memoization (2) | 2022.03.13 |
React vs Vue (2) | 2021.07.13 |
React의 컴포넌트 수명주기 (0) | 2021.02.12 |
Virtual DOM (0) | 2020.01.16 |