React에서 성능 최적화는 효율이 높지 않습니다. 더 중요한 것은 가독성과 생산성입니다.
하지만, 알고 있는데 안하는 것과, 모르는 것은 다릅니다.
프론트엔드에서 성능은 DOM 조작이 가장 큰 부분을 차지합니다.
리액트에서는 DOM을 조작하는 대신, 데이터를 변경하면 다시 렌더링합니다.
이러한 렌더링을 줄이는게 성능 최적화의 가장 간단한 방법입니다.
기본적으로 props
나 state
가 변경되어, 새로운 엘리먼트가 반환되면
React는 이전 엘리먼트와 비교하여 다른 부분만 실제 DOM에 업데이트 합니다.
변경된 부분만 실제 DOM에 업데이트하더라도, 리렌더링은 여전히 시간이 걸립니다.
불필요한 리렌더링은 다음과 같습니다.
props
값이 같아도 부모 컴포넌트가 리렌더링 되면, 자식 컴포넌트도 리렌더링됩니다.state
값이 같아도, setState
를 호출하면 항상 리렌더링 됩니다.이 때문에, 속도 저하가 눈에 띌 수 있습니다.
다시 렌더링 되기 전에 props, state
값이 같다면, 리렌더링을 막아 속도를 높힐 수 있습니다.
메모이제이션 : 동일한 계산을 반복할 때, 이전 계산값을 저장하여 연산의 반복 수행을 제거합니다.
리렌더링 이전에 실행되는 생명주기 메서드 ShouldComponentUpdate
를 사용해 리렌더링을 조절합니다.
컴포넌트가 props.color
나 state.count
를 통해 변경된다고 할 때,
다음처럼, 다른 값일 때만 true
를 반환하여 리렌더링 하도록 합니다.
class CounterButton extends React.Component {
constructor(props) {
//...
}
ShouldComponentUpdate(nextProps,nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return <button style={{ background : this.props.color }}> </button>
}
}
ShouldComponentUpdate
는 리렌더링을 일으키는 요소가 많다면, 조건이 매우 길어질 수 있습니다.
위와 같은 패턴은, React.PureComponent
를 상속받는 것으로 대체하여 간단히 할 수 있습니다.
class CounterButton extends React.PureComponent {
render() {
return <button style={{ background : this.props.color }}> </button>
}
}
다만, React.PureComponent
는 얕은 비교로 리렌더링을 막기 때문에 참조형 데이터에 주의해야 합니다.
// parent
function App() {
return (
<div className="App">
<CounterButton color={'red'} />
</div>
);
}
// =====
// child
class CounterButton extends React.PureComponent {
render() {
const {color} = this.props;
return (
<button style={{ background:color }}>
button
</button>
)
}
}
// =====
위의 예제는 PureComponent
가 잘 동작합니다.
color
가 바뀌지 않아, 부모가 렌더링 되어도, 다시 렌더링되지 않습니다.
자식 컴포넌트의 프로퍼티를 참조형 데이터로 바꾸면 어떻게 될까요?
color
프로퍼티를 colors
로 바꾸고, 배열을 할당했습니다.
// parent
function App() {
return (
<div className="App">
<CounterButton colors={['red']} /> // (*)
</div>
);
}
// =====
// child
class CounterButton extends React.PureComponent {
render() {
const {colors} = this.props;
return (
<button style={{ background:colors[0] }}>
button
</button>
)
}
}
// =====
이번에는, colors
가 바뀌지 않음에도 부모가 렌더링되면 같이 렌더링됩니다.
클래스형 컴포넌트에서, 렌더링 될 때마다 render
메서드가 호출됩니다.
['red']
는 리터럴로 render
의 호출마다 (값은 같지만)새로운 배열을 만들어 할당합니다.
얕은 비교를 하기 때문에 배열의 값이 같아도 다른 참조를 가지는 배열입니다.
미리 선언하여, 해결합니다.
function App() {
colors = ['red']; // (*)
return (
<div className="App">
<CounterButton colors={this.colors} />
</div>
);
}
클래스형 컴포넌트는, 인스턴스화 되고 라이프 사이클에 맞게 메서드가 호출됩니다.
클래스 필드로 선언하면, 다시 실행되지 않아 렌더링되어도 같은 참조의 배열이 할당됩니다.
함수형 컴포넌트에서는, React.memo()
를 export하여 값이 같다면 리렌더링을 막을 수 있습니다.
function CounterButton(props) {
return(
<button style={{background:props.color}}>
button
</button>
)
}
export default React.memo(CounterButton);
함수형 컴포넌트도 마찬가지로 참조형 데이터를 주의해야합니다.
추가로, 함수형 컴포넌트는 리렌더링 될 때, 함수가 다시 실행됩니다.
배열이나 객체같은 참조형 데이터를 할당할 때,
함수 내에서 선언하면 렌더링 마다 매번 선언됩니다.
아래 코드는 불필요한 렌더링을 막지 않습니다.
// parent
function App() {
// 렌더링 될 때마다, 함수가 실행
// 매번 할당되는 color는 다른 참조를 가진다.
const color = {main:'red'};
return(
<CounterButton color={color}> // 매번 다른 프로퍼티를 할당한다.
button
</CounterButton>
)
}
// =====
// child
function CounterButton(props) {
return(
<button style={{background:props.color}}> // props가 달라 렌더링된다.
button
</button>
)
}
export default React.memo(CounterButton);
// =====
종종 부모 컴포넌트에서 만든 함수를 자식 컴포넌트에 할당하게 됩니다.
function app() {
const [count,setCount] = useState(0);
const increaseCount = () => {
setCount(count=> count+1);
}
const decreaseCount = () => {
setCount(count=> count-1);
}
return (
<CounterButton decrease={decreaseCount}/>
)
}
함수도 참조형 데이터입니다.
위 처럼 콜백 함수를 할당하면, 부모 컴포넌트가 렌더링 될 때마다 decreseCount
가 다시 선언됩니다.
그리고 매번 다른 콜백함수가 할당됩니다.
때문에, decreseCounte
의 내용은 그대로지만 CounterButton
은 리렌더링 됩니다.
useCallback
은 메모이제이션 된 콜백을 반환합니다.
콜백 함수의 내용과, 의존성 값을 배열에 전달하여 만듭니다.
메모이제이션된 콜백은 의존성이 변경되었을 때만 변경됩니다.
Hooks Reference : 불필요한 렌더링을 방지하기 위해,
자식컴포넌트에 콜백을 전달할 때 유용하다고 되어 있습니다.
함수형 컴포넌트는 렌더링 마다 함수를 실행하기 때문에, 이를 방지하는 여러 Hooks가 존재합니다.
function app() {
const [count,setCount] = useState(0);
const decreaseCount = useCallback(() => { // 메모이제이션된 콜백을 반환
setCount(count=> count-1);
},[])
return (
<CounterButton decrease={decreaseCount}/> // 같은 참조를 가진 콜백을 할당
)
}
React의 성능을 최적화 하기 위해, 불필요한 렌더링을 막아야합니다.
렌더링시키는 props, state
의 값이 같을 때는 렌더링 하지 않으면 됩니다.
다만, 참조형 데이터에 주의하여 할당합니다.