불필요한 리렌더링 문제 해결하기
프로젝트를 진행하다 보면 성능 이슈 중 하나로 페이지 리렌더링 문제가 자주 발생합니다.
특히, 우리 방끗과 같이 여러 개의 인풋 필드가 있는 폼을 제공하는 서비스에서 사용자가 입력할 때마다 페이지 전체가 리렌더링 되는 경우 성능 저하를 유발할 수 있습니다.
우리도 같은 문제를 겪었고 이번 글에서 우리 팀이 이러한 문제를 해결한 과정을 공유하고자 합니다.
1. 문제 정의: 페이지 전체 리렌더링
문제가 발생한 페이지는 방의 기본 정보를 입력하는 약 10개의 인풋 필드가 있는 폼 페이지였습니다.
초기 작업할 때는 빠르게 작업을 진행하려다보니 발견하지 못했지만 레벨 3 이후 프로젝트 회고를 통해 이러한 이슈가 있다는 것을 발견했습니다.
발견된 문제는 폼의 각 인풋 필드가 변경될 때마다 페이지 전체가 리렌더링 된다는 것이었습니다. 이 문제는 사용자가 데이터를 입력할 때 UI 응답성을 저하시키고, 장기적으로는 사용자 경험에 부정적인 영향을 미칠 수 있었습니다.
2. 원인 분석: 상태 관리 방식의 문제
팀원이 작성한 회고를 읽으면서 코드를 다시 한번 읽어보게 되었습니다.
휴가 기간 동안 코드를 잠시 떠나 있었던 덕분에, 우리가 작성한 코드를 더 객관적으로 볼 수 있었습니다. (물론, 잠깐 동안 코드를 잊었기 때문일지도 모르지만요! 헷)
처음에는 스토어에서 문제가 발생한다고 생각했고, 코드를 다시 한번 살펴봤습니다.
각 인풋 필드가 스토어에서 관리되고 있어 인풋 값이 변경될 때마다 스토어 전체 상태가 업데이트되면서 페이지가 리렌더링 되는 것이라고 추측했습니다.”
// 스토어 호출 예시
const address = useStore(roomInfoStore, state => state.address);
const actions = useStore(checklistRoomInfoStore, state => state.actions);
그런데 분명한 건, 이러한 문제를 막기 위해 스토어를 제작할 때 작은 상태로 하나씩 관리될 수 있도록 작업을 진행했다는 점이었습니다. 그리고 실제로도 상태를 세분화하여 관리하도록 설정되어 있었습니다.
해당 스토어에서는 아무런 문제가 없었습니다.
문제는 데이터를 POST 하기 위한 훅 호출 위치에서 발생했습니다. 제출 버튼과 함께 추상화되지 않고, 페이지 내에서 호출되면서 props로 전달되고 있었기 때문에, 모든 인풋 값이 변경될 때마다 페이지 전체가 다시 구독되고 리렌더링 되는 상황이었습니다.
// 체크리스트 POST
const { handleSubmitChecklist } = useChecklistPost(summaryModalClose);
3. 해결 방안: 상태 분리와 최적화
문제를 해결하기 위해 우리는 코드의 구조를 개선하고 상태 관리를 보다 효율적으로 변경했습니다. 이전에는 SummaryModal 컴포넌트에서 리렌더링을 유발하는 주요 원인이 모든 상태와 데이터를 상위 컴포넌트에서 전달하는 방식이었습니다.
이를 최적화하기 위해 상태를 개별적으로 관리하고 추상화된 훅을 도입했습니다.
기존 코드
기존 코드에서는 SummaryModal이 열릴 때마다 submitChecklist 함수가 상위 컴포넌트에서 props로 전달되어 전체 페이지가 리렌더링 되었습니다.
{isSummaryModalOpen && (
<SummaryModal
isModalOpen={isSummaryModalOpen}
modalClose={summaryModalClose}
submitChecklist={handleSubmitChecklist} // 상위에서 props로 전달
/>
)}
기존 SummaryModal 컴포넌트는 submitChecklist를 직접 받아 처리하고 있었습니다.
interface Props {
isModalOpen: boolean;
modalClose: () => void;
submitChecklist: () => void;
}
const SummaryModal = ({ isModalOpen, modalClose, submitChecklist }: Props) => {
const { rawValue: roomInfo } = useStore(checklistRoomInfoStore);
return (
<Modal isOpen={isModalOpen} onClose={modalClose}>
<Modal.header>
<S.CounterContainer>
<CounterBox currentCount={roomInfo.summary?.length || 0} totalCount={15} />
</S.CounterContainer>
<Button size="full" color="dark" onClick={submitChecklist} isSquare label="체크리스트 저장하기" />
</Modal.body>
</Modal>
);
};
최적화 후 코드
최적화 후, SummaryModal은 더 이상 상위에서 submitChecklist를 props로 전달받지 않고, 내부에서 훅을 통해 처리하도록 변경되었습니다. 이렇게 함으로써 불필요한 props 전달로 인한 리렌더링을 줄일 수 있었습니다. 또한, mutateType을 통해 체크리스트 추가 또는 수정 기능을 통합하여 컴포넌트를 더 간결하게 만들었습니다.
<SummaryModal
isModalOpen={isSummaryModalOpen}
modalClose={summaryModalClose}
mutateType="add" // 상태별로 모달 동작을 제어
/>
SummaryModal 컴포넌트도 변경되었습니다. 이제 submitChecklist 대신 내부에서 훅(useMutateChecklist)을 사용해 상태를 관리하고, 이로 인해 컴포넌트의 응집력이 향상되었습니다.
interface Props {
isModalOpen: boolean;
modalClose: () => void;
mutateType: MutateType;
checklistId?: number;
}
const SummaryModal = ({ isModalOpen, modalClose, mutateType, checklistId }: Props) => {
const navigate = useNavigate();
const { rawValue: roomInfo } = useStore(checklistRoomInfoStore);
// 체크리스트 (작성/수정) 훅 사용
const { handleSubmitChecklist } = useMutateChecklist(mutateType, checklistId);
const handleCloseModal = () => {
handleSubmitChecklist(); // 훅으로 체크리스트 처리
modalClose();
navigate(ROUTE_PATH.checklistList); // 모달 닫기 후 네비게이션
};
return (
<Modal isOpen={isModalOpen} onClose={modalClose}>
<Modal.header>
<S.CounterContainer>
<CounterBox currentCount={roomInfo.summary?.length || 0} totalCount={15} />
</S.CounterContainer>
<Button size="full" color="dark" onClick={handleCloseModal} isSquare label="체크리스트 저장하기" />
</Modal.body>
</Modal>
);
};
이러한 방식으로 상태를 분리하고 props 의존도를 줄임으로써 리렌더링 문제를 해결할 수 있었습니다.
4. 적용 후 결과
최적화를 적용한 후, 페이지의 리렌더링 빈도가 눈에 띄게 줄어들었고, 인풋 필드가 변경될 때 전체 페이지가 아닌 해당 필드만 업데이트되는 것을 확인할 수 있었습니다.
적용 후 주요 개선 사항
- 리렌더링 최소화: 상태가 분리되면서 불필요한 리렌더링이 사라졌고, 각 인풋 필드가 사용되는 시점에만 리랜더링이 되었습니다.
- 페이지 성능 개선: 여러 인풋 필드에서 데이터를 동시에 입력하더라도 성능 저하가 발생하지 않을 것입니다.
- 유지보수 용이성: 코드의 구조가 단순해졌고, 각 컴포넌트의 역할이 분명해져 유지보수가 수월해졌습니다.
이 과정을 통해 불필요하게 리랜더링 된 페이지를 고칠 수 있었던 좋은 기회였습니다.
다음에도 방끗의 성능 개선을 위해 많은 시도를 해볼 거 같습니다.
'우테코' 카테고리의 다른 글
📦 코드잽 프로젝트를 모노레포로 마이그레이션하는 이유와 계획 (0) | 2024.12.18 |
---|---|
[우테코 6기] 사용자 피드백 반영 및 A/B 테스트로 UX 개선하기 (2) | 2024.11.27 |
[우테코 6기] 모든 팀의 AWS S3 프로젝트 폴더를 날렸다고..? (0) | 2024.10.02 |
[우테코 6기] 개발자가 현장 유저 테스트에서 깨달은 것: 불편함을 개선하다 (2) | 2024.10.02 |
[우테코 6기] 지치지 않고 협업하는 법 (0) | 2024.10.01 |