jeongmingi123 님의 상세페이지[2팀 정민기] Chapter 4-2 코드 관점의 성능 최적화

과제 체크포인트

과제 요구사항

  • 배포 후 url 제출 https://jeongmingi123.github.io/front_6th_chapter4-2/

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

  • SearchDialog 불필요한 연산 최적화

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

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

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

과제 셀프회고

이전에 react에서 props 얕은 비교를 어떻게 하는지, 3주차에서 공부했던 부분이 정말 도움이 많이 되었던거같다. https://github.com/jeongmingi123/front_6th_chapter1-3/issues/5

최적화 방식에 여러 방식이 있지만 useMemo, useCallback, memo와 같은 함수를 이전에 어떻게 비교하는지 몰라서 프로젝트를 할 때마다 최적화를 어떻게 해야할까 고민이 많았다. 얕은 비교에 대한 개념이 많이 부족했지만, 이전에 공부했던게 도움이 많이 되었던 것 같다.

기술적 성장

Promise.all을 다시 보게 되었다..

Promise.all은 여러 프로미스를 동시에 실행하고, 모든 프로미스가 성공(fulfilled)할 때까지 기다림.

  1. 인자: 프로미스들의 배열을 받습니다. 배열의 요소가 프로미스가 아니면 Promise.resolve()로 변환됩니다.
  2. 성공: 배열의 모든 프로미스가 성공하면, 그 결과들을 원래 순서 그대로 담은 배열로 반환(resolve)합니다.
  3. 실패: 하나라도 프로미스가 실패(rejected)하면, 가장 먼저 발생한 실패 이유를 즉시 반환(reject)합니다.
  4. 병렬성: 모든 작업을 동시에 시작하기 때문에, 총 소요 시간은 가장 오래 걸리는 작업의 시간과 동일합니다.

문제점

제공된 fetchAllLectures 함수는 Promise.all을 사용했음에도 다음과 같은 문제가 있습니다.

  1. 직렬 실행 await 키워드가 배열 내부의 각 항목에 사용되고 있습니다.

이로 인해 Promise.all이 시작되기도 전에 각 API 호출이 순차적으로(직렬로) 완료될 때까지 기다리게 되어, Promise.all의 병렬 처리 장점이 사라짐

  1. 중복 API 호출 동일한 API 호출(fetchMajors, fetchLiberalArts)이 각각 3번씩 반복됨 이는 네트워크 자원을 낭비하고, 서버에 불필요한 부하를 주며, 전체 응답 시간을 지연시킴

코드 품질

리렌더링을 방지하기 위해 렌더링이 많이 되는 부분을 먼저 체크하고, 렌더링이 자주 발생하는 부분을 잘개 쪼갠거같습니다.

    <VStack spacing={4} align="stretch">
            <HStack spacing={4}>
              <SearchControls.SearchInput
                value={searchOptions.query || ""}
                onChange={handleQueryChange}
              />
              <SearchControls.CreditsSelect
                value={searchOptions.credits || ""}
                onChange={handleCreditsChange}
              />
            </HStack>

            <HStack spacing={4}>
              <SearchControls.GradeFilter
                value={searchOptions.grades}
                onChange={handleGradeChange}
              />
              <SearchControls.DayFilter
                value={searchOptions.days}
                onChange={handleDayChange}
              />
            </HStack>

            <HStack spacing={4}>
              <SearchControls.TimeFilter
                value={searchOptions.times}
                onChange={handleTimeChange}
              />
              <SearchControls.MajorFilter
                value={searchOptions.majors}
                onChange={handleMajorChange}
                allMajors={allMajors}
              />
            </HStack>

비슷한 컴포넌트들을 Modal Filter 부분을 SearchControls 하나로 묶어서 관리하여 좀더 가독성을 높인거같습니다.

학습 효과 분석

Intersection Observer에 대해 몰랐는데, 무한 스크롤중 성능 저하가 되었고, 과제 발제자료중 해당 부분을 찾아봤고, 처음 알게되어 내용을 정리했음.

Intersection Observer를 활용한 스크롤 성능 최적화

Intersection Observer는 특정 요소가 뷰포트(브라우저 화면)에 노출되었는지 여부를 비동기적으로 감지하는 JavaScript API입니다. 이 API는 스크롤 이벤트 리스너를 직접 사용하는 것보다 훨씬 효율적으로 스크롤 성능을 최적화하는 데 사용됨.

스크롤 이벤트 사용의 문제점

일반적으로 스크롤을 감지하여 특정 작업을 수행할 때 scroll 이벤트 리스너를 사용함.

  1. 빈번한 이벤트 발생: 스크롤은 매우 짧은 시간 안에 수십, 수백 번 발생할 수 있음.
  2. 잦은 계산: 이벤트가 발생할 때마다 getBoundingClientRect() 같은 메서드로 요소의 위치를 계산하면, **레이아웃 스래싱(Layout Thrashing)**이 발생하여 렌더링 성능이 저하됨.

Intersection Observer의 해결책

Intersection Observer는 스크롤 이벤트 자체를 직접 관찰하지 않음. 대신, 요소의 교차 상태 변화에 대한 콜백 함수를 한 번만 등록해 두면 시스템이 자동으로 효율적으로 처리함.

  1. 비동기 처리: 메인 스레드에서 분리되어 비동기적으로 동작하므로 렌더링 성능에 영향을 주지 않음.
  2. 콜백 호출: 콜백 함수는 요소가 뷰포트에 진입하거나 이탈할 때만 호출됨. 이 덕분에 불필요한 연산을 대폭 줄일 수 있음.

주요 활용 사례

  1. 지연 로딩 (Lazy Loading): 이미지가 화면에 보일 때만 로드하여 초기 로딩 시간을 단축함.
  2. 무한 스크롤: 페이지 하단에 도달했을 때 다음 콘텐츠를 자동으로 불러옴.
  3. 애니메이션 효과: 특정 요소가 화면에 나타날 때만 애니메이션을 실행함.

과제 피드백

공유해주신 코드 관점의 성능 최적화 사례의 부분이 많은 도움이 된거같습니다. https://www.notion.so/10-3-21b2dc3ef514805bb2d1f90028280f32?source=copy_link 최적화를 어떻게 진행해야할지, 어떤식으로 해야할지 고민이 많았는데 해당글 보면서 많은 도움이 되었던거 같습니다.

리뷰 받고 싶은 내용

안녕하세요 첫번쨰 코드와 두번째 코드 질문이있습니다. 매우 단순한 코드인데 궁금한 부분이 있어 코드 첨부드립니다.

첫번째 코드

const CreditsSelect = memo(({ value, onChange }: CreditsSelectProps) => {
  return (
    <FormControl>
      <FormLabel>학점</FormLabel>

      <Select value={value || ""} onChange={(e) => onChange(e.target.value)}>
        <option value="">전체</option>

        <option value="1">1학점</option>

        <option value="2">2학점</option>

        <option value="3">3학점</option>
      </Select>
    </FormControl>
  );
});

두번째 코드

const CreditsSelect = memo(({ value, onChange }: CreditsSelectProps) => {
  const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    onChange(e.target.value);
  };

  return (
    <FormControl>
      <FormLabel>학점</FormLabel>

      <Select value={value || ""} onChange={handleChange}>
        <option value="">전체</option>

        <option value="1">1학점</option>

        <option value="2">2학점</option>

        <option value="3">3학점</option>
      </Select>
    </FormControl>
  );
});

인라인 화살표함수랑, handle 함수로 묶었을 때 궁금한 부분이 있습니다.

첫번째 코드 인라인 함수의 경우 컴포넌트가 렌더링될 때마다 새로운 화살표 함수를 생성해서 onChange prop으로 전달함. 두번째 코드는 handleChange라는 별도의 함수를 정의하고, onChange prop에 함수 자체의 참조를 전달함. 컴포넌트가 리렌더링되어도 handleChange 함수는 다시 생성되지 않고, 동일한 함수를 참조하게 되어 memo를 했을 때, 리렌더링이 안됨.

제가 첫번째 코드와 두번째 코드를 이해한게 맞을까요.? 그리고 두번째 코드의 경우 인라인 함수로 작성을 하게되면, 렌더링될 때마다 새로운 함수 생성이 되어, 불필요하게 리렌더링이 발생하는거같습니다. 그러면 이렇게 인라인 함수를 작성하지 않고, 위에처럼 handle 함수로 만드는게 좋을까요?

과제 피드백

안녕하세요 민기님! 10주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!!

--

안녕하세요 민기님! 10주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!!

심화과제의 경우 지금 drop을 했을 때 전체적으로 렌더링이 다시 발생하고 있어요..! 이 부분만 잘 개선해주시면 좋았을 것 같네요.. ㅠㅠ 아쉽지만 불합격으로 남겨놓도록 하겠습니다..

그리고 질문에 대한 답변을 드리자면, handleChange로 함수를 정의하여도 함수가 렌더링 되면서 결국 새로운 메모리에 함수가 할당된답니다 ㅎㅎ 참조를 유지하고 싶다면 메모이제이션을 해야해요!

결국, 메모이제이션을 해주지 않으면 두 코드의 동작은 큰 차이가 없답니다..!