과제 체크포인트
https://ckdwns9121.github.io/front_6th_chapter4-2/
과제 요구사항
-
배포 후 url 제출
-
API 호출 최적화(
Promise.all이해) -
SearchDialog 불필요한 연산 최적화
-
SearchDialog 불필요한 리렌더링 최적화
-
시간표 블록 드래그시 렌더링 최적화
-
시간표 블록 드롭시 렌더링 최적화
과제 셀프 회고
기술적 성장
이번 프로젝트를 통해 성능 최적화 기법을 실제 코드에 적용해보고 Profiler를 활용해서 실제로 불필요한 렌더링이 일어나는지 확인해볼 수 있는 재미있는 과제였습니다. 특히 좋았던건 Profiler를 활용한 문제 진단과 Context 구조 개선, 그리고 렌더링 격리를 통한 Drag & Drop 최적화가 가장 큰 성장 포인트라고 생각합니다.
React Profiler 학습과 활용
이전까지는 성능 문제가 발생하면 막연히 memo나 useCallback을 적용하는 방식으로 대응했습니다. 하지만 지금 생각해보니 정확히 사용하지 않았던거 같아요. 그냥 무분별하게 컴포넌트에 memo를 적용하고 useCallback으로 감싸서 사용했지만 실제로 리렌더링이 일어나는지 아닌지는 명확히 체크하지 않고 사용했었던거 같습니다.
하지만 Profiler를 활용해 드래그 앤 드롭 시 어떤 컴포넌트가, 왜 리렌더링되는지를 시각적으로 확인하면서 정확한 원인을 분석할 수 있었습니다. 이 과정에서 DndProvider가 불필요하게 전역 상태에 의존하고 있다는 근본적 문제를 발견했고, 이를 개선해 불필요한 리렌더링을 획기적으로 줄일 수 있었습니다. 앞으로 어떤 프로젝트에서도 성능 문제를 진단하고 해결해서 최적화할 수 있는 자신감을 얻게 되었습니다.
Context는 역시 의존성 주입 도구이다.
처음 구조는 Context를 전역 상태 저장소처럼 사용하고 있었습니다. schedulesMap과 setSchedulesMap을 같은 Context에 넣으면서 상태가 변경될 때마다 모든 구독 컴포넌트가 리렌더링되는 문제를 겪었습니다. 이를 해결하기 위해 상태 컨텍스트와 액션 컨텍스트를 분리했고, 이 과정에서 Context는 전역 상태 보관소라기보다 **의존성 주입(Dependency Injection)과 레이어 격리(Layer Isolation)**에 적합한 도구라는 점을 다시 한번 깨달았습니다. 상태를 읽는 컴포넌트와 상태를 변경하는 컴포넌트를 분리함으로써 불필요한 리렌더링을 막고 구조적 명확성을 확보할 수 있었습니다.
불필요한 리렌더링 개선
특히 이번 심화과제인 Drag & Drop 기능에서도 큰 공부가 되었습니다. 기존에는 한 테이블에서 드래그 이벤트가 발생하면 모든 테이블이 리렌더링되었는데, 이는 DndProvider가 전역 상태에 직접 의존했기 때문입니다. 이 문제를 어떻게 개선하지? 고민하게 되었고 첫 상태를 ScheduleProvider로 주입하고 이후 상태는 테이블별로 격리시켜서 해결하면 될 것 같다는 아이디어를 떠올렸습니다. 이를onSchedulesChange 콜백을 통해 해당 테이블의 스케줄만 업데이트하는 구조로 변경하면서, 상태 격리와 렌더링 범위 최소화를 동시에 만족할 수 있었습니다.
memo의 커스텀 compare 함수
ScheduleCard 컴포넌트에 memo를 적용할 때, 단순히 props의 얕은 비교에 의존하지 않고 커스텀 비교 함수를 직접 구현했습니다. 이를 통해 useAutoCallback으로 인해 함수 참조가 항상 동일하게 유지되는 openSearch 함수와 같이, 컴포넌트의 렌더링에 직접적인 영향을 주지 않는 props의 변경은 무시하도록 설정했습니다. 이 기법은 특히 부모 컴포넌트가 자주 리렌더링될 때 하위 컴포넌트의 렌더링을 효율적으로 제어하는 데 매우 유용하다는 것을 깨달았습니다.
const ScheduleCard = memo(
({ index, tableId, schedules, disabledRemoveButton, openSearch, isActive }: ScheduleCardProps) => {
// 컴포넌트 로직
},
(prev, next) =>
prev.tableId === next.tableId &&
prev.isActive === next.isActive &&
prev.disabledRemoveButton === next.disabledRemoveButton &&
prev.openSearch === next.openSearch &&
prev.schedules === next.schedules
);
코드 품질
부분 구독 패턴(Partial Subscription Pattern) 적용
Context를 상태와 액션으로 분리해 각 역할이 독립적으로 작동하도록 만들었습니다. 이를 통해 불필요한 리렌더링이 방지되었을 뿐 아니라, 코드의 가독성과 유지보수성도 크게 향상되었습니다.
// ScheduleContext.tsx
const ScheduleStateContext = createContext<ScheduleStateContextType | undefined>(undefined);
const ScheduleActionsContext = createContext<ScheduleActionsContextType | undefined>(undefined);
export const useScheduleState = () => useContext(ScheduleStateContext); // 상태 전용
export const useScheduleActions = () => useContext(ScheduleActionsContext); // 액션 전용
DnD 격리를 통한 부분 구독
Drag & Drop 상태를 테이블 단위로 격리하여, 한 테이블의 스케줄 변경이 다른 테이블에 영향을 주지 않도록 했습니다.
// ScheduleDndProvider.tsx
interface ScheduleDndProviderProps {
tableId: string;
schedules: Schedule[];
onSchedulesChange: (schedules: Schedule[]) => void;
}
<ScheduleDndProvider tableId={tableId} schedules={schedules} onSchedulesChange={handleSchedulesChange}>
<ScheduleTable />
</ScheduleDndProvider>
DnD성능 최적화 비교
[성능 최적화 시나리오]
- 시간표2의 스케줄을 DnD로 변경한 뒤 Profiler로 측정
| 최적화 전 | 최적화 후 |
23프레임때가 정확히 Drop이 일어난 시점인데 이때 다른 테이블이 모두 리렌더링이 되는 반면 최적화 후에는 DnD가 이루어진 테이블만 리렌더링이 되고 있음을 확인했습니다.
ScheduleTable에서 렌더링 격리
준일코치님이 QnA시간에 이렇게 렌더링을 격리하는 방식을 보여주셔서 바로 적용해봤습니다.
문제상황
드래그 중에 schedules 배열이 변경되면, 정적인 Grid까지 포함된 전체 ScheduleTable이 리렌더링되는 문제가 발생했습니다.
해결 방법
ScheduleTableGrid를 별도 컴포넌트로 분리하고 memo로 감싸 정적 부분과 동적 부분을 분리했습니다.
const ScheduleTable = memo(({ tableId, schedules, onScheduleTimeClick }: Props) => {
return (
<Box>
<ScheduleTableGrid onScheduleTimeClick={onScheduleTimeClick} /> {/* 정적 */}
{schedules.map(schedule => <DraggableSchedule key={...} {...schedule} />)} {/* 동적 */}
</Box>
);
});
const ScheduleTableGrid = memo(({ onScheduleTimeClick }) => {
return <Grid>{/* 정적 셀 */}</Grid>;
});
이렇게 렌더링을 격리시켰을 때의 장점
- 드래그 중에는 Grid는 고정, 스케줄만 리렌더링
- 많은 셀이 매번 다시 그려지지 않아 성능 향상
- 정적/동적 UI 관심사 분리로 가독성과 유지보수성 개선
- 사용자 입장에서는 부드럽고 안정적인 드래그 경험 제공
4. 관심사 분리
거대한 SearchDialog를 여러 작은 컴포넌트로 분리하여 단일 책임 원칙을 적용했습니다. 각 필터의 상태 변경 시 해당 컴포넌트만 리렌더링되도록 하여 성능을 개선했습니다.
const SearchDialog = memo(({ searchInfo, onClose }: Props) => {
const [searchOptions, setSearchOptions] = useState<SearchOption>(searchInfo.options);
const changeSearchOption = useCallback((key: keyof SearchOption, value: any) => {
setSearchOptions(prev => ({ ...prev, [key]: value }));
}, []);
return (
<VStack spacing={4} align="stretch">
<SearchInputFilter query={searchOptions.query} onChange={changeSearchOption} />
<HStack spacing={4}>
<GradeFilter grades={searchOptions.grades} onChange={changeSearchOption} />
<DayFilter days={searchOptions.days} onChange={changeSearchOption} />
</HStack>
<MajorFilter majors={searchOptions.majors} onChange={changeSearchOption} />
</VStack>
);
});
// SearchInputFilter.tsx
const SearchInputFilter = memo(({ query, onChange }: SearchInputFilterProps) => {
return (
<FormControl>
<FormLabel>검색어</FormLabel>
<Input value={query} onChange={(e) => onChange('query', e.target.value)} />
</FormControl>
);
});
// GradeFilter.tsx
const GradeFilter = memo(({ grades, onChange }: GradeFilterProps) => {
return (
<FormControl>
<FormLabel>학년</FormLabel>
<Select value={grades} onChange={(e) => onChange('grades', e.target.value)} />
</FormControl>
);
});
최종 성능 최적화 결과
드래그앤 드롭으로 스케줄 변경시 최적화 드래그앤 드롭 시 불필요한 리렌더링을 개선했습니다.
https://github.com/user-attachments/assets/002a0c3d-c5c2-40dd-b7a4-62e6d14a93fd
시간표 복제 및 삭제 시간표 복제, 시간표 삭제 시 불필요한 리렌더링을 개선했습니다.
https://github.com/user-attachments/assets/82f64d24-4b5e-4781-b660-633727c1fe10
시간표 추가 시간표 추가시 추가되는 테이블만 렌더링이 일어나도록 개선했습니다.
https://github.com/user-attachments/assets/16efa23b-6600-40ea-88b1-f2eebc521cd6
시간표 모달 시간표 모달에서 상태가 변경될 때 불필요한 렌더링을 개선했습니다.
https://github.com/user-attachments/assets/6ee0e823-d96c-4633-9f57-2f687d0123d1
학습 효과 분석
성능최적화는 프로세스이다..!
성능 최적화는 단순히 감각으로 하는게 아니라 프로세스라는 점을 배웠습니다.
측정(Profiler) → 분석(원인 파악) → 구현(최적화) → 검증(효과 확인)
이라는 단계적 과정을 거쳐 문제를 해결하는 방식을 토대로 과제를 하면서, 무분별하게 메모이제이션을 적용했었던 지난날을 반성하게 되었고.. 앞으로는 위 프로세스에 맞춰서 성능 최적화를 진행해야겠다 생각했습니다.
추가 학습이 필요한 영역
- 사용자 경험 중심의 성능 지표 (Lighthouse, Web Vitals)
- 대용량 데이터를 처리하는 가상화(윈도우징)를 직접 구현해보기
이 부분을 앞으로 더 깊이 학습하면 좋을거같습니다. 그리고 가상화 같은 경우는 직접 구현해보고 싶었는데 시간이 부족한 관계로 구현하지 못했습니다. 시간표 모달 내부에서 약 25000개 정도의 전체 스케줄이 있는데 이 부분에 가상화를 적용하면 좋을것 같습니다.
실무 적용 가능성
이번에 배운 Profiler 기반 성능 진단, Context 기반 아키텍처 개선, 컴포넌트 렌더링 격리 기법은 실제 대규모 React 애플리케이션에서도 즉시 적용 가능한 실전 역량이라고 생각합니다.
과제 피드백
React Profiler를 활용해서 실제로 어느 부분에서 불필요한 렌더링이 일어나는지 확인할 수 있었고 단순 기능을 구현하는 것을 넘어 실제 프로덕션 환경에서 문제가 될 수 있을 성능 문제 해결 과정을 직접 경험할 수 있어서 좋았습니다.
리뷰 받고 싶은 내용
Q1. 이번 과제를 진행하면서 useCallback이나 useMemo로 메모이제이션을 적용했음에도 불구하고, 예상치 못하게 새로운 참조(reference)가 생성되어 컴포넌트가 리렌더링되는 경우가 종종 있었습니다. 실무에서는 이러한 참조 문제를 예방하거나 관리하기 위해 어떤 방식으로 접근하시는지 궁금합니다.!
Q2. 이번 과제를 통해 React Profiler를 활용해서 성능측정을 하는 경험을했습니다. 실무에서는 Profiler뿐만 아니라 웹 전체 성능(Web Vitals, Lighthouse)을 고려해야 하는 경우가 많은데요, 이런 웹 성능 최적화를 학습하거나 실습하는 방법에 대해 추천해주실 수 있을까요?
Q3. 주니어 입장에서 성능 최적화 경험이라고 하면, 주로 불필요한 리렌더링 방지, LCP 개선, 번들 사이즈 최적화 정도가 떠오르고 실제로도 이런 경험밖에 해보지 못했습니다. 혹시 시니어 레벨에서는 웹 성능 최적화를 어떤 범위까지 고려하며, 실제 프로젝트에서 주로 어떤 전략이나 기법을 적용하는지 궁금합니다.
과제 피드백
창준님 고생하셨습니다. 10주간 정말 열심히 해주셨던것 기억 잘 나는데요. 특히 잘 정리해주신 회고들이 참 기억에 많이 남는것 같아요. 마지막까지 과제 잘 정리해주셨고, 말씀해주신것처럼 성능 최적화는 감각이 아니라 명확하게 기준을 잡고 문제를 개선하는 흐름을 가져야한다고 생각해요. 이 부분 정말 잘 이해해주신것 같아서 좋습니다 :+1
Q1. 이번 과제를 진행하면서 useCallback이나 useMemo로 메모이제이션을 적용했음에도 불구하고, 예상치 못하게 새로운 참조(reference)가 생성되어 컴포넌트가 리렌더링되는 경우가 종종 있었습니다. 실무에서는 이러한 참조 문제를 예방하거나 관리하기 위해 어떤 방식으로 접근하시는지 궁금합니다.!
결국 말씀해주신것처럼 성능 이슈가 발생하는 명확한 범위가 지정이 되면 그 범위를 해결하는 과정에서 발생하는 이슈라고 생각이 되요. 고로 정말 뜬금없는 위치에 있어서 예상이 안되는 곳에 문제가 발생하는 경우는 거의 없을거기 때문에 기존 성능 최적화를 거치는 과정처럼 해당 범위에 있는 자식컴포넌트들에서 새로운 참조들이 발생하는게 없나 계속 모니터링을 하고 수정한다음, 이슈가 없다면 수정하는 형태가 되지 않을까! 싶네요.
Q2. 이번 과제를 통해 React Profiler를 활용해서 성능측정을 하는 경험을했습니다. 실무에서는 Profiler뿐만 아니라 웹 전체 성능(Web Vitals, Lighthouse)을 고려해야 하는 경우가 많은데요, 이런 웹 성능 최적화를 학습하거나 실습하는 방법에 대해 추천해주실 수 있을까요?
아마 과제를 만들면서도 고민을 많이 하셨을 것 같은데, 뭔가 이런것들을 실습하는것들을 위한 적절한 과제를 만들기가 어려운 것 같아요.
성능 최적화라는건 늘 예방을 위해 하는 작업이 아니라 문제가 발생했을 때 해야하는 세부적인 작업이라고 생각해요. 그리고 늘 이런 작업들이 하나하나 개별로 봤을때는 엄청난 작업은 아니지만 티끌모아태산이 되는 작업이 성능 최적화라고 생각하는데요. 일단은 관련 도구들의 목적과 지표들에 대해 가이드를 통해 명확하게 학습을 하는것으로 하고, 사내에서 선제적으로 창준님이 이슈가 발생할 것 같은 부분들을 특정해보고 배운것들을 적용해보는게 제일 좋은 학습이지 않을까!
Q3. 주니어 입장에서 성능 최적화 경험이라고 하면, 주로 불필요한 리렌더링 방지, LCP 개선, 번들 사이즈 최적화 정도가 떠오르고 실제로도 이런 경험밖에 해보지 못했습니다. 혹시 시니어 레벨에서는 웹 성능 최적화를 어떤 범위까지 고려하며, 실제 프로젝트에서 주로 어떤 전략이나 기법을 적용하는지 궁금합니다.
시니어라고 더 특별한 것이 있겠냐 싶지만, 좀 더 깊고 넓은 범위를 바라보지 않을까 싶긴해요. 깊다면 엔진단의 최적화까지 고려를 할 수 있을 것 같고 추가로 단순히 FE관점의 문제로 국한하고 해결하기보다는 기획적인 관점, BE관점, 인프라관점에서 문제를 바라보고 더 해결할 수 있는 나은 방법이 있나 계속 고민하게 되는것들도 있을것 같아요.
고생하셨고, 앞으로 개발인생 화이팅 하세요! 그럼 내일봬요~