[항해플러스] 10주차 회고
항해플러스
- 01 [항해플러스] WIL 1주차: Chapter 1-1: 프론트엔드 테스트 코드 익숙해지기
- 02 [항해플러스] WIL 2주차: Chapter1-2. AI를 활용한 안정적인 기능 개발을 위한 TDD 적용
- 03 [항해플러스] WIL 3주차
- 04 [항해플러스] WIL 4주차
- 05 유난은 때로 철학이 된다.
- 06 [항해플러스] WIL 5주차
- 07 [항해플러스] WIL 6주차
- 08 [항해플러스] WIL 7주차
- 09 [항해플러스] WIL 8주차
- 10 [항해플러스] 9주차 회고
- 11 [항해플러스] 10주차 회고
- 12 [항해플러스] 최종 회고
Farewell
개요
오늘로 10주 간의 항해가 끝이 났습니다.
나름 열심히 했다고 생각하는데, 아쉬움도 남고 그르네요,,? 하지만 미련 한 스푼 정도는 남겨두는 것도 좋을 것 같습니다.
열심히 편지 남기기
이번 주차는 정말 많은 양의 롤링페이퍼를 작성했습니다.
우리 팀 정말 열심히 잘 놀았다는 생각이 마구 들더군요..? 사진 넣은거 보는데 진짜 감탄스러웠음
멘토링
이번 주차도 역시 성호 코치님이었습니다!
이번 주차에는 정말 다양한 질문들이 오갔는데요.. 비교에 대하여, 사랑에 대하여, 그리고 역할에 대하여 였습니다. 코치님께서 굉장히 적은 연차이실 때 팀장을 맡으셨다는 말씀을 해주셨는데, 결국 그 자리가 주어지니 동기부여가 됐다는 말씀을 듣고 좀 놀랐던 것 같습니다.
아마 우리 팀도 다들 같은 마음이시지 않았을까..? 싶구요
과제
이번 주 과제는 All Pass를 했습니다!
이렇게 퍼플 뱃지를 달성하게 되었는데요. 사실 첫 주차에 조금 근자감을 얻어서 블랙뱃지 받아야지!!! 했는데, 생각보다 쉽지 않았습니다ㅎㅋㄱㄱㅋ.
목표
React 성능 최적화를 통해 렌더링 비용 줄이기
시간표 제작 서비스에서 발생하는 성능 저하를 개선하는 것이 목표였구요,, 크게 두 가지 문제가 있었습니다:
- 수업 검색 모달의 느린 페이지네이션 + 중복 API 호출
- DnD 시 모든 컴포넌트가 리렌더링되는 문제
React 렌더링 최적화가 뭔데?
React는 상태가 바뀌면 컴포넌트를 다시 그리는데요, 불필요한 렌더링을 막는 게 핵심이에요.
| 최적화 기법 | 용도 | 적용 사례 |
|---|---|---|
| useMemo | 연산 결과 캐싱 | 필터링된 강의 목록 |
| useCallback | 함수 참조 유지 | 이벤트 핸들러 |
| React.memo | 컴포넌트 리렌더링 방지 | LectureRow, |
| ScheduleTable | ||
| 클로저 캐싱 | API 응답 재사용 | fetchMajors, |
| fetchLiberalArts |
실제 적용한 최적화
1. API 호출 최적화
Before (직렬 실행 + 중복 호출)
const fetchAllLectures = async () => await Promise.all([
(console.log('API Call 1'), await fetchMajors()), // await
때문에 직렬 실행됨
(console.log('API Call 2'), await fetchLiberalArts()),
(console.log('API Call 3'), await fetchMajors()), // 같은
API 또 호출
// ... 6개나 호출
]);
After (병렬 실행 + 캐싱)
// 클로저를 활용한 캐싱
const createCachedFetch = <T,>(fetchFn: () => Promise<T>) => {
let cache: T | null = null;
let pending: Promise<T> | null = null;
return async (): Promise<T> => {
if (cache) return cache; // 캐시 있으면 바로 반환
if (pending) return pending; // 진행 중이면 같은 Promise
반환
pending = fetchFn();
cache = await pending;
return cache;
};
};
// 2개만 호출 + 병렬 실행
const fetchAllLectures = async () =>
await Promise.all([fetchMajors(), fetchLiberalArts()]);
결과: 6개 → 2개 호출, 직렬 → 병렬 실행, 약 20ms 개선
2. useMemo로 연산 최적화
Before (매 렌더링마다 필터링)
const filteredLectures = getFilteredLectures(); // 매번 3000개
필터링
After (의존성 변경 시에만 재계산)
const filteredLectures = useMemo(() => {
return lectures
.filter(lecture => ...)
.filter(lecture => ...);
}, [lectures, searchOptions]); // 이게 바뀔 때만 재계산
3. React.memo + useCallback으로 리렌더링 방지
Before
// 부모가 리렌더링되면 모든 LectureRow도 리렌더링
{visibleLectures.map(lecture => (
<tr onClick={() => addSchedule(lecture)}>...</tr>
))}
After
// memo로 props 변경 시에만 리렌더링
const LectureRow = memo(({ lecture, onAdd }) => (
<tr>...</tr>
));
// useCallback으로 함수 참조 유지
const addSchedule = useCallback((lecture) => {...}, [deps]);
결과: 30페이지 스크롤 시 600ms → 새 항목만 렌더링
4. Context 구독 최적화 (심화)
Before (전체 구독)
// schedulesMap 전체를 구독 → 하나만 바뀌어도 모두 리렌더링
const { schedulesMap, setSchedulesMap } =
useScheduleContext();
After (선택적 구독)
// 특정 시간표만 구독하는 훅
export const useSchedule = (tableId: string) => {
const { schedulesMap, setSchedulesMap } =
useScheduleContext();
const schedules = useMemo(
() => schedulesMap[tableId] || [],
[schedulesMap, tableId]
);
return { schedules, updateSchedules };
};
// 시간표 ID 목록만 구독하는 훅
export const useScheduleIds = () => {
const { schedulesMap } = useScheduleContext();
return useMemo(() => Object.keys(schedulesMap),
[schedulesMap]);
};
삽질 포인트
1. Promise.all 안에서 await 쓰면 안 됨
처음에 왜 병렬이 안 되나 했더니…
// ❌ await가 있으면 순차 실행됨
await Promise.all([
(await fetchA()), // 이게 끝나야
(await fetchB()), // 이게 시작됨
]);
// ✅ Promise 객체를 넘겨야 병렬 실행
await Promise.all([fetchA(), fetchB()]);
2. memo만 쓴다고 끝이 아님
// ❌ 이러면 매번 새 함수 참조가 생겨서 memo 무용지물
<LectureRow onAdd={(lecture) => addSchedule(lecture)} />
// ✅ useCallback으로 함수 참조 유지
const handleAdd = useCallback((lecture) => {...}, []);
<LectureRow onAdd={handleAdd} />
3. useDndContext 주의
DnD 라이브러리의 useDndContext()를 쓰면 드래그할 때마다 리렌더링이 발생해요.
필요한 곳에서만 구독하도록 TableBorderWrapper 컴포넌트로 분리했습니다.
React DevTools Profiler 활용법
React DevTools의 Profiler 탭에서:
- 녹화 버튼 누르고 → 동작 수행 → 정지
- Flamegraph에서 어떤 컴포넌트가 얼마나 걸리는지 확인
- “Why did this render?” 옵션 켜면 리렌더링 원인도 알려줌
이거 없었으면 어디가 병목인지 찾지도 못했을 듯…
배운 점
1. 측정 없이 최적화하지 마라
- React DevTools Profiler로 병목 지점 먼저 확인
- 체감 느린 것 ≠ 실제 병목 지점
2. 메모이제이션은 만능이 아니다
- 의존성 배열 관리 비용도 있음
- 정말 비용이 큰 연산에만 적용
3. Context 설계가 중요하다
- 큰 객체 하나로 관리하면 구독 최적화가 어려움
- 선택적 구독 훅을 만들어두면 편함
4. Promise.all은 await 없이 써야 병렬
- 기본적인 건데 은근 헷갈림 ![[항해플러스] 9주차 회고](https://velog.velcdn.com/images/suadesu/post/a6fab038-cc4c-4401-96c4-c566d575081d/image.jpeg)