[React 성능 개선] 1. 디바운싱
해당 포스트는 리액트 문법에 대한 설명은 대부분 생략되어 있습니다.
기본적으로 리액트는 다른 프런트앤드 언어와 다른 방식으로 랜더링을 진행합니다. 상태 변경이 발생하면 기존의 UI를 모두 지우고 리랜더링을 진행하는 방식입니다. 해당 방식은 Virtual DOM를 이용하기때문에 전체 리랜더링을 진행하더라도 성능면에서도 뒤떨어지지 않는 방식이므로 충분히 프런트앤드를 대표할만한 언어라 생각합니다.
실제로 프로그램을 제작하다 보면 전체 리랜더링 방식은 성능 저하가 발생하는 경우가 많습니다. 그렇기에 개발할 때 해당 부분을 신경 쓰며 개발을 진행하셔야 합니다. 이번 장에서는 실제로 저자가 아래 프로젝트를 진행하면서 개선한 성능들 중 하나인 "Input태그로 값을 받았 경우"에 대해 설명하겠습니다.
GitHub - dotredbee/URTrader: 마틴 게일 배팅법을 기반으로 제작 된 자동 매매 프로그램.
마틴 게일 배팅법을 기반으로 제작 된 자동 매매 프로그램. Contribute to dotredbee/URTrader development by creating an account on GitHub.
github.com
코드 전체를 뜯어서 보기에는 다소 무리가 있기에 간단한 태그 검색 폼을 만들어 설명하겠습니다.
import './App.css';
import { useMemo, useState } from 'react';
// 태그 없을때 나오는 페이지
function NotFoundPage() {
return <span>입력한 해쉬태그가 없습니다. </span>
}
// 태그 출력 컴포넌트
function DrawTagTable({ searchText, hashtags }) {
const filter = useMemo(() => {
const re = new RegExp(`\\W*${searchText}\\W*`)
let _tags = []
if(searchText === ""){
_tags = hashtags
}else{
_tags = hashtags.filter((hashtag) => re.test(hashtag))
}
return _tags
}, [hashtags, searchText])
return(
<ul>
{filter.map((tag, idx) => <li key={idx}>{tag}</li>)}
</ul>
)
}
// 서치 컴포넌트
function SearchInput({ searchText, setSearchText }) {
const onChange = (e) => {
setSearchText(e.target.value)
}
return (
<input
type="text"
value={searchText}
onChange={onChange}
/>
)
}
function App() {
const [ hashtags, setHashTags ] = useState([
"test",
"test1",
"test2",
"admin",
"ad",
"add",
"react",
"node"
])
const [ searchText, setSearchText ] = useState('')
return (
<div>
<div>
<SearchInput
searchText={searchText}
setSearchText={setSearchText}
/>
</div>
<div>
{!hashtags.length ?
<NotFoundPage /> :
<ul>
<DrawTagTable searchText={searchText} hashtags={hashtags}/>
</ul>
}
</div>
</div>
);
}
export default App;
기능에대해 간략하게 설명하자면 <SearchInput />에서 입력된 값을 <DrawTagTable /> 태그에서 받아 해당 값을 정규표현식을 통해 패턴 검출 후 값을 리스트화시키는 코드입니다.
겉으로 보기에는 잘 작동하는 코드입니다. (실제로도 복잡한 계산이 없기에 Input태그가 성능상 저하를 발생하지 않습니다.) 내부적으로 자세히 알아보기 위해 <DrawTagTable />의 filter 함수에 콘솔을 찍어보겠습니다.
// 태그 출력 컴포넌트
function DrawTagTable({ searchText, hashtags }) {
const filter = useMemo(() => {
const re = new RegExp(`\\W*${searchText}\\W*`)
let _tags = []
if(searchText === ""){
_tags = hashtags
}else{
_tags = hashtags.filter((hashtag) => re.test(hashtag))
}
// 내부적으로 값 변화를 관찰하기 위한 코드
console.log(_tags)
return _tags
}, [hashtags, searchText])
return(
<ul>
{filter.map((tag, idx) => <li key={idx}>{tag}</li>)}
</ul>
)
}
input값이 변할 때마다 콘솔창에 값이 출력되는 현상을 볼 수 있습니다. 현재는 패턴매칭(정규표현식) 계산만 진행되기에 크게 지장은 없습니다. 하지만 내부적으로 계산이 추가되고 복잡해질수록 매 입력마다 성능 저하가 일어납니다. 뿐만 아니라 리액트 성격상 값이 변하면 전체 컴포넌트가 리랜더링(따로 처리하는 방법이 있습니다, 추후 설명하겠습니다.)을 진행하기에 서버로부터 내려받는 데이터가 많은 사이트인 경우에는 상당한 비용 발생으로 성능 저하가 예상됩니다.
디바운싱
디바운싱은 지속적으로 발생하는 함수중 마지막 함수만 실행하도록 만드는 작업입니다. 위 예제를 통해 설명하자면 연속적인 검색으로 인하여 filter(패턴 검출) 함수가 연이어 발생합니다. 이를 개선해보자면 입력이 일정 시간 동안 일어나지 않는다면 입력이 완료된 상태로 간주하도록 합니다. 그리고 완료상태일 때만 특정 함수가 실행되도록 만들도록 합니다.
// 서치 컴포넌트
function SearchInput({ setSearchText }) {
let searchRef = useRef("")
const onKeyUp = () => {
let inputVal = searchRef.current.value;
// 0.5초 후에도 이전 값과 같다면 반영한다.
setTimeout(() => {
if(inputVal === searchRef.current.value) setSearchText(inputVal)
}, 500)
}
return (
<input
ref={searchRef}
type="text"
onKeyUp={onKeyUp}
/>
)
}
개선된 코드는 input 값을 useRef()를 통해 받고 실제로 검색 값 반영은 setTimeout()을 통해 0.5초 후에도 이전 값과 현재의 값이 같다면(0.5초 동안 입력하지 않았다면) 반영됩니다.
실제 찍히는 콘솔을 위의 콘솔과 비교해보시면 출력되는 값이 현저히 줄어든 것이 확인됩니다. 이번 포스트에서 진행했던 짧은 코드는 위에서도 잠깐 언급했듯이 성능에 문제 되지 않습니다. 하지만 나중에 수많은 컴포넌트를 관리하다 보면 이 작은 문제로 인해 머리 아플 수도 있기에 알고 있어서 나쁠 건 없다 생각합니다.