Skip to content

Conversation

@daehyunk1m
Copy link

@daehyunk1m daehyunk1m commented Dec 22, 2025

과제 체크포인트

과제 배포 링크

과제 요구사항

  • 배포 후 url 제출
  • API 호출 최적화(Promise.all 이해)
  • SearchDialog 불필요한 연산 최적화
  • SearchDialog 불필요한 리렌더링 최적화
  • 시간표 블록 드래그시 렌더링 최적화
  • 시간표 블록 드롭시 렌더링 최적화

과제 셀프회고

기술적 성장

새로 학습한 개념

1. CQRS 패턴을 활용한 Context 최적화

기존에는 하나의 Context에 상태와 업데이트 함수를 함께 제공했는데, 이 방식은 상태가 변경될 때마다 업데이트 함수만 필요한 컴포넌트도 리렌더링되는 문제가 있었습니다. CQRS(Command Query Responsibility Segregation) 패턴을 적용하여 Command Context(상태 업데이트)와 Query Context(상태 조회)를 분리했습니다.

// ScheduleContext.tsx - CQRS 패턴 적용
const ScheduleCommandContext = createContext<SetSchedulesMap | undefined>(undefined);
const ScheduleQueryContext = createContext<SchedulesMap | undefined>(undefined);

export const useScheduleCommand = () => useContext(ScheduleCommandContext);
export const useScheduleQuery = () => useContext(ScheduleQueryContext);

이를 통해 SearchDialogScheduleDndProvideruseScheduleCommand만 구독하여 상태 변경 시 리렌더링되지 않고, 실제로 상태를 표시하는 ScheduleTables만 리렌더링되도록 최적화했습니다.

2. 클로저를 활용한 API 캐싱 패턴

클로저가 외부 변수를 "기억"하는 특성을 활용하여 API 응답을 캐싱하는 고차 함수를 구현했습니다.

// utils.ts - 클로저 캐싱
export const createCachedFetcher = <T>(fetcher: () => Promise<AxiosResponse<T>>) => {
  let cache: AxiosResponse<T> | null = null; // 클로저로 캐시 유지

  return async () => {
    if (cache !== null) return cache; // 캐시 히트
    const data = await fetcher();
    cache = data;
    return data;
  };
};

// 사용
const cachedFetchMajors = createCachedFetcher(fetchMajors);
const cachedFetchLiberalArts = createCachedFetcher(fetchLiberalArts);

3. useDndContext 격리를 통한 드래그 성능 최적화

@dnd-kituseDndContext는 드래그 중 매 프레임마다 상태가 변경되어 구독하는 모든 컴포넌트가 리렌더링됩니다. 이를 해결하기 위해 Context를 사용하는 부분만 별도 컴포넌트로 분리했습니다.

// ScheduleTable.tsx - TableOutline 분리
const TableOutline = ({ tableId, children }: PropsWithChildren<{ tableId: string }>) => {
  const dndContext = useDndContext(); // 이 컴포넌트만 드래그 중 리렌더링
  const activeTableId = getActiveTableId(dndContext);

  return (
    <Box outline={activeTableId === tableId ? "5px dashed" : undefined}>
      {children} {/* children은 리렌더링되지 않음 */}
    </Box>
  );
};

기존 지식의 재발견/심화

useMemo/useCallback 선택적 적용의 중요성

처음에는 "모든 함수에 useCallback, 모든 계산에 useMemo를 적용하면 되겠지"라고 생각했지만, 실제로는 메모이제이션 자체에도 비용(의존성 비교, 메모리 사용)이 발생합니다. 이 과제를 통해 다음 기준으로 선택적 적용의 중요성을 체감했습니다:

  • useMemo 적용 기준: 5단계 필터링 체인처럼 계산 비용이 높거나, 결과가 자식 컴포넌트의 props로 전달되는 경우
  • useCallback 적용 기준: memo된 자식 컴포넌트에 전달되는 콜백이거나, 의존성 배열에 포함되는 함수
// SearchDialog.tsx - 5단계 필터링은 useMemo 적용 가치 있음
const filteredLectures = useMemo(() => {
  return lectures
    .filter(/* 검색어 */)
    .filter(/* 학년 */)
    .filter(/* 전공 */)
    .filter(/* 학점 */)
    .filter(/* 요일/시간 */);
}, [searchOptions, lectures]);

React.memo 커스텀 비교 함수

기본 React.memo는 props를 얕은 비교를 통해 다름을 판단하는데, 객체 props의 경우 매번 새 참조가 생성되어 메모이제이션이 무력화될 수 있습니다. 커스텀 비교 함수로 실제로 비교가 필요한 값만 확인하도록 최적화했습니다.

// LectureItem.tsx - ID만 비교하는 커스텀 비교 함수
export const LectureItem = React.memo(
  ({ lecture, onClick }) => {
    /* ... */
  },
  (prev, next) => prev.lecture.id === next.lecture.id // 같은 강의면 리렌더링 스킵
);

구현 과정에서의 기술적 도전과 해결

문제: 드래그 중 시간표 전체가 리렌더링되어 버벅임 발생

원인 분석: React DevTools Profiler로 확인한 결과, useDndContext를 사용하는 ScheduleTable 컴포넌트가 드래그 중 60fps로 리렌더링되고 있었습니다. useDndContext는 드래그 위치, 충돌 감지 등의 정보를 실시간으로 업데이트하기 때문입니다.

해결 과정:

  1. ScheduleTable에서 useDndContext를 사용하는 부분(드래그 중 테두리 표시)을 TableOutline 컴포넌트로 분리
  2. ScheduleTableDraggableScheduleReact.memo 적용
  3. 결과적으로 드래그 중에는 TableOutline만 리렌더링되고, 실제 시간표 데이터는 드롭 시에만 업데이트

코드 품질

특히 만족스러운 구현

1. ScheduleContext의 CQRS 패턴

단순히 "리렌더링을 줄인다"는 목표를 넘어서, 관심사 분리라는 설계 원칙을 적용했습니다. 상태를 "읽기"와 "쓰기"로 분리함으로써:

  • 컴포넌트가 자신에게 필요한 Context만 구독
  • 불필요한 의존성 제거로 테스트 용이성 향상
  • 향후 상태 관리 로직 변경 시 영향 범위 최소화

2. createCachedFetcher의 재사용성

제네릭 타입을 활용하여 어떤 API fetcher에도 적용 가능한 범용 캐싱 유틸리티를 구현했습니다. 기존 API 호출 코드를 수정하지 않고도 캐싱을 적용할 수 있어 관심사 분리가 잘 되었습니다.

// 타입 안정성을 유지하면서 어떤 fetcher에도 적용 가능
const cachedFetch = createCachedFetcher<Lecture[]>(fetchLectures);

코드 설계 관련 고민과 결정

메모이제이션 적용 범위 결정

모든 컴포넌트에 React.memo를 적용하는 것은 오히려 성능에 해로울 수 있다고 생각했고 다음 기준으로 선택적 적용을 결정했습니다:

컴포넌트 memo 적용 이유
LectureItem O 리스트 아이템, 수백 개 렌더링 가능
MajorItem O 체크박스 리스트, 개별 변경 빈번
DraggableSchedule O 드래그 중 리렌더링 방지 필요
ScheduleTable O 드래그 중 리렌더링 방지 필요
SearchDialog X 최상위 컴포넌트, 부모 리렌더링 드뭄

하지만 Q&A 시간때 오히려 "가끔" 메모이제이션을 적용하는 것보다 오히려 "모두" 메모이제이션을 적용하는 것이 좋을 수 있다고 하여 이후엔 모두 메모이제이션하는 방향을 고려해볼 예정입니다.

참조 - https://yceffort.kr/2022/04/memo-for-referential-stability-in-react


학습 효과 분석

가장 큰 배움이 있었던 부분

Context 리렌더링 메커니즘의 이해

이론적으로 "Context 값이 변경되면 구독 컴포넌트가 리렌더링된다"는 것은 알고 있었지만, 실제로 어떤 상황에서 문제가 되는지 체감하지 못했습니다. 이 과제에서:

  1. React DevTools Profiler로 드래그 중 리렌더링 횟수 측정
  2. useDndContext가 매 프레임 변경됨을 확인
  3. Context 분리/컴포넌트 분리로 해결
  4. 최적화 전후 렌더링 횟수 비교

이 과정을 통해 "왜 Context 최적화가 필요한지"를 직접 경험하고, 해결 패턴을 체득했습니다.

Promise.all의 동작 원리 재확인

Promise.all은 배열 내 모든 Promise가 resolve될 때까지 기다립니다. 하지만 단순히 Promise.all로 감싼다고 병렬 실행이 되는 것은 아닙니다:

// 이건 병렬 실행이 아님! (이미 실행된 Promise를 기다리는 것)
const result1 = await fetch("/api/1");
const result2 = await fetch("/api/2");
await Promise.all([result1, result2]);

// 이게 진짜 병렬 실행
const [result1, result2] = await Promise.all([fetch("/api/1"), fetch("/api/2")]);

추가 학습이 필요한 영역

  • React Profiler 심화: Ranked 차트 분석, 렌더링 시간 측정
  • Bundle 최적화: Code splitting, Tree shaking, Dynamic import
  • Web Vitals: LCP, FID, CLS 측정 및 개선

과제 피드백

시간표 애플리케이션이라는 구체적인 맥락이 있어서 "왜 이 최적화가 필요한지" 체감할 수 있었습니다. 드래그 중 버벅임은 사용자 경험에 직접 영향을 주기 때문에 최적화의 필요성을 절실히 느꼈습니다.


리뷰 받고 싶은 내용

Q&A에서 "선택적 메모이제이션보다 전체 적용이 나을 수 있다"는 내용이 인상깊었습니다. 현재는 리렌더링 비용이 큰 컴포넌트만 메모이제이션을 적용했는데 React 19의 React Compiler가 도입되면 수동 메모이제이션이 불필요해질 것으로 예상되는데, 현 시점에서도 전체 적용이 더 좋은 전략일까요?

daehyunk1m and others added 14 commits December 22, 2025 23:46
- gh-pages 패키지 추가
- Claude Code 학습 환경 설정 파일 추가
- MCP 설정 및 문서 파일 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Promise.all 내부 await 제거로 진짜 병렬 실행 구현
- createCachedFetcher 클로저 함수로 API 캐싱 구현
- 중복 API 호출 제거 (6회 → 2회)
- 모달 재오픈 시 API 재호출 방지

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- useMemo를 활용한 filteredLectures 메모이제이션 구현
- 의존성 배열 [searchOptions, lectures] 지정으로 불필요한 재계산 방지
- Task 2 완료 및 진행 상황 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- allMajors 계산을 useMemo로 메모이제이션 적용
- visibleLectures 계산을 useMemo로 메모이제이션 적용
- MajorList 컴포넌트 분리 및 React.memo 적용
- 불필요한 리렌더링 방지로 성능 개선

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- LectureItem 컴포넌트 분리 및 React.memo 비교 함수 적용
- MajorItem 컴포넌트 분리 및 React.memo 적용
- MajorList 컴포넌트를 MajorItem으로 대체
- 페이지네이션 시 새로 추가되는 컴포넌트만 렌더링

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- TableOutline 컴포넌트 분리하여 useDndContext 격리
- ScheduleTable, DraggableSchedule에 React.memo 적용
- 드래그 시 TableOutline만 리렌더링되도록 최적화
- children prop 활용하여 내부 컴포넌트 리렌더링 방지

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- ScheduleTables: 핸들러 함수 useCallback으로 메모이제이션
- ScheduleTable: Props 타입 변경으로 함수 참조 직접 전달
- ScheduleDndProvider: handleDragEnd 최적화 및 함수형 업데이트 적용
- ScheduleContext: CQRS 패턴으로 Command/Query 컨텍스트 분리
- SearchDialog, ScheduleDndProvider: useScheduleCommand로 불필요한 리렌더링 방지

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- package.json: gh-pages 스크립트 추가
- vite.config.ts: GitHub Pages base URL 설정
- 모든 태스크 완료 (6/6, 100%)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- API 호출 경로에 BASE_URL 환경변수 적용
- vite-env.d.ts 타입 정의 파일 추가
- tsconfig.app.json에 vite-env.d.ts include 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
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