adds9810 님의 상세페이지[4팀 김지혜] Chapter 4-2 코드 관점의 성능 최적화

과제 체크포인트

https://adds9810.github.io/front_6th_chapter4-2/

과제 요구사항

  • 배포 후 url 제출

  • API 호출 최적화(Promise.all 이해)

  • SearchDialog 불필요한 연산 최적화

  • SearchDialog 불필요한 리렌더링 최적화

  • 시간표 블록 드래그시 렌더링 최적화

  • 시간표 블록 드롭시 렌더링 최적화

과제 셀프회고

기술적 성장

API 호출 최적화 - Promise.all의 올바른 사용법

await 키워드를 제거하여 진정한 병렬 실행을 구현하는 방법을 학습했습니다. 이를 통해 API 호출이 6번에서 2번으로 66% 감소하고, 실행 시간이 101.6ms에서 56.8ms로 약 44.8ms 단축되는 성능 개선을 달성했습니다.

// 최적화 전: 직렬 실행 + 중복 호출 (6번)
const fetchAllLectures = async () => await Promise.all([
  (console.log('API Call 1', performance.now()), await fetchMajors()),
  (console.log('API Call 2', performance.now()), await fetchLiberalArts()),
  // ... 6번 중복 호출
]);

// 최적화 후: 병렬 실행 + 캐시 활용 (2번)
const fetchAllLectures = async (): Promise<Lecture[]> => {
  const [majorsData, liberalData] = await Promise.all([
    getCachedMajors(),      // 캐시된 전공 데이터
    getCachedLiberalArts(), // 캐시된 교양 데이터
  ]);
  return [...majorsData, ...liberalData];
};
image

SearchDialog 불필요한 연산 최적화 - useMemo 활용

복잡한 필터링 로직이 매번 렌더링 될 때마다 실행되는 문제를 useMemo로 해결했습니다. 이를 통해 의존성 배열 값이 변경될 때만 필터링 로직이 재실행되도록 최적화했습니다.

// 최적화 전: 매 렌더링마다 복잡한 필터링 실행
const getFilteredLectures = () => {
  const { query = '', credits, grades, days, times, majors } = searchOptions;
  return lectures
    .filter(lecture => lecture.title.toLowerCase().includes(query.toLowerCase()) || lecture.id.toLowerCase().includes(query.toLowerCase()))
    .filter(lecture => grades.length === 0 || grades.includes(lecture.grade))
    .filter(lecture => majors.length === 0 || majors.includes(lecture.major))
    .filter(lecture => !credits || lecture.credits.startsWith(String(credits)))
    .filter(lecture => {
      if (days.length === 0) return true;
      const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : [];
      return schedules.some(s => days.includes(s.day));
    })
    .filter(lecture => {
      if (times.length === 0) return true;
      const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : [];
      return schedules.some(s => s.range.some(time => times.includes(time)));
    });
};
const filteredLectures = getFilteredLectures(); // 매 렌더링마다 실행

// 최적화 후: useMemo로 메모이제이션
const filteredLectures = useMemo(() => {
  return lectures
    .filter(lecture => lecture.title.toLowerCase().includes(query.toLowerCase()))
    .filter(lecture => grades.length === 0 || grades.includes(lecture.grade))
    .filter(lecture => majors.length === 0 || majors.includes(lecture.major));
}, [lectures, query, grades, majors]);

SearchDialog 불필요한 리렌더링 최적화 - useCallback과 React.memo

이벤트 핸들러 함수들이 매 렌더링마다 새로 생성되어 자식 컴포넌트들이 계속 리렌더링되는 문제를 해결했습니다. useCallback으로 함수 참조를 안정화하고, 각 필터 섹션을 useMemo로 분리하여 독립적인 리렌더링을 보장했습니다.

// 최적화 전: 매 렌더링마다 새 함수 생성
const changeSearchOption = (field: keyof SearchOption, value: SearchOption[typeof field]) => {
  setPage(1);
  setSearchOptions({ ...searchOptions, [field]: value });
  loaderWrapperRef.current?.scrollTo(0, 0);
};

// 최적화 후: useCallback으로 메모이제이션
const handleQueryChange = useCallback((value: string) => {
  setPage(1);
  setQuery(value);
  loaderWrapperRef.current?.scrollTo(0, 0);
}, []);

// 각 필터 섹션을 useMemo로 메모이제이션
const querySection = useMemo(
  () => (
    <FormControl>
      <FormLabel>검색어</FormLabel>
      <Input
        placeholder="과목명 또는 과목코드"
        value={query}
        onChange={(e) => handleQueryChange(e.target.value)}
      />
    </FormControl>
  ),
  [query, handleQueryChange]
);

시간표 블록 드래그시 렌더링 최적화 - isLazy prop과 메모이제이션

드래그 중에도 Popover 컴포넌트가 계속 리렌더링되는 문제를 해결했습니다. isLazy prop을 사용하여 PopoverContent가 실제로 열릴 때만 렌더링되도록 하고, React.memo의 커스텀 비교 함수를 통해 드래그하는 블록만 리렌더링되도록 최적화했습니다.

// 최적화 전: 드래그 중에도 Popover 렌더링
<Popover>
  <PopoverTrigger><Box /* ... */ /></PopoverTrigger>
  <PopoverContent>
    <PopoverBody>
      <Text>강의를 삭제하시겠습니까?</Text>
      <Button onClick={onDeleteButtonClick}>삭제</Button>
    </PopoverBody>
  </PopoverContent>
</Popover>

// 최적화 후: isLazy prop + 조건부 렌더링
<Popover isLazy>
  <PopoverTrigger><Box /* ... */ /></PopoverTrigger>
  <MemoizedPopoverContent onDeleteButtonClick={onDeleteButtonClick} />
</Popover>

// 드래그 중일 때는 Popover 없이 렌더링
if (isDragging) {
  return <Box /* ... */ />;
}

시간표 블록 드롭시 렌더링 최적화 - Context 분리 패턴

드래그 앤 드롭으로 스케줄을 이동할 때, 변경되지 않은 다른 시간표들도 모두 리렌더링되는 문제를 해결했습니다. 각 테이블마다 독립적인 TableContext를 생성하여 완전히 격리된 상태 관리로 해결했습니다. 이를 통해 드롭한 시간표만 리렌더링되고 다른 시간표는 영향받지 않는 최적화를 달성했습니다.

// 최적화 전: 전역 상태로 인한 모든 테이블 리렌더링
const { schedules } = useScheduleContext();

// 최적화 후: TableContext 분리로 독립적인 상태 관리
export const TableProvider = ({ children, initialSchedules = [] }) => {
  const [schedules, setSchedules] = useState<Schedule[]>(initialSchedules);
  
  const updateSchedules = useCallback((newSchedules: Schedule[]) => {
    setSchedules(newSchedules);
  }, []);

  const contextValue = useMemo(
    () => ({ schedules, updateSchedules }),
    [schedules, updateSchedules]
  );

  return (
    <TableContext.Provider value={contextValue}>
      {children}
    </TableContext.Provider>
  );
};

// 각 테이블을 TableProvider로 감싸기
<TableProvider tableId={tableId} initialSchedules={schedules}>
  <ScheduleDndProvider>
    <ScheduleTable />
  </ScheduleDndProvider>
</TableProvider>

// ScheduleTable에서 독립적인 TableContext 구독
const { schedules } = useTableContext();

학습 효과 분석

  • 가장 큰 배움이 있었던 부분: React Profiler를 사용해 렌더링 병목 지점을 직접 확인하고, 최적화 기법을 적용했을 때 실제로 렌더링 횟수와 시간이 줄어드는 것을 보며 최적화의 필요성과 효과를 체감한 경험입니다. 이론으로만 알던 개념을 실제로 적용하고 검증하는 과정에서 가장 크게 성장했습니다.
  • 추가 학습이 필요한 영역: 현재 Context API로 충분하지만, 애플리케이션 규모가 더 커질 경우를 대비해 Zustand나 Recoil 같은 상태 관리 라이브러리들이 어떻게 렌더링 최적화 문제를 해결하는지 비교 분석하며 학습하고 싶습니다.
  • 실무 적용 가능성: 이번에 학습한 렌더링 최적화 기법과 Context 분리 패턴은 실무에서 복잡하고 인터랙티브한 UI를 개발할 때 발생할 수 있는 성능 문제를 해결하는 데 직접적으로 도움이 될 것이라 확신합니다.

과제 피드백

과제에서 좋았던 부분 : React DevTools를 활용해 성능 최적화를 눈으로 볼 수 있어서 좋았습니다. 드롭 시 해당 시간표만 리렌더링되는 것을 직접 확인하고, API 호출이 6번에서 2번으로 줄어드는 것을 네트워크 탭에서 확인할 수 있어서 최적화의 효과를 체감할 수 있었습니다. 또한 React Profiler로 렌더링 횟수와 시간을 직접 측정해보면서 React.memo, useMemo, useCallback 같은 메모이제이션의 효과를 실시간으로 확인할 수 있어서 이론으로만 알던 개념을 실제로 적용하고 검증하는 과정에서 깊게 이해하게 되었습니다.

리뷰 받고 싶은 내용

  • SearchDialog 메모이제이션 검증: SearchDialog.tsx에서 React.memo와 useMemo를 가장 많이 사용했는데, 혹시 과도하게 사용한 부분이 있을까요? 특히 filteredLectures useMemo의 의존성 배열이 과도하게 많은지, useCallback으로 감싼 이벤트 핸들러들이 실제로 리렌더링을 방지하는 효과가 있는지, SearchItem 컴포넌트의 memo 사용이 적절한지 검토해주실 수 있을까요? 현재 코드에서 제거해도 되는 메모이제이션이나, 오히려 추가해야 하는 부분이 있다면 조언해주세요.
  • Context 분리 패턴: 각 테이블마다 독립적인 Context를 생성하여 리렌더링을 최적화했습니다. 이 접근 방식이 효과적이었는지, 그리고 혹시 발생할 수 있는 잠재적인 문제점이나 더 나은 설계 방식이 있는지에 대한 피드백을 받고 싶습니다.
  • 커스텀 훅 구조화: useTableSchedules, useTableKeys 등 여러 커스텀 훅을 만들어 로직을 분리했습니다. 이런 방식의 추상화가 적절했는지, 혹은 애플리케이션이 더 복잡해질 경우를 대비해 더 나은 구조화 방식이 있을지 조언을 구하고 싶습니다.
  • 과제 복습 전략: 10주간의 모든 과제들을 체계적으로 복습하고 싶습니다. 각 과제별로 정리한다고 정리했지만, 혹 효과적인 복습 방법이 있을지 여쭤봅니다. 예를 들면 React 최적화, 상태 관리, 컴포넌트 설계 등 핵심 개념들을 어떻게 연결해서 학습하면 좋을지, 그리고 실무에서 바로 적용할 수 있는 지식으로 만드는 방법에 대해 조언을 구하고 싶습니다.

과제 피드백

수고했어요 지혜!! 이번 과제는 React 애플리케이션에서 실제 성능 병목 지점을 찾고 최적화하는 것이 목표였습니다.

잘했어요. API 호출의 병렬 호출 방식과 캐싱을 이용하는 방식으로 잘 해결했습니다. 이런 방식이 바로 TanStack Query가 내부적으로 QueryKey를 활용해 최적화를 제공하는 원리와 같다는 걸 기억해두세요.

SearchDialog에서 복잡한 필터링 로직을 useMemo로 메모이제이션도 잘했어요. 시간표 블록 드래그 시 Popover 컴포넌트가 계속 리렌더링되는 문제를 isLazy prop으로 해결한 것도 좋았습니다. 자주 발생되는 이벤트시 필요없는 리렌더링은 격리를 시키는건 큰 도움이 되죠.

React Profiler를 적극 활용해서 렌더링 병목 지점을 직접 확인하고 최적화 효과를 체감한 것도 좋은 경험이 되었을거라 생각해요.

성능 최적화는 어떻게 하는지 방법보다도 어디서 병목이 생기는지를 이해하고 찾는 게 더 큰 실력이라고 생각해요. 심심할 때마다 어디가 병목이 될까 등을 한 번씩 고민해보다 보면 이러한 경험이 쌓여 나중에 복잡한 성능 문제를 만났을 때 어디서부터 접근할지 감이 생기는 거라 생각해요.

너무 너무 잘했습니다.

Q) SearchDialog 메모이제이션이 과도하게 사용된 부분이 있을까요?

=> 시간 관계상 코드를 세부적으로 보질 못했어요. (오늘이 종강인데 채점을 빨리 해줘야 하잖아요? ^^;) 과도하게 느껴진다고 생각이 들면 하나씩 제거하고 분리하면서 수정해볼 수 있겠네요. 과도하게 느껴진다면 아마 책임이 너무 많은 구조로 설계되었기 때문일 수도 있어요. 설계상 결함이 없어 보인다면 과도하게라는 건 어쩔 수 없다라는 말이기도 해요. 그러니 해당 과제를 더 큰 관점에서 더 나은 설계로 한번 만들어보고 그래도 성능이 문제가 생긴다면 변화면 설계에서 한번 더 도전해보길 바래요.

Q) Context 분리 패턴에 대한 피드백

=> Context는 업데이트가 필요하면 자신의 영역을 전부 리렌더링을 호출하는 방식인 만큼 렌더링 범위에 맞게 격리시키는 접근법은 잘 했습니다. Context는 의존성보다는 Context에 포함된 모든 범위을 전부 리렌더링 시키는 만큼 규모가 커지거나 코드 변경이 많아 지는 경우 비슷한 작업을 관리해야 하는 경우가 있죠. 그래서 다른 상태관리들은 실제 사용과 접근하는 값의 의존성에 따라 업데이트를 하는 방식으로 되어 있습니다. 그래서 Zustand의 slice 패턴처럼 하나의 store에서 영역별로 분리하는 방법을 더 선호하게 되었습니다.

Q) 효과적인 복습 방법

=> 우선 코치들이 왜 이러한 커리큘럼을 만들고자 했는지 왜 이러한 과제들을 줬는지 과제의 취지를 곰곰히 이해보기를 바래요. 이걸 이론으로써 확립하기 보다는 배경을 이해하고 나면 실무에서 그 취지에 맞게 더 나은 코드나 접근을 할 수 있을 거라고 생각합니다. 그렇게 하나씩 어떤 점을 실전응용으로 했는지를 한번 회고하면서 남겨보면 훨씬 더 좋은 방법이 될거게요

=> 우리가 지난 10주간 배운것들은 결국 실무를 잘하기 위함이니 어떻게 하면 지금의 업무를 더 잘 할 수 있을까의 힌트를 지난 10주간의 배움에 찾아보고 또 그 키워드들을 실마리 삼아 AI에게 구글에게 물어가며 찾아가면서 "알면 더 나은 방식으로 만들 수 있다!" 라는 사실을 기억하며 항상 "어떻게 하면... ... 할 수 있을까?" 하는 생각을 떠올려보면서 적용해나가길 바래요.

=> 끝으로 회고의 효과를 기억하면서 블로그나 TIL이나 실무에서 적용해본 경험이나 혹은 막연하게 떠오로는 생각들을 글로 말로 정리해보는 것도 크게 도움이 될거에요.

지난 10주간 너무 너무 수고 많았어요! 이번 경험들이 앞으로 개발하는 데 있어서 나 스스로 충분히 잘하려고 노력할 수 있구나, 해냈구나 하는 마음으로 새로운 것에 도전하고 성장하는 데 있어 더 쉽게 도전할 수 있게 되는 계기가 되기를 바랍니다. 화이팅입니다!