Intro
이번 글은 프로젝트 리팩토링 중 리렌더링 이슈에 대한 고민을 적은 글입니다.
해결하기 위한 가설과 시도만 있고 정확히 왜 발생했는지에 대한 결론은 없습니다. 추측만 존재합니다...
내용과 작성자가 다소 애송이처럼 보이시더라도 너그럽게 봐주시면 감사하겠습니다.
혹시나 내용에 대한 피드백이 있다면 댓글을 부탁드리겠습니다!
무슨 문제였나요?
장소 컴포넌트의 버튼을 눌러서 장소를 추가시키는 상황입니다.
상단의 일정 목록 컴포넌트는 데이터가 추가되니까 배열에 변화가 있어 리렌더링이 되는 것이라고 생각했습니다.
그에 반해 하단의 장소 목록 컴포넌트는 데이터가 동일합니다. 그래서 리렌더링이 왜 발생하는지 궁금했습니다.
사실 저 상태에서는 리렌더링이 되는 것이 맞습니다.
장소를 추가할 때마다 새롭게 추가된 장소를 기준으로 장소 검색을 다시 하도록 구현했기 때문입니다...
그래서 변하지 않는 고정된 데이터를 넣고 해 보았지만 동일하게 리렌더링이 발생했습니다.
❓ useMemo
useMemo를 사용하면 값을 메모이제이션을 하여 동일한 값일 때 렌더링을 다시 하지 않으므로 장소 목록 배열을 useMemo를 사용하여 바꿔보았습니다.
하지만 똑같이 리렌더링이 발생했습니다. 바로 위에서 서술했듯이, 사실 배열은 계속 변하고 있었기 때문에 의미 없는 방법이었습니다.
그래서 같은 장소를 추가해서 배열의 변화를 없애보기도 했고, 고정된 데이터를 담은 배열로 바꿔서 시도해 보았지만 결과는 같았습니다.
❓ React.memo
그렇다면 컴포넌트 자체를 메모이제이션하는 React.memo는 어떨까 하여 적용해 보았습니다.
하지만 마찬가지로 리렌더링이 발생했습니다.
[하루메이트] React.memo를 사용해서 불필요한 리렌더링 제거
수정 전에는 반경 태그와 카테고리 태그를 클릭했을 때 렌더링이 될 필요없는 검색된 장소 목록에 계속 리렌더링이 발생했습니다. // 장소 목록 컴포넌트 const PlaceList = ({ searchPlace, radius, }: { searc
highero.tistory.com
대신 태그를 클릭했을 때 발생하는 리렌더링은 제거할 수 있었습니다.
Reflow
메모이제이션 기법으로는 해결이 안 되고 있으니 다른 방향으로 생각을 해보다가 이런 생각이 들었습니다.
- 장소 추가 버튼을 누른다.
- 장소가 일정 목록에 추가된다.
- 일정 목록에 추가되면 일정 목록 컴포넌트의 height가 늘어난다.
- 그에 따라 밑에 있는 컴포넌트들의 위치가 다시 조정된다.
- 어라.. 그럼 Reflow가 발생해서 리렌더링이 되는 건가...?
먼저 간단하게 브라우저의 동작 원리를 짚고 가겠습니다.
- 브라우저에서 서버에 필요한 자원을 요청합니다.
- 브라우저의 렌더링 엔진이 DOM트리와 CSSOM을 생성합니다.
- 생성된 DOM트리와 CSSOM을 합쳐 렌더 트리를 생성합니다.
- 자바스크립트 코드가 있다면 자바스크립트 엔진으로 파싱 하여 AST를 생성한 뒤, 인터프리터에 전달하여 DOM 또는 CSSOM을 변경합니다. 변경된 트리를 렌더 트리에 반영하여 변경시킵니다. 여기서 Reflow와 Repaint가 발생합니다.
- 렌더 트리를 기반으로 HTML 요소의 레이아웃을 계산하여 페인팅합니다.
Reflow란 레이아웃 계산을 다시 하는 것을 말합니다. 노드의 추가 또는 삭제, 요소의 크기 또는 위치 변경, 윈도우 리사이징 등 레이아웃에 영향을 주는 변경이 발생한 경우 실행됩니다.
DOM은 DOM 내부를 변경하는 DOM API가 사용됐거나 스타일이 변경됐다면 위의 브라우저 동작 과정을 반복한 뒤 리렌더링을 진행합니다.
장소 추가 버튼을 눌러 일정 목록에 장소가 추가되었습니다. 일정 목록 컴포넌트는 추가된 장소 컴포넌트만큼 크기가 커졌고, 그만큼 아래의 컴포넌트들의 위치가 변경되었으므로 Reflow가 발생했고, 리렌더링이 발생하는 것이라고 추측할 수 있었습니다.
음... Reflow가 문제라면 지금 당장 해결할 수는 없을 것 같습니다. 아예 디자인을 변경해야 하고, 그에 따라 컴포넌트를 새로 만들어야 하기 때문에 같이 했었던 팀원들과 상의를 해야 하기 때문입니다.
그래도 리렌더링이 발생하는지는 일정 목록 컴포넌트를 제거해서 확인해 볼 수 있을 것 같습니다.
아... 그대롭니다...
Reflow 만이 문제가 아니었던 것입니다. 다른 방향으로 다시 생각해봐야 할 것 같습니다.
전역 상태?
저희 프로젝트에서는 상태 관리를 Redux-Toolkit을 사용했습니다.
유저 데이터, 커뮤니티 데이터, 일정 데이터, 장소 데이터, UI 변경을 위한 데이터 등 전역으로 관리해야 할 데이터가 많았고, 컴포넌트도 굉장히 많았기 때문입니다.
리액트의 여러 가지의 리렌더링 조건 중 state가 변경이 될 때 리렌더링이 된다는 것이 기억났습니다.
const handleClick = (item: PlacesSearchResultItem, id: string) => {
const placeId = uuidv4();
if (scheduleList.length < 10) {
dispatch(
scheduleListActions.addList({
placeName: item.place_name,
placeUrl: item.place_url,
roadAddressName: item.road_address_name,
id: placeId,
phone: item.phone,
categoryGroupCode: item.category_group_code,
categoryGroupName: item.category_group_name,
x: item.x,
y: item.y,
})
);
} else {
showToast('error', '일정은 10개까지 등록 가능합니다!')();
}
};
이 코드는 장소 컴포넌트에서 추가 버튼을 눌렀을 때 호출되는 함수입니다.
여기서 scheduleListActions.addList를 통해 전역으로 관리되는 일정 목록 배열에 선택한 장소를 추가합니다.
그러면 Redux에 존재하고 있는 scheduleList state에 변경이 일어날 것입니다. 그렇다면 이것을 구독해서 사용하고 있는 컴포넌트에는 리렌더링이 일어날 것입니다.
// 일정 등록 페이지 컴포넌트
const ScheduleRegister = () => {
...
const scheduleList = useSelector(
(state: RootState) => state.scheduleList.list
);
...
};
export default ScheduleRegister;
현재 일정 등록 페이지의 가장 상위 컴포넌트인 일정 등록 페이지 컴포넌트에서 사용하고 있습니다.
또 다른 리액트의 리렌더링 조건 중 하나, 부모 컴포넌트가 렌더링 되면 자식 컴포넌트도 렌더링 됩니다.
따라서, 가장 상위 컴포넌트가 리렌더링 되었으므로, 하위 컴포넌트 중 메모되지 않은 것들은 전부 리렌더링이 될 것이라고 추측할 수 있었습니다.
이 부분 또한 변경해야 할 것이 너무나 많기 때문에 유사한 상황에 빠져있는 다른 컴포넌트로 실험해 보았습니다.
음... 클릭만 해도 리렌더링이 발생합니다. 추가 버튼을 누르지 않았으므로 Reflow는 아닐 것이라고 생각합니다.
export const LocationCard = ({
id,
title,
category,
address,
phone,
onClick,
place_url,
x,
y,
}: LocationCardInfo) => {
const cardRef = useRef<HTMLElement>(null);
const dispatch = useDispatch();
const selectedId = useSelector((state: RootState) => state.marker.markerId);
const highlightMarker = () => {
dispatch(
markerActions.selectMarker({
markerId: id,
center: { lat: y, lng: x },
})
);
};
useEffect(() => {
if (cardRef.current && selectedId === id) {
dispatch(markerActions.setscroll(cardRef.current.offsetTop));
}
}, [dispatch, id, selectedId]);
return (
<LocationCardContainer
ref={cardRef}
selected={selectedId === id}
onClick={highlightMarker}
>
...
</LocationCardContainer>
);
};
이 코드는 장소 카드 컴포넌트입니다.
위에서 설명했던 것과 비슷하게 전역으로 관리되고 있는 marker의 id를 변경하기도 하고, 사용하기도 합니다.
그래서 selectedId를 주석처리하고 다시 확인했습니다.
장소 카드 컴포넌트 각각은 리렌더링 되지 않습니다.
뭔가 마음이 살짝 편안해졌습니다. 하지만 이렇게 유지할 수는 없습니다. selectedId는 좋은 UX 제공을 위해 카드 하이라이트를 유지할 수 있게 해 주고 선택된 카드 위치로 스크롤이 이동할 수 있게 해주는 상태이기 때문입니다.
결론 ?
리렌더링이 발생하니까 단순하게 메모해서 없애야지~ 하는 생각으로 접근했었습니다.
단순하게 접근했지만 여러 가지의 고민을 해보게 되었고, 복합적인 요소들로 인해 발생하는 리렌더링이었다는 것을 알게 되었습니다.
(하지만 제가 추측한 이유들이 맞을 수도 있고 아닐 수도 있고, 또 다른 이유 때문에 발생한 것일 수도 있습니다...)
이 과정들을 거치며 제가 작성한 코드들을 다시 볼 수 있는 좋은 기회가 되었습니다.
분명 작성할 때는 컴포넌트를 이렇게 분리해야지, 저렇게 분리해야지, 이 상태는 전역으로 관리하자, 이건 그냥 컴포넌트 내부에서 관리하자 같은 생각을 하면서 작성했지만... 어림도 없었습니다.
어떻게 컴포넌트를 분리해서 조합할지, 상태를 어떻게 관리하고 사용할지, useMemo 또는 memo() 같은 기술을 사용할 때 정확하게 알고 사용하기 위해 앞으로 더 노력해야겠습니다.
3줄 요약
- 프로젝트에서 리렌더링이 발생해서 해결하기 위해 단순하게 접근했다.
- 추측해 볼 수 있는 이유가 많았고, 내가 알 수 없는 이유들도 있을 거란 생각이 들었다.
- 코드를 작성할 때 명확한 근거를 가지고 작성해야 하고, 그렇게 하기 위해서 많은 공부가 필요하다.
참고
Web: 최적화와 React.memo, useMemo 알아보기
최적화에 대해 알아보고 React에서 최적화를 위한 몇몇 기능을 알아보자.
medium.com
Reflow, Repaint을 알아보자!
오랜만의 글인 것 같습니다. 최근에 제 자신에 대한 회의감도 들었고, 그래서 잠시 슬럼프를 겪었지만, 이제 어느정도 극복하고, 다시 꾸준함을 찾으려합니다. 결국 꾸준함이야 말로 제가 성장
velog.io
[react] 리렌더링이 되는 조건들 살펴보기
state 변경이 있을 때 - react 에서 유동적인 데이터를 저장하기 위해서 state 라는 것을 이용한다. 이때 state 값을 바꿔주기 위해서는 state 를 직접 조작해서는 안되고 setState() 메서드를 이용해 주어
seungddak.tistory.com
'React' 카테고리의 다른 글
RTK Query를 간단하게 알아보자 ( Feat. TMDB API를 활용한 예제 ) (4) | 2023.04.25 |
---|---|
Redux를 간단하게 알아보자 (0) | 2023.04.24 |
React 간단 정리 (5) | 2023.03.22 |