과제 체크포인트
https://legitgoons.github.io/front_6th_chapter4-2
과제 요구사항
-
배포 후 url 제출
-
API 호출 최적화(
Promise.all이해) -
SearchDialog 불필요한 연산 최적화
-
SearchDialog 불필요한 리렌더링 최적화
-
시간표 블록 드래그시 렌더링 최적화
-
시간표 블록 드롭시 렌더링 최적화
과제 셀프회고
기술적 성장
1. 캐시를 구현해보기
- 캐시로 api 호출 최적화하기
const createCachedFetch = () => {
const cache = new Map<string, Promise<AxiosResponse<Lecture[]>>>();
const fetchMajors = () => {
if (!cache.has('majors')) { // 캐시가 없다면 새로 axios 요청 호출 후 저장
cache.set('majors', axios.get<Lecture[]>('./schedules-majors.json'));
}
return cache.get('majors')!; // 동일한 Promise 객체 반환
};
//...
// 6번 호출하지만 실제로는 2번의 네트워크 요청만 발생
const fetchAllLectures = async () =>
await Promise.all([
fetchMajors(), // 새로운 요청
fetchLiberalArts(), // 새로운 요청
fetchMajors(), // 캐시에서 반환
fetchLiberalArts(), // 캐시에서 반환
fetchMajors(), // 캐시에서 반환
fetchLiberalArts() // 캐시에서 반환
]);
2. props에는 가능한 원시값을 넘겨주기
컴포넌트는 랜더링 될 때 마다 내부에서 새로운 선언이 이뤄진다. 근데 리액트는 Object.is()를 사용해서 원시값은 값만 비교하는데, 객체는 참조를 비교한다
고로 부모에서 매번 리랜더링이 일어나는 상태에서 참조값을 자식 컴포넌트에 props로 넘겨주면? memo해봤자 props가 계속 바뀌니까 자식 컴포넌트도 계속 리랜더링된다!
그렇기에, props에는 가능한 원시값을 넘겨주는 것이 권장된다.
3. 메모이제이션을 적절하게 잘 사용하는 방법
그럼에도 불구하고 참조값을 props로 넘기고 싶다면 두 가지 방법이 있다.
- 일단 받은 다음 react.memo의 propsAreEqual 함수 활용
- react.memo의 propsAreEqual 함수 활용 예시
// LectureTableRow.tsx
const LectureTableRow = React.memo<LectureTableRowProps>(
({ lecture, index, onAddSchedule }) => {
return (
<Tr key={`${lecture.id}-${index}`}>
//...
</Tr>
);
},
// 커스텀 비교 함수: 각 필드를 하나씩 개별적으로 비교, 값이 true면 그대로 유지하고, false일 때에만 리렌더링 진행
(prevProps, nextProps) => {
return (
prevProps.lecture.id === nextProps.lecture.id &&
prevProps.lecture.grade === nextProps.lecture.grade &&
//...
);
}
);
- useCallback으로 메모이제이션 한 후 props로 넘겨주기
- useCallback의 경우, 컴포넌트가 리랜더링 되는 경우에도 의존값만 바뀌지 않으면 매번 재생성되는 것을 막을 수 있기에 props로 넘겨줘도 안전하다.
NOTE
메모이제이션 기법 중 나머지 하나인 useMemo의 경우, 비용이 큰 계산을 최적화 할 때 사용된다. feat: 필터링된 강의 목록과 전공 목록 메모이제이션 참조
Context의 Provider를 사용해서 상태를 격리하기
<ScheduleDndProvider>의 경우 모든 table이 같은 provider 내부에 있어서 하나의 table의 값만 변경되어도 모두 리랜더링이 일어나고 있었다. 이를 provider를 분리해서 상태를 격리하고, 리랜더링이 각 테이블만 일어나도록 수정했다.
이후 Drop 시 전체 리렌더링이 일어나는 ScheduleContext도 비슷한 문제가 있었는데, 이는 아래와 같은 방법으로 해결했다.
코드 품질
가장 기억에 남는 부분: Drop 시 렌더링 최적화
ScheduleContext는 모든 테이블의 스케줄 데이터를 하나의 schedulesMap으로 관리한다. 고로 Drop 시 schedulesMap가 다시 업데이트되고, 이를 구독하는 모든 컴포넌트들에서 리랜더링이 일어나고 있었다.
생각한 해결방안은 다음과 같았다.
- Context 분리 - 모두 동일한 Context를 사용하는게 문제였기에, 가장 먼저 떠올린 방법. Context를 분리함으로써 구독하던 상태도 분리하면 문제가 해결된다.
- zustand 사용
- 메모이제이션만으로 해결
- State에 주입 ✅ - 사실 재밌는 방법이라고 생각해서 선택했다. 전역으로 있는 상태를 TableWrapper로 각 Table마다 state를 만들어 준 다음 상태를 주입하고, 그 다음부터는 각 테이블별로 상태를 관리하도록 구현했다.
// ScheduleContext.tsx
interface ScheduleContextType {
initialSchedulesMap: Record<string, Schedule[]>; // 초기값만 제공
//...
}
// TableWrapper.tsx - 각 테이블별 독립 상태
const TableWrapper = ({ tableId, index }: TableWrapperProps) => {
const { initialSchedulesMap, duplicateTable, removeTable } = useScheduleContext();
// 각 테이블마다 독립적인 로컬 state
const [schedules, setSchedules] = useState<Schedule[]>(
initialSchedulesMap[tableId] || []
);
// 로컬 상태 변경 함수들
const handleScheduleAdd = useCallback((newSchedules: Schedule[]) => {
setSchedules(prev => [...prev, ...newSchedules]); // 자신의 state만 변경
}, []);
return (
<ScheduleDndProvider
schedules={schedules}
onSchedulesChange={setSchedules}
>
{/* 테이블 UI */}
</ScheduleDndProvider>
);
};
다만 메모리측면에서 같은 상태를 context와 state 양쪽에서 가지고 있어야 한다는 점, 그리고 전역 상태가 아닌 로컬 상태로 만들어서 관리하는 방법인데 전역상태가 필요하다면 골치아파지기에 실제 프로덕트였다면 zustand를 사용했을 것 같다.
학습 효과 분석
가장 아쉬운 점은 지난 회사에서 대용량 데이터로 차트를 그릴 일이 많았는데, 그때는 이런 성능 최적화 기법들을 알고 있지 못했다는 점이다. 그 당시에는 메모이제이션 기법 조차도 명확하게 이해하지 못해서 사용을 꺼려했었다.
이제는 어떤 최적화 기법들이 있고, 왜 사용해야 하는지와 언제 사용해야하는지를 익혔기에 조금씩이나마 사용할 수 있을 것 같다.
as-is
- useCallback, useMemo, React.memo 등의 메모이제이션 기법을 정확히 알고 사용하지 못함
- 컴포넌트를 적절하게 나누는 것이 성능 최적화에도 영향을 미치는 것을 알지 못함
to-be
- 메모이제이션 기법을 언제, 왜 사용해야하는지 이해함
- 성능 최적화를 위해 컴포넌트를 적절하게 나눌 수 있음
과제 피드백
리뷰 받고 싶은 내용
이번 과제를 진행하면서 이제 useCallback과 React.memo의 경우에는 감이 잡히는 것 같습니다. 하지만 useMemo의 경우에는 아직 좀 아리송한데요. useMemo는 그냥 단순히 계산이 큰 값을 메모이제이션 하는 것 외에는 사용방법이 없는지가 궁금합니다. 또, react 공홈에서는 useMemo의 경우 계산에 1ms이상이 걸릴 때, useMemo를 사용하는 것을 권장한다는데, 실제로 코치님의 경우에는 이걸 측정하면서 사용하시나요...?
과제 피드백
안녕하세요 의찬님! 마지막 과제 잘 진행해주셨네요 ㅎㅎ 그동안 너무 고생 많으셨어요!!!
이번 과제를 진행하면서 이제 useCallback과 React.memo의 경우에는 감이 잡히는 것 같습니다. 하지만 useMemo의 경우에는 아직 좀 아리송한데요. useMemo는 그냥 단순히 계산이 큰 값을 메모이제이션 하는 것 외에는 사용방법이 없는지가 궁금합니다. 또, react 공홈에서는 useMemo의 경우 계산에 1ms이상이 걸릴 때, useMemo를 사용하는 것을 권장한다는데, 실제로 코치님의 경우에는 이걸 측정하면서 사용하시나요...?
측정하면서 사용하진 않아요 ㅎㅎ 대신 주로 배열 연산을 하거나 혹은 Memo를 적용한 컴포넌트에 메모이제이션된 값을 넘겨줄 때 사용하는 편이랍니다!
특히 한 번 메모이제이션을 적용하기 시작하면 연쇄적으로 적용해야 하는 경우가 많다보니.. 최적화를 할 때 한 군데만 적용하기가 어려워요.
결론은, 필요할 때 적용하면 된답니다 ㅋㅋ