angielxx 님의 상세페이지[1팀 이은지] Chapter 4-2 코드 관점의 성능 최적화

과제 체크포인트

배포 링크

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

과제 요구사항

  • 배포 후 url 제출

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

  • SearchDialog 불필요한 연산 최적화

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

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

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

과제 셀프회고

이번 과제는 마지막 주차의 과제라 그런지 과제 진행이 수월하여 빨리 끝낼 수 있었습니다. 그동안 준일 코치님께서 만드셨던 과제를 생각하면 이번 과제는 일부러 마지막이니까 좀 쉬라고(?) 하는 느낌이었습니다. 감사합니다..덕분에 내일 해외여행을 가는데 멀쩡한 컨디션으로 여행을 시작할 수 있을 것 같아요! 수료식에 참여하지 못하는 건 너무 슬프지만요...

이번 과제를 하며 배운 것/꺠달은 것

  1. 최적화를 제대로 하고 있지 않았다.
  2. profiler로 성능 측정하고 렌더링 분석하기
  3. 렌더링 격리

최적화를 제대로 하고 있지 않았다.

심화과제를 진행하면서 드래그앤드롭 시 테이블 자체가 재렌더링되는 문제를 겪었습니다. 드래그 후 드랍하는 순간 테이블이 재렌더링 되는 이유를 profiler의 'How it render?'값을 보고 원인을 파악했습니다. 재렌더링을 발생시키던 원인은 메모이제이션 누락된 prop 한 개 때문이었습니다. 이 현상을 보고 그저 컴포넌트 내부의 함수나 변수를 메모이제이션 하는 걸로는 성능 최적화라는 작업이 완전하진 않겠다고 깨달았습니다.

문제의 메모이제이션 누락된 props onDeleteButtonClick

// src/components/ScheduleTable/ScheduleList.tsx
return (
    <>
 {schedules.map((schedule, index) => (
        <DraggableSchedule
        key={`${tableId}-${index}`} // 안정된 key
        id={`${tableId}:${index}`}
        data={schedule}
        bg={getColor(schedule.lecture.id)}
        onDeleteButtonClick={() => {
            onDeleteButtonClick?.({ day: schedule.day, time: schedule.range[0] });
        }}
        />
    ))}
</>
);

onDeleteButtonClick을 useCallback으로 메모이제이션

// src/components/ScheduleTable/ScheduleList.tsx
const handleDeleteButtonClick = useCallback(
    (day: string, time: number) => () => {
    onDeleteButtonClick?.({ day, time });
    },
    [onDeleteButtonClick]
);

return (
    <>
    {schedules.map((schedule, index) => (
        <DraggableSchedule
        key={`${tableId}-${index}`} // 안정된 key
        id={`${tableId}:${index}`}
        data={schedule}
        bg={getColor(schedule.lecture.id)}
        onDeleteButtonClick={handleDeleteButtonClick(schedule.day, schedule.range[0])}
        />
    ))}
    </>
);

profiler로 성능 측정하고 렌더링 분석하기

이번 과제를 진행하면서 성능 최적화에 대한 인식이 완전히 바뀌었습니다. 단순히 "useMemo 쓰면 빨라진다"는 수준에서 벗어나 체계적으로 문제를 진단하고 해결하는 과정을 경험할 수 있었습니다.

위에서 말한 것처럼 테이블이 재렌더링 되는 이슈를 해결하기 위해 재렌더링의 원인이 무엇인지를 알 수 있는 방법이 없을까 찾아봤고, profiler에서 'Why did this render?'를 확인하면 렌더링 원인을 확인할 수 있다는 걸 알았습니다.

뿐만 아니라 과제의 목표대로 수행하기 위해 과제 내용에 첨부되어 있는 profiler 스크린샷과 제 화면을 비교하기도 했습니다. 처음에는 profile의 FlameGraph를 볼 줄 몰라 디버깅에 어려움이 있었습니다. profiler의 기능을 살펴보고 어떻게 활용하여 성능을 분석하고 활용할 수 있는지 배웠습니다. 사실 profiler를 알고는 있었지만 사용해보지 않은 기능이라 여태껏 공부를 미뤄왔던 부분인데, 이번 기회에 알게 되어 앞으로 실무에서도 잘 활용할 수 있을 것 같아요!

렌더링 격리

이번 과제에서 가장 크게 체감한 건 “리액트에서 성능 최적화 = 불필요한 렌더링의 전파를 끊는 작업”이라는 점이었어요. 이를 위해 컴포넌트를 분리하고, 분리된 경계마다 props를 안정화하여 렌더링을 격리하는 패턴을 집중적으로 연습했습니다.

역할 기준 분리:

  • ScheduleTable를 *정적 그리드(틀)*와 *동적 아이템(드래그되는 블록)*으로 나눴습니다.

  • TableGrid: 셀/라인처럼 상태가 바뀌지 않는 정적 레이아웃만 담당(순수 컴포넌트).

  • ScheduleList: 실제로 변하는 schedules만 구독하여 드래그/삭제 등 상호작용 처리.

렌더링 단위 축소:

  • 아이템 단위 컴포넌트(DraggableSchedule)를 최하위로 내리고 React.memo로 감쌌습니다. 이렇게 하면 schedules 배열의 한 요소가 바뀌어도 그 요소에 해당하는 컴포넌트만 다시 렌더링됩니다.

props 안정화(“격리의 절반은 안정화”):

  • 콜백은 useCallback, 파생값은 useMemo로 고정했습니다.
  • 인라인 객체/배열/함수 생성 금지(= 참조가 매번 바뀌지 않게) → 부모의 재렌더가 자식까지 “도미노”로 번지지 않음.

Profiler의 Flamegraph에서 상위 트리의 흔들림이 크게 줄었고, 드래그/드롭 시 바뀌는 아이템만 다시 그려지는 걸 확인했습니다. 특히 “Why did this render?”에서 props changed 원인이 거의 사라지며, commit 횟수/렌더 시간도 눈에 띄게 감소했습니다.

최종 회고

10주간의 항해 과정을 통해 기술적인 성장 뿐만아니라 스스로 될 때까지 부딪히는 끈기를 기를 수 있었습니다! 10주동안 고생하신 동기 여러분, 그리고 코치님들 너무 감사드려요. 다들 최고 <3 수고하셨습니다!!!

과제 피드백

안녕하세요 은지님..! 마지막 과제 잘 진행해주셨네요! 다만 배포된 기본과제에서 SearchDialog 가 정상적으로 동작하지 않고 있어요.. ㅠㅠ 수업을 검색해도 나오질 않는달까... 그런 상황이네요. majors 는 아예 노출이 안 되고 있답니다....!

나중에 원인 파악해서 수정해보시면 좋겠어요! 심화과제는 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!!


원인을 보니까 json을 가져올 때 BASE_URL 이 제대로 적용되지 않고 있네요.. ㅎㅎ 아마 로컬에서는 정상동작 했을텐데 배포하고 나니까 정상동작이 안 되는 그런 문제로 보여집니다.