항해 마지막 주차 과제 회고!
이번주차는 코드 기반 성능 최적화 주차였다.
SearchDialog가 초기부터 렌더링 되지않고, 필요할때 리렌더링될수 있게 수정
//ScheduleTables
{searchInfo && <SearchDialog searchInfo={searchInfo} onClose={() => setSearchInfo(null)} />}
SearchForm들은 각 카테고리별로 수정이 될때 리렌더링이 되면 되는데 현재는 다른 카테고리가 수정되더라고 전체가 리렌더링되고 있음. 각 form별로 컴포넌트 분리 및 memo적용.
<ModalBody>
<VStack spacing={4} align="stretch">
<HStack spacing={4}>
<SearchQuery query={searchOptions.query ?? ""} changeSearchOption={changeSearchOption} />
<SearchCredits credits={searchOptions.credits ?? 0} changeSearchOption={changeSearchOption} />
</HStack>
<HStack spacing={4}>
<SearchGrade grades={searchOptions.grades} changeSearchOption={changeSearchOption} />
<SearchDay days={searchOptions.days} changeSearchOption={changeSearchOption} />
</HStack>
<HStack spacing={4}>
<SearchTimes times={searchOptions.times} changeSearchOption={changeSearchOption} />
<SearchMajors
allMajors={allMajors}
majors={searchOptions.majors}
changeSearchOption={changeSearchOption}
/>
</HStack>
<Text align="right">검색결과: {filteredLectures.length}개</Text>
...
</VStack>
</ModalBody>
필터가 수정됨에따라 수정될필요가 없는 TableHeader가 리렌더링되고 있음. TableHeader 컴포넌트 분리 및 memo 적용.
const filteredLectures = useMemo(() => getFilteredLectures(), [lectures, searchOptions]);
const lastPage = useMemo(() => Math.ceil(filteredLectures.length / PAGE_SIZE), [filteredLectures]);
const visibleLectures = useMemo(() => filteredLectures.slice(0, page * PAGE_SIZE), [filteredLectures, page]);
const allMajors = useMemo(() => [...new Set(lectures.map(lecture => lecture.major))], [lectures]);
검색결과로 나오는 과목 목록들을 SearchLecture 컴포넌트 분리 및 memo 적용
<Box overflowY="auto" maxH="500px" ref={loaderWrapperRef}>
<Table size="sm" variant="striped">
<Tbody>
{visibleLectures.map((lecture, index) => (
<SearchLecture key={`${lecture.id}-${index}`} lecture={lecture} addSchedule={addSchedule} />
))}
</Tbody>
</Table>
<Box ref={loaderRef} h="20px" />
</Box>
ScheduleDndProvider의 범위를 전역 -> 개별 시간표로 줄여주면서 리렌더링의 범위를 축소시켜주었습니다.
수정전
function App() {
return (
<ChakraProvider>
<ScheduleProvider>
<ScheduleDndProvider>
<ScheduleTables/>
</ScheduleDndProvider>
</ScheduleProvider>
</ChakraProvider>
);
}
수정후
function App() {
return (
<ChakraProvider>
<ScheduleProvider>
<ScheduleTables />
</ScheduleProvider>
</ChakraProvider>
);
}
export const ScheduleTables = () => {
...
return (
<Flex w='full' gap={6} p={6} flexWrap='wrap'>
{Object.entries(schedulesMap).map(([tableId, schedules], index) => (
...
<ScheduleDndProvider>
<ScheduleTable {...} />
</ScheduleDndProvider>
...
)}
)
}
문제1
시간표 블록 드롭시 해당되는 수정한 시간표뿐만 아니라 전체 시간표가 리렌더링이 됩니다.
원인
useContext의 context를 참조하는 컴포넌트는 memo가 되어도 context값이 변경되면 리렌더링되는게 원인이었습니다.
1. 시간표 A에서 드래그 종료
2. 시간표 A의 `ScheduleDndProvider`의 `handleDragEnd` 실행
3. `setSchedulesMap` 호출 → 전역 `schedulesMap` 업데이트
4. 모든 `ScheduleDndProvider`가 전역 `schedulesMap`을 구독하고 있으므로:
- 시간표 B의 `ScheduleDndProvider`도 재렌더링
- 시간표 C의 `ScheduleDndProvider`도 재렌더링
5. 각 `ScheduleDndProvider` 재렌더링 → 하위 `DraggableSchedule` 모두 리렌더링
해결
ScheduleContext에서 scheduleValue와 setScheduleValue 로 context를 분리해주었습니다.
export const ScheduleProvider = ({ children }: PropsWithChildren) => {
const [schedulesMap, setSchedulesMap] = useState<Record<string, Schedule[]>>(dummyScheduleMap);
return (
<SetScheduleContext.Provider value={setSchedulesMap}>
<ScheduleContext.Provider value={schedulesMap}>{children}</ScheduleContext.Provider>
</SetScheduleContext.Provider>
);
};
ScheduleDndProvider에서 scheduleContext호출을 제거하고,setScheduleContext만 참조할수 있게 수정해주었습니다.
const { setSchedulesMap } = useSetScheduleContext() ?? { setSchedulesMap: () => {} };
const handleDragEnd = (event: any) => {
const { active, delta } = event;
const { x, y } = delta;
const [tableId, index] = active.id.split(":");
const moveDayIndex = Math.floor(x / 80);
const moveTimeIndex = Math.floor(y / 30);
setSchedulesMap((prevSchedulesMap: Record<string, Schedule[]>) => {
...
}
};
문제2
시간표 블록 드롭시 수업만이 아니라 렌더링될 필요없는 그리드까지 리렌더링이 되는 문제가 있었습니다.
해결
변경이 불필요한 Grid부분만 컴포넌트를 분리후 memo로 변경되지 않게 수정해주었습니다.
const ScheduleTable = memo(({ tableId, schedules, onScheduleTimeClick, onDeleteButtonClick }: Props) => {
const TableGrid = memo(
({ onScheduleTimeClick }: { onScheduleTimeClick: (timeInfo: { day: string; time: number }) => void }) => {
return (
<Grid
...
>
...
</Grid>
);
},
);
return (
<Box position="relative" outline={activeTableId === tableId ? "5px dashed" : undefined} outlineColor="blue.300">
<TableGrid onScheduleTimeClick={onScheduleTimeClick} />
{schedules.map((schedule, index) => (
<DraggableSchedule
key={`${schedule.lecture.title}-${index}`}
id={`${tableId}:${index}`}
data={schedule}
bg={getColor(schedule.lecture.id)}
onDeleteButtonClick={() => onDeleteButtonClick?.({ day: schedule.day, time: schedule.range[0] })}
/>
))}
</Box>
);
});
최종 개선