[4팀 안소은] Chapter 4-2. 코드 관점의 성능 최적화 #29
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
과제 체크포인트
과제 배포 URL
https://ahnsummer.github.io/front_7th_chapter4-2/
과제 요구사항
배포 후 url 제출
API 호출 최적화(
Promise.all이해)SearchDialog 불필요한 연산 최적화
SearchDialog 불필요한 리렌더링 최적화
시간표 블록 드래그시 렌더링 최적화
시간표 블록 드롭시 렌더링 최적화
구현 내용
1. API 호출 최적화
기존 코드의 문제점
문제점:
Promise.all내부에await가 있어서 병렬이 아닌 직렬로 실행됩니다개선 흐름
1단계: await 제거하여 병렬 처리
Promise.all내부의await를 제거하여 Promise 객체만 전달2단계: 캐시 객체를 사용하여 데이터 캐싱
3단계: 추상화된 훅으로 고도화
useFetch훅을 구현하여 재사용 가능하고 타입 안전한 API 호출 시스템 구축query함수로 API 호출을 추상화하고,useFetches훅으로 여러 쿼리를 병렬 처리최종 구현:
성능 개선:
2. SearchDialog 불필요한 연산 최적화
문제점
getFilteredLectures함수가 매 렌더링마다 실행됩니다allMajors배열이 매번 재계산됩니다개선 내용
filteredLectures를useMemo로 메모이제이션합니다 (의존성:lectures,searchOptions)allMajors를useMemo로 메모이제이션합니다 (의존성:lectures)3. SearchDialog 불필요한 리렌더링 최적화
문제점
개선 내용
absolute와top을 이용하여 레이아웃 시프트를 방지합니다4. 시간표 블록 드래그시 렌더링 최적화
문제점
useDndContext를 각 테이블에서 사용하여 드래그 상태 변경 시 전체가 리렌더링됩니다개선 내용
ScheduleTables에서만useDndContext를 사용합니다isActive={true}prop을 전달합니다ScheduleTable을 메모이제이션하여schedules와isActive가 변경되지 않으면 리렌더링을 방지합니다작동 방식:
activeTableId가 "schedule-1"로 변경됩니다isActive={true}prop을 받아 하이라이트를 표시합니다isActive={false}로 유지되어 리렌더링되지 않습니다5. 시간표 블록 드롭시 렌더링 최적화
문제점
schedulesMap전체가 하나의 큰 객체로 관리되어 한 개의 스케줄만 변경되어도 모든 테이블이 리렌더링됩니다개선 내용
ScheduleTable의 비교 함수에서schedules배열만 깊은 비교를 수행합니다schedulesprop만 확인하여 변경되지 않으면 리렌더링을 방지합니다작동 방식:
schedulesMap["schedule-1"]만 변경됩니다schedulesprop이 변경되어 리렌더링됩니다schedulesprop이 동일하여 리렌더링되지 않습니다과제 셀프회고
가상 스크롤 구현 삽질기
처음엔 IntersectionObserver로 구현했는데, 스크롤을 빠르게 움직이면 onImpression이 씹히는 문제가 있었습니다. 최대한 IntersectionObserver로 구현해보고자 했으나 실패하여 스크롤 이벤트로 갈아엎었고, 레이아웃 시프트 때문에 또 한참 삽질했습니다...
처음에는 빈 tr 요소에 height 박아서 상하단 마진을 구현하고 있었습니다. 그런데 스크롤을 내린 후 다시 위로 올리면 스크롤 위치가 이상해지면서 계속 스크롤이 되는 버그에 걸려서 찾아보니 레이아웃 재계산을 유발하는 CSS 속성 변경으로 인한 레이아웃 쉬프트가 원인일 것이라는 글들이 많이 보였습니다. 그래서 absolute positioning + transform으로 다시 구현했습니다.
Promise.all과 캐싱
초반 코드에서는 Promise.all 안에 await가 있어서 코드가 병렬 동작하지 않고 순차 실행되고 있었습니다. await 제거하고 Promise 객체만 넘겨서 병렬처리 되도록 수정했습니다.
중복 호출도 캐시 객체로 처리했는데, 이걸 좀 더 일반화해서 useFetchs 훅으로 만들어봤습니다.
React.memo 최적화
드래그할 때 전체 테이블이 다 리렌더링되는게 거슬려서 ScheduleTable을 memo로 감싸고 커스텀 비교 함수에서 JSON.stringify로 비교를 구현 했는데... 이게 좀 찝찝하긴 합니다. 성능상으론 괜찮을까?
상태 관리 위치도 ScheduleTables로 올려서 activeTableId만 관리하게 했더니 불필요한 리렌더링이 많이 줄었습니다. 보통은 렌더링 최적화를 위해 상태를 하위 요소로 더 보냈던 것 같은데(렌더링 범위를 줄이기 위해) 오히려 전역상태나 특수한 경우에는 부모가 상태를 가지게 하고 memo를 조합하여 사용하는 것이 더 유리할 수 있다는 것을 처음 이해하게 되었습니다.
아쉬운 점
리뷰 받고 싶은 내용
1. React.memo의 JSON.stringify 비교 함수
ScheduleTable.tsx에서 memo 비교 시JSON.stringify를 사용했는데, 성능상 문제가 될 수 있을까요? schedules 배열을 직접 순회하며 비교하는 게 나을까요? 아니면 이 정도는 오버 엔지니어링인가요?2. VirtualScroll 스크롤 이벤트 최적화
현재 스크롤 이벤트를 throttle/debounce 없이 바로 처리하는데, 실제로 성능 문제가 될까요? 추가해야 한다면 어느 정도 delay가 적절할까요?
3. useFetch 훅 구조
useFetch를 구현하면서 캐싱을 전역 객체로 관리하고 createStore, useStore와 같은 유틸리티를 만들어서 전역 상태와 같이 사용하고 있습니다. 이렇게 할 경우 ContextAPI를 사용하지 않아도 되어서 사용하는 입장에서 좀 더 효율적일 것 같은데, 실제 라이브러리에서는 왜 이런 모델을 사용하지 않고 Provider를 주입하는 패턴을 더 많이 유지할까요?