3월 이맘때쯤, 반복되는 요소들을 출력할 때 React.memo를 사용하면 보다 효율적으로 렌더링할 수 있다는 이야기를 듣고 여러 가지로 테스트를 해본 적이 있습니다.
단순히 React.memo를 사용하는 것 자체를 이해하는 것은 크게 어렵지 않았습니다.
하지만 Key값이 들어가니 갑자기 헷갈리기 시작했습니다.
같은 위치가 아니라서 밀려 내려가면, 새롭게 렌더링 되는게 아닌가? 라는 생각이 들었습니다.
(결론부터 말하자면 React.memo와 Key는 쓰임새가 조금 다릅니다!)
그래서 직접 테스트를 하면서 익힌 내용을 간단하게 글로 정리해보려고 합니다.
(많은 자료들을 통해 최대한 검증하려고 노력하였으나, 그럼에도 틀린 부분이 있다면 언제든지 가차없이 댓글을 남겨주시면 빠르게 수정하도록 하겠습니다!!)
목차
0. Key와 React.memo
React는 기본적으로 컴포넌트를 렌더링함으로써 Virtual DOM을 업데이트하고, Diff 알고리즘을 이용하여 트리를 비교하는 과정을 거쳐서 DOM에 직접 적용하는 방식을 사용합니다.
아래 공식 문서에서 이에 대한 자세한 내용이 담겨있습니다.
https://ko.reactjs.org/docs/reconciliation.html
이 방식은 개발자가 어떤 방식으로 컴포넌트를 배치하냐에 따라 성능 차이가 굉장히 많이 날 수 있습니다.
보다 효율적으로 요소들을 비교하고 DOM 트리의 변경을 최적화하기 위해서 key를 사용할 수도 있죠.
이에 대해서는 아래 공식문서 2개의 링크에서 보다 자세히 확인하실 수 있습니다.
https://ko.reactjs.org/docs/reconciliation.html#keys
https://ko.reactjs.org/docs/lists-and-keys.html#keys
그렇다면 컴포넌트는 어떻게 최적화해서 렌더링할 수 있을까요?
React.memo를 이용해서 래핑하는 방식으로 컴포넌트가 동일한 props로 동일한 결과값을 렌더링할 때 결과값을 저장해 두었다가 재사용함으로써 성능을 향상할 수 있습니다. (이것을 Memoizing, 메모이징이라고 부릅니다!)
이렇게 재사용하게되면 Virtual DOM과 Real DOM을 비교하는 작업을 하지 않기 때문에 성능이 향상됩니다.
아래 링크를 통해서 페이스북에서 공개한 React.memo 코드를 확인해보시면 구조 파악에 도움이 되실거라 생각해서 링크도 가져왔습니다!
사용법은 간단합니다. React.memo의 매개변수로 컴포넌트를 넣어주기만 하면 됩니다.
또한 React.memo는 Higher-Order Components(HOC)라서 클래스형 컴포넌트에도 적용이 가능합니다!!
하지만 클래스 기반의 컴포넌트에서는 React.memo의 사용 대신 PureComponent를 확장해서 사용하거나 shouldComponentUpdate() 메서드를 구현해서 사용하는 것이 좋습니다.
(TMI : HOC는 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환하는 구조를 가진 함수를 의미합니다!!)
const MyComponent = React.memo(function MyComponent(props) {
/* props를 사용하여 렌더링 */
});
function MyComponent(props) {
/* props를 사용하여 렌더링 */
}
export default React.memo(MyComponent);
React.memo는 props의 변화에만 관심을 가지고 작동하게 됩니다.
그래서 내부에서 state나 context가 변화한다면 다시 렌더링됩니다.
또한 두 번째 매개변수로 compare함수를 넣을 수 있습니다.
기본적으로 React.memo는 props나 props의 객체를 비교할 때 얕은 비교를 하는데, 이런 비교 방식을 바꾸고 싶을 때 직접 비교 함수를 만들어서 넘겨줄 수 있습니다.
특히 props로 객체가 들어갈 수 있기 때문에 얕은 비교 사용에 대한 주의가 필요합니다.
(객체를 얕은 비교하게 되면 내용물이 아닌 주소값만을 비교하게 됩니다!!!
따라서 같은 값들이 들어있는 다른 주소의 객체가 들어가게 되면 두 요소는 같지 않게 되서 렌더링이 발생하게 됩니다!!)
// React.memo 사용법
React.memo(Component, [areEqual(prevProps, nextProps)]);
// 공식문서의 예제
function MyComponent(props) {
/* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
/*
nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
*/
}
export default React.memo(MyComponent, areEqual);
1. React.memo를 언제 쓰고 언제 쓰지 말아야 할까?
1.0. 쓰자!
같은 props를 가진 상태로 자주 렌더링이 발생한다면 메모이징을 이용해서 성능을 향상시키기 용이합니다.
부모 컴포넌트가 리렌더링됨으로써 자식 컴포넌트가 자동적으로 리렌더링이 되야하는 상황이 있다고 생각해봅시다.
근데 해당 자식 컴포넌트는 같은 props를 그대로 가지고 있어서 리렌더링이 되지 않아도 될 수가 있겠죠?
만약 아래와 같은 케이스에서 <Parent> 컴포넌트 내부의 상태인 cnt가 변화한다고 가정하겠습니다.
그런데 p1, p2, p3이 변하지 않는다면?
Child1, Child2, Child3은 아무것도 변한 게 없는데 부모 컴포넌트에 의해서 필요없는 리렌더링이 발생하게 됩니다.
<Parent>
<p>Changing... {cnt}</p>
<Child1 props={p1} />
<Child2 props={p2} />
<Child3 props={p3} />
</Parent>
이런 경우에 Child 컴포넌트들에 React.memo를 걸어두게 되면 불필요한 리렌더링이 일어나지 않게 되겠죠?
이와 비슷하게 무겁고 비용이 큰 연산이 포함된 컴포넌트에 사용하면 좋습니다.
같은 값에 대해서 다시 무거운 연산이 실행되는 것을 방지함으로써 성능을 향상시킬 수 있습니다.
1.1 쓰지 말자!
위에 언급한 상황 이외의 상황에는 적용하지 않는 것이 좋습니다.
굳이 성능적으로 이득을 보지 못하는 상황에서 사용할 필요는 당연히 없겠죠?
위에서 언급한 "쓰자" 케이스에서는 경험적으로 저런 상황에서 쓰면 좋다는 데이터가 존재합니다만, 이것 역시 실제로 cost를 체크해서 이득이 되는지 여부를 살펴보아야 합니다.
cost에 대해서 조금 더 자세히 살펴봅시다.
React.memo를 쓰면서 발생하는 cost는 메모이징된 컴포넌트의 결과물 저장을 위한 추가적인 메모리가 발생한다는 것과 props의 비교를 위한 비교함수의 실행이 있습니다.
실제 예시를 생각해보면, 렌더링 될 때마다 props가 많이 변하는 경우, 어짜피 대다수의 케이스에서 리렌더링되는데 비교 함수가 매번 실행되는데다가 결과물 저장을 위한 메모리 소모까지 추가되어서 비효율적으로 변할 수 있습니다.
2. 그럼 key랑 React.memo는 도대체 무슨 차이?
위에서 살짝 언급을 했지만 다시 이야기를 하자면!!
key값을 사용함으로써 얻는 이득은 Virtual DOM의 diff 알고리즘을 통해 같은 key값이라면 화면에 요소들을 그리기 위한 트리들을 새롭게 만드는 것이 아니라 기존의 요소를 재활용한다는 점입니다.
반면 React.memo를 사용하면 DOM Tree에 들어갈 값들을 생성하는 과정에서 발생할 수 있는 불필요한 컴포넌트 함수의 실행 과정을 메모이징을 통해서 다시 계산하는 과정 없이 빠르게 처리할 수 있습니다.
이걸 단순히 말로만 하면 알아보기 어렵겠죠?
그리서 실제로 새로운 요소가 추가되는 상황을 가정하여 테스트를 만들었습니다.
위 레포지토리의 코드에서 의미있는 부분을 하나씩 살펴봅시다.
App.tsx를 확인해보면, key값을 index로 부여하는 경우와 고유한 값으로 부여하는 경우에 따라 어떻게 다르게 렌더링 되는지를 테스트했습니다.
또한 공식문서에 나와있는 대로 rest parameter의 위치를 다르게 해서 새로운 요소가 앞에 추가되는지(역배열) 뒤에 추가되는지(정배열)에 따라 어떻게 diff 알고리즘이 작동하는지도 함께 테스트했습니다.
그리고 NumComponent에서는 React.memo를 이용해서 메모이징을 하는 경우와 하지 않는 경우로 나눠서 테스트했습니다.
추가로 실제로 컴포넌트가 새롭게 동작하는 지를 눈으로 확인하기 위해서 console.log를 이용해서 컴포넌트함수가 동작할 때마다 콘솔창에 해당 값이 출력될 수 있도록 만들어뒀습니다.
아래 진행되는 실험에서는 React Development Tools를 사용했습니다.
리렌더링이 되는 요소를 하이라이팅하는 옵션과 Components 탭을 보면서 진행했으니 참고하세요!
2.0. 정배열, key: index
먼저 정배열에다가 key값에 index를 부여하고, React.memo도 사용하지 않은 케이스를 살펴보면, 요소의 위치가 변하지 않음에도 컴포넌트함수가 작동되어서 매번 새롭게 화면에 그려주는 모습을 확인할 수 있습니다.
(콘솔창을 확인해보면 보다 명확하게 확인할 수 있습니다)
2.1. 정배열, key: index, React.memo
이제 React.memo를 적용하면 어떻게 될까요?
이미 메모이징이 되어있기 때문에 같은 props를 전달하는 경우 해당 컴포넌트 함수는 새롭게 동작하지 않고 메모이징된 값을 가져오게 됩니다.
따라서 새롭게 추가되는 요소만 화면에 그려지게 되죠!
(React Development Tool의 Components 탭에서 Memo가 되어있다는 사실을 눈으로 확인할 수 있습니다!)
2.2. 역배열, key: index, React.memo
그렇다면 이번에는 역배열로 아까와 같은 조건을 돌리면 어떻게 될까요?
정배열과는 다르게, key값이 index가 되는 경우 내용이 같은 요소임에도 다른 key값을 가지게 되는 문제가 발생합니다.
Diff 알고리즘에 의해 같은 key값의 요소를 비교하게 되는데, 같은 key값인데 다른 props가 들어간다면?
제아무리 React.memo라도 props의 변화는 컴포넌트 함수의 작동을 유발한다고 했죠?!
이것에 의해 매번 새롭게 컴포넌트 함수가 작동하는 불상사가 발생하게 됩니다.
이 내용이 이번 포스팅에서 가장 핵심이 되는 부분이라고 볼 수 있겠네요!!
2.3. 역배열, key: 고유
이번엔 key에 고유값을 부여했을 때를 테스트했습니다.
고유한 key값을 부여했기 때문에 Diff 알고리즘은 우리가 원하는 대로 비교를 할 것입니다.
하지만 React.memo처리가 되어있지 않기 때문에 매번 컴포넌트 함수가 돌아가게 되는 불상사가 발생할 수 있습니다.
2.4. 역배열, key: 고유, React.memo
마지막으로 역배열에 key값을 고유한 값으로 주고, React.memo처리를 한 케이스입니다.
고유한 key값을 부여했기 때문에 우리가 원하는 대로 Diff 알고리즘이 작동할 것이고, 같은 props를 전달하기 때문에 React.memo로 메모이징된 값을 가져다가 쓸 것입니다.
3. 결론
React.memo는 컴포넌트 함수의 불필요한 재동작을 막기 위해서 사용합니다.
부모 컴포넌트가 잦은 리렌더링이 발생함에도 자식 컴포넌트가 같은 props를 가지는 경우 유용하게 사용할 수 있습니다.
하지만 무작정 사용하는 것은 메모리적인 측면과 비교함수의 작동이라는 측면에서 손해를 볼 수 있기 때문에 항상 Profiling 등의 성능 테스트를 통해 적용이 이득이 되는지를 확인해야 합니다.
마지막으로 map의 key값에는 되도록이면 index가 아닌 고유한 값을 부여합시다!
최근댓글