리액트 랜더링 최적화

김범식·2023년 8월 9일
0

React

목록 보기
3/3

⭐ 리액트의 랜더링과정

리액트에서는 최적화는 컴포넌트의 리랜더링을 줄이는 과정이라고해도 과언이 아니다. useCallbackuseMemo 그리고 React.memo를 사용하여 메모이제이션 기법으로 리랜더링을 줄이게 된다.

리액트의 렌더링은 Render PhaseCommit Phase 크게 두가지 로 나뉜다.

📌 Render Phase

stateprops 가 변하면 해당 컴포넌트를 불러서 React.createElement를 동작시켜 컴포넌트를 실행시킨다. 이과정을 리랜더링이라고 한다.

해당 리랜더링 과정을 통해 생성된 새로운 virtual DOM을 만들게 된다.

📌 재조정(Reconciliation)

이전에 만들어진 virtual DOM과 현재 만든 virtual DOM을 비교하여 실제 Real DOM에 반영할 목록들을 확인한다.

📌 Commit Phase

render phase에서 확인했던 변경이 필요한 목록들을 실제 Real DOM에 적용한다.

변경이 필요한 부분이 없다면 commit phase는 스킵된다.

이해를 돕기 위해 다음과 같은 예시코드를 준비했다

//부모 컴포넌트
const Parent = ()=>{
	const [value, setValue] = useState(null);

	const handleClick = () =>{};
	
	useEffect(()=>{
		setTimeout(()=>{
			setValue("changeValue");
		},3000);
	},[])

	return(
		<>
			<FirstChild value={value}/>
			<SecondChild onClick={handleClick}/>
		</>
	)
}
// 첫번째 자식 컴포넌트
const FirstChild = ({value})=>{
	return <div>{value}</div>
}
// 두번째 자식 컴포넌트
const SecondChild = ({ onClick })=>{
	<div onClick={onclick}>
		{Array.from({length:1000}).map((_,idx)=>(
			<GrandChild key={idx+1) order={idx}/>.    //오래걸리는 작업의 예시 
		))}
	</div>
}

React의 리랜더링은 조건은 propsstate의 변경에 있다. Parent의 value의 값이 변경되면서 Parent는 리랜더링이 되고 그로인해 Firstchild도 리랜더링된다. 여기서 주의할 점은 SecondChildprops값도 변경된다는 것이다 Parent가 리랜더링 되면서 handleClick재정의 되고 이전과는다른 handleClick함수가 생성되면서 자연스럽게 SecondChildprops는 변경되게 된다. SecondChild는 불필요하게 리랜더링 작업을 수행하게 된다.

이 불필요한 과정을 최적화 할 순 없을까?



⭐ useCallback 사용

//부모 컴포넌트
const Parent = ()=>{
	const [value, setValue] = useState(null);

	const handleClick = useCallback(()=>{},[]);
	
	useEffect(()=>{
		setTimeout(()=>{
			setValue("changeValue");
		},3000);
	},[])

	return(
		<>
			<FirstChild value={value}/>
			<SecondChild onClick={handleClick}/>
		</>
	)
}

useCallback을 사용해서 handleClick을 메모이제이션 하였다. 의존성 배열이 변경되지 않는다면 handleClick의 참조값은 변경되지 않는다. 때문에 리랜더링의 조건중 하나인 props를 동일하게 만들었기 때문이다.

그렇다면 이제 리랜더링은 발생하지 않을까? 안타깝게도 여전히 리랜더링이 발생한다.

왜냐하면 Parent가 리랜더링되면서 React.createElement를 사용해 자식컴포넌트들을 재생성하기 때문이다. 그렇다고 useCallback을 사용하는게 아주의미없는것은 아니다. SecondChild는 리랜더링 되지만 render phase재조정과정에서 이전 virtual DOM과 변경된 점이 없기 때문에 commit phase를 생략할 수 있다!

그치만 여전히 render phase에서 불필요하게 리랜더링 되고 있다..

이때 React.memo를 사용하게 된다. React.memo를 사용한 컴포넌트는 propsstate가 변하지 않는다면 컴포넌트의 리랜더링을 막고 메모이제이션한 컴포넌트를 사용하여 render phase를 생략할 수 있다. !



⭐ 두번째 자식 컴포넌트에 React.memo 사용

// 두번째 자식 컴포넌트
const SecondChild = ({ onClick })=>(
	<div onClick={onclick}>
		{Array.from({length:1000}).map((_,idx)=>(
			<GrandChild key={idx+1) order={idx}/>.    //오래걸리는 작업의 예시 
		))}
	</div>
)

export default React.memo(SecondChild); //리액트 메모의 사용

React.memo가 props를 이전과 비교할 때는 얕은비교를 사용한다. 얕은 비교란 원시타입은 값을 비교하고, 참조타입은 참조값이 같은지를 비교한다.

이제 React.memo를 통해 컴포넌트 전체를 메모이제이션 해주었다. React.memo는 props가 변경되지 않으면 해당 컴포넌트를 이전에 랜더링 했을 때 생성된 가상 DOM을 재사용하여 리랜더링을 방지한다.

이전에 useCallback을 사용해 handleClick을 재사용하여 props의 변경을 방지했기 때문에 Parent 컴포넌트가 리랜더링이 되어서 SecondChild는 더이상 리랜더링이 되지 않는다!



⭐ 불변하는 값을 props로 넘길때 useMemo

//부모 컴포넌트
const Parent = ()=>{
	const [value, setValue] = useState(null);

	const item = {
		name : "이순신",
		age : 45
		}
	
	useEffect(()=>{
		setTimeout(()=>{
			setValue("changeValue");
		},3000);
	},[])

	return(
		<>
			<FirstChild value={value}/>
			<SecondChild item={item}/>
		</>
	)
}
// 두번째 자식 컴포넌트
const SecondChild = ({ item })=>(
	<div>이름 : {item.name},  나이 : {item.age}</div>
)

export default React.memo(SecondChild); //리액트 메모의 사용

다음 코드에서 Parent는 3초후에 리랜더링되고 item 변수또한 재정의된다. item은 객체로 참조타입이기 때문에 React는 이전과 다른 객체로 판단하고 SecondChild props가 변경된것으로 판단하여 SecondChild 리랜더링 하게된다. 기껏한 React.memo가 소용없게 되버렸다.



⭐ useMemo 사용

이런경우에는 값을 저장하는 useMemo를 사용할 수 있다. 의존성 배열이 변경되지 않는다면 useMemo는 메모이제이션한 값을 반환한다.

//부모 컴포넌트
const Parent = ()=>{
	const [value, setValue] = useState(null);

	const item = {
		name : "이순신",
		age : 45
		}
	
	const memoizationItem = useMemo(()=> item, []);
	
	useEffect(()=>{
		setTimeout(()=>{
			setValue("changeValue");
		},3000);
	},[])

	return(
		<>
			<FirstChild value={value}/>
			<SecondChild item={memoizationItem}/>
		</>
	)
}

이로써 SecondChild의 item의 참조값은 유지되고 props는 변경되지 않아 리랜더링 되지 않고, 메모이제이션한 컴포넌트를 재사용할 수 있다.



⭐ “모든곳에서 useCallback, useMemo, React,memo 사용하면 좋을까?”

해당 함수들도 결국 코드이고 메모이제이션을 하기위한 작업이 필요하다. 때문에 props나 state가 자주 변경되는 컴포넌트에 사용하면 오히려 메모리의 낭비를 초례할 수 있다.



⭐ 최적화 도구들을 사용하기 전에 근본적인 코드를 개선

기본적으로 코드를 잘 작성해놓고 최적화 도구를 사용하도록하자

const Component = () =>{
	const forceUpdate = useForceUpdate(); // 강제 리랜더링하는 함수 

	return(
		<>
			<button onClick={forceUpdate}> force</button>
			<Consoler value="fixedValue"/>
		</>
	)
}

force라는 버튼을 누르면 강제로 리랜더링이 동작하는 컴포넌트이다. 여기서 Consoler의 value값은 변경되지 않았지만 버튼을 눌러 Component를 리랜더링하게 되면 React.createElement가 자동으로 내부 컴포넌트들을 리랜더링하게 된다.

이 코드를 근본적을 개선할 수 있는 방법은 무엇일까?

const Component = ({children}) =>{
	const forceUpdate = useForceUpdate();
	return(
		<>
			<button onClick={forceUpdate}> force</button>
			{children}
		</>
	)
}

function App(){
	<div>
		<Component>
			<Consoler value="fixedValue"/>
		</Component>
	</div>
}

다음과 같이 사용하면 Consoler는 Component 컴포넌트 안에 있는 것이 아니기 때문에 React.createElement로 리랜더링을 하지 않는다.

이처럼 메모이제이션을 사용하기 이전에 근본적으로 최적화를 먼저 해야한다.


“dont optimize rendering prematurely, do it when needed”

미리 최적화 하지 말자, 필요할 때 하자

profile
frontend developer

0개의 댓글