Skip to content

Conversation

@ahnsummer
Copy link

@ahnsummer ahnsummer commented Dec 23, 2025

과제 체크포인트

과제 배포 URL
https://ahnsummer.github.io/front_7th_chapter4-2/

과제 요구사항

  • 배포 후 url 제출

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

  • SearchDialog 불필요한 연산 최적화

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

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

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

구현 내용

1. API 호출 최적화

기존 코드의 문제점

const fetchAllLectures = async () =>
  await Promise.all([
    (console.log("API Call 1", performance.now()), await fetchMajors()),
    (console.log("API Call 2", performance.now()), await fetchLiberalArts()),
    (console.log("API Call 3", performance.now()), await fetchMajors()),
    (console.log("API Call 4", performance.now()), await fetchLiberalArts()),
    (console.log("API Call 5", performance.now()), await fetchMajors()),
    (console.log("API Call 6", performance.now()), await fetchLiberalArts()),
  ]);

문제점:

  1. 직렬 실행 문제: Promise.all 내부에 await가 있어서 병렬이 아닌 직렬로 실행됩니다
    • 각 Promise가 생성되기 전에 이전 Promise가 완료될 때까지 대기합니다
    • 결과적으로 API Call 1 → API Call 2 → ... 순서로 실행되어 병렬 처리의 이점이 없습니다
  2. 중복 호출: 동일한 API를 여러 번 호출하지만 캐싱이 없어 매번 네트워크 요청이 발생합니다
  3. 재사용 불가: 컴포넌트가 리렌더링될 때마다 API를 다시 호출합니다

개선 흐름

1단계: await 제거하여 병렬 처리

  • Promise.all 내부의 await를 제거하여 Promise 객체만 전달
  • 이렇게 하면 모든 API 호출이 동시에 시작되어 병렬로 실행됩니다
const fetchAllLectures = () =>
  Promise.all([
    fetchMajors(),
    fetchLiberalArts(),
    fetchMajors(),
    fetchLiberalArts(),
    fetchMajors(),
    fetchLiberalArts(),
  ]);

2단계: 캐시 객체를 사용하여 데이터 캐싱

  • 캐시 객체를 생성하여 동일한 API 호출 결과를 저장
  • 이미 호출한 API는 캐시에서 재사용하여 중복 호출을 방지합니다
const cache: Record<string, Promise<any>> = {};

const fetchAllLectures = () =>
  Promise.all([
    cache["majors"] ?? (cache["majors"] = fetchMajors()),
    cache["liberalArts"] ?? (cache["liberalArts"] = fetchLiberalArts()),
    cache["majors"], // 캐시에서 재사용
    cache["liberalArts"], // 캐시에서 재사용
    cache["majors"], // 캐시에서 재사용
    cache["liberalArts"], // 캐시에서 재사용
  ]);

3단계: 추상화된 훅으로 고도화

  • useFetch 훅을 구현하여 재사용 가능하고 타입 안전한 API 호출 시스템 구축
  • query 함수로 API 호출을 추상화하고, useFetches 훅으로 여러 쿼리를 병렬 처리
  • 전역 캐시 스토어를 통해 컴포넌트 간 데이터 공유

최종 구현:

const fetchMajors = query("fetchMajors", () =>
  axios.get<Lecture[]>("/schedules-majors.json")
);
const fetchLiberalArts = query("fetchLibralArts", () =>
  axios.get<Lecture[]>("/schedules-liberal-arts.json")
);

const { data: lecturesResponse, isLoading } = useFetches(
  fetchMajors,
  fetchLiberalArts,
  fetchMajors, // 캐시에서 재사용
  fetchLiberalArts, // 캐시에서 재사용
  fetchMajors, // 캐시에서 재사용
  fetchLiberalArts // 캐시에서 재사용
);

성능 개선:

  • 병렬 실행으로 API 호출 시간 단축 (직렬 → 병렬)
  • 캐싱을 통한 중복 호출 제거

2. SearchDialog 불필요한 연산 최적화

문제점

  • getFilteredLectures 함수가 매 렌더링마다 실행됩니다
  • allMajors 배열이 매번 재계산됩니다
  • 인피니티 스크롤 시 전체 필터링 결과를 매번 재계산합니다

개선 내용

  • filteredLecturesuseMemo로 메모이제이션합니다 (의존성: lectures, searchOptions)
  • allMajorsuseMemo로 메모이제이션합니다 (의존성: lectures)
  • 검색 옵션이 변경될 때만 필터링을 재실행합니다

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

문제점

  • 인피니티 스크롤 시 모든 행이 리렌더링됩니다
  • 전공 목록의 모든 체크박스가 리렌더링됩니다
  • 페이지네이션 시 전체 리스트가 다시 렌더링됩니다

개선 내용

  • VirtualScroll 컴포넌트 구현: 보이는 영역만 렌더링하는 가상 스크롤을 구현했습니다
    • 스크롤 위치에 따라 보이는 아이템만 계산합니다
    • absolutetop을 이용하여 레이아웃 시프트를 방지합니다
    • 스크롤 이벤트를 직접 감지하여 빠른 스크롤 시 빈 화면을 방지합니다
  • 최초에는 top - bottom margin을 빈 tr 요소를 만들고, 해당 요소에 height를 변경시키는 방향으로 구현하였으나 레이아웃 시프트로 인한 스크롤 오동작이 발생하여 absolute 요소의 top 속성을 변경시키는 방향으로 구현 방향 변경
  • 최초 ImpressionArea를 구현하여 observe 기반으로 구현하고자 하였으나 스크롤을 빠르게 움직이는 경우 onImpression이 동작하지 않는 문제가 있어 스크롤 기반으로 변경

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

문제점

  • 드래그 시 모든 시간표가 리렌더링됩니다
  • useDndContext를 각 테이블에서 사용하여 드래그 상태 변경 시 전체가 리렌더링됩니다

개선 내용

  • 상태 관리 분리: ScheduleTables에서만 useDndContext를 사용합니다
  • isActive prop 패턴: 드래그 중인 테이블만 isActive={true} prop을 전달합니다
  • React.memo 적용: ScheduleTable을 메모이제이션하여 schedulesisActive가 변경되지 않으면 리렌더링을 방지합니다
  • JSON.stringify 비교: 커스텀 비교 함수로 깊은 비교를 수행합니다

작동 방식:

  • 시간표 1에서 드래그 시작 → activeTableId가 "schedule-1"로 변경됩니다
  • 시간표 1만 isActive={true} prop을 받아 하이라이트를 표시합니다
  • 시간표 2는 isActive={false}로 유지되어 리렌더링되지 않습니다

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

문제점

  • schedulesMap 전체가 하나의 큰 객체로 관리되어 한 개의 스케줄만 변경되어도 모든 테이블이 리렌더링됩니다

개선 내용

  • React.memo 커스텀 비교: ScheduleTable의 비교 함수에서 schedules 배열만 깊은 비교를 수행합니다
  • 각 테이블은 자신의 schedules prop만 확인하여 변경되지 않으면 리렌더링을 방지합니다

작동 방식:

  • 시간표 1에서 스케줄 드롭 → schedulesMap["schedule-1"]만 변경됩니다
  • 시간표 1: schedules prop이 변경되어 리렌더링됩니다
  • 시간표 2: schedules prop이 동일하여 리렌더링되지 않습니다

과제 셀프회고

가상 스크롤 구현 삽질기

처음엔 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를 조합하여 사용하는 것이 더 유리할 수 있다는 것을 처음 이해하게 되었습니다.

아쉬운 점

  • JSON.stringify 비교는 성능이 걱정되기도 하고 좀... 짜치는 것 같은데 deepEqual같은 함수를 직접 구현하여 사용하는 게 이득일까?
  • VirtualScroll 스크롤 이벤트에 throttle 안 걸었는데, 필요할까?

리뷰 받고 싶은 내용

1. React.memo의 JSON.stringify 비교 함수

ScheduleTable.tsx에서 memo 비교 시 JSON.stringify를 사용했는데, 성능상 문제가 될 수 있을까요? schedules 배열을 직접 순회하며 비교하는 게 나을까요? 아니면 이 정도는 오버 엔지니어링인가요?

React.memo(ScheduleTable, (prev, next) => {
  return (
    JSON.stringify(prev.schedules) === JSON.stringify(next.schedules) &&
    prev.isActive === next.isActive
  );
});

2. VirtualScroll 스크롤 이벤트 최적화

현재 스크롤 이벤트를 throttle/debounce 없이 바로 처리하는데, 실제로 성능 문제가 될까요? 추가해야 한다면 어느 정도 delay가 적절할까요?

3. useFetch 훅 구조

useFetch를 구현하면서 캐싱을 전역 객체로 관리하고 createStore, useStore와 같은 유틸리티를 만들어서 전역 상태와 같이 사용하고 있습니다. 이렇게 할 경우 ContextAPI를 사용하지 않아도 되어서 사용하는 입장에서 좀 더 효율적일 것 같은데, 실제 라이브러리에서는 왜 이런 모델을 사용하지 않고 Provider를 주입하는 패턴을 더 많이 유지할까요?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant