Skip to content

Conversation

@yoonhihi97
Copy link

@yoonhihi97 yoonhihi97 commented Dec 22, 2025

과제 체크포인트

과제 요구사항

  • 배포 후 url 제출
    https://yoonhihi97.github.io/front_7th_chapter4-2/

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

  • SearchDialog 불필요한 연산 최적화

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

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

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

과제 셀프회고

기술적 성장

1. Context 분리의 중요성

문제: 하나의 스케줄을 드래그하면 6개 테이블의 139개 DraggableSchedule이 모두 리렌더링됨

원인: 모든 테이블이 하나의 공유된 DndContext를 사용. useDraggable 훅이 내부적으로 useContext를 사용하기 때문에 DndContext 상태 변경 시 모든 구독 컴포넌트가 리렌더링됨.

해결: dnd-kit 공식 문서에 따르면 DndContext를 중첩하면 각 context 내의 draggable/droppable 노드만 서로 접근 가능. 즉, 각 테이블에 독립적인 DndContext를 적용하면 다른 테이블에 영향을 주지 않음.

// Before: 공유 DndContext
<ScheduleDndProvider>
  <ScheduleTable tableId="schedule-1" />
  <ScheduleTable tableId="schedule-2" />
</ScheduleDndProvider>

// After: 각 테이블에 독립 DndContext (src/components/schedule/ScheduleTable.tsx)
const ScheduleTable = memo(({ tableId, schedules }: Props) => {
  return (
    <DndContext sensors={sensors} modifiers={snapModifiers} onDragEnd={handleDragEnd}>
      <Box position="relative">
        <ScheduleGrid />
        {schedules.map((schedule) => <DraggableSchedule ... />)}
      </Box>
      <DragOverlay>...</DragOverlay>
    </DndContext>
  );
});

결과: schedule-1 드래그 시 schedule-2~6은 리렌더링 없음

2. 같은 테이블 내 다른 아이템 리렌더링 방지

문제: 스케줄을 드롭하면 이동한 아이템뿐 아니라 같은 테이블의 다른 아이템들도 모두 리렌더링됨

원인:

  • DraggableSchedule 컴포넌트가 useDraggable 훅을 사용
  • useDraggable은 내부적으로 useContext(DndContext)를 호출
  • React에서 useContext를 사용하는 컴포넌트는 memo로 감싸도 context 값이 바뀌면 무조건 리렌더링됨
  • 드래그 상태가 바뀔 때마다 DndContext가 업데이트되므로 모든 DraggableSchedule이 리렌더링

해결: 컴포넌트를 2개로 분리

  • DraggableSchedule: useDraggable 훅만 사용 (리렌더링 불가피)
  • ScheduleItem: 순수 UI 컴포넌트, memo 적용 (props 안 바뀌면 리렌더링 안됨)
// src/components/schedule/DraggableSchedule.tsx
const DraggableSchedule = memo(({ id, data, bg }: Props) => {
  // useContext를 쓰므로 context 변경 시 이 함수는 호출됨
  const { setNodeRef, listeners } = useDraggable({ id, data });

  // 하지만 ScheduleItem은 props가 안 바뀌면 리렌더링 안됨
  return <ScheduleItem left={left} top={top} ... />;
});

// src/components/schedule/ScheduleItem.tsx
const ScheduleItem = memo(({ left, top, ... }: Props) => {
  return <Box ...>...</Box>;  // 순수 UI만
});

결과: 드롭 시 위치가 바뀐 아이템 1개만 실제 DOM 업데이트

코드 품질

Context Query/Command 패턴 분리

코치님 Q&A 세션에서 배운 패턴인데, 기존에 제가 사용하던 방식과 비교하면서 정리해봤습니다.

기존에 사용하던 Context 방식 (Before)

// 값과 setter를 하나의 객체로 묶어서 제공
const ScheduleContext = createContext<{
  schedulesMap: SchedulesMap;
  setSchedulesMap: Dispatch<SetStateAction<SchedulesMap>>;
} | undefined>(undefined);

// Provider
const ScheduleProvider = ({ children }) => {
  const [schedulesMap, setSchedulesMap] = useState<SchedulesMap>({});
  return (
    <ScheduleContext.Provider value={{ schedulesMap, setSchedulesMap }}>
      {children}
    </ScheduleContext.Provider>
  );
};

// 사용하는 쪽
const SearchDialog = () => {
  const { setSchedulesMap } = useContext(ScheduleContext);  // setter만 필요
  // ...
};

문제점: SearchDialog는 setSchedulesMap만 필요한데, schedulesMap이 바뀔 때마다 리렌더링됩니다. Context의 value 객체가 바뀌면 그걸 구독하는 모든 컴포넌트가 리렌더링되기 때문입니다.

Query/Command 패턴 (After)

읽기(Query)와 쓰기(Command)를 별도의 Context로 분리합니다.

// src/contexts/schedule/scheduleContext.ts
// 읽기 전용 - 값이 바뀌면 구독자 리렌더링
export const ScheduleQueryContext = createContext<SchedulesMap | undefined>(undefined);
// 쓰기 전용 - setter는 참조가 안 바뀌므로 리렌더링 없음
export const ScheduleCommandContext = createContext<Dispatch | undefined>(undefined);

// Provider
const ScheduleProvider = ({ children }) => {
  const [schedulesMap, setSchedulesMap] = useState<SchedulesMap>({});
  return (
    <ScheduleQueryContext.Provider value={schedulesMap}>
      <ScheduleCommandContext.Provider value={setSchedulesMap}>
        {children}
      </ScheduleCommandContext.Provider>
    </ScheduleQueryContext.Provider>
  );
};

// 사용하는 쪽
const SearchDialog = () => {
  const setSchedulesMap = useScheduleCommand();  // Command만 구독
  // schedulesMap이 바뀌어도 리렌더링 안됨!
};

const ScheduleTables = () => {
  const schedulesMap = useScheduleContext();  // Query 구독
  // schedulesMap이 바뀌면 리렌더링됨 (의도한 동작)
};

왜 이게 되는가?

  • useState의 setter 함수는 stable identity를 가집니다 (공식 문서: "The set function has a stable identity")
  • 따라서 ScheduleCommandContext의 value는 절대 바뀌지 않습니다
  • setter만 필요한 컴포넌트는 Command Context만 구독하면 불필요한 리렌더링을 피할 수 있습니다

배운 점: 이 패턴을 몰랐을 때는 "Context 쓰면 리렌더링 많이 되니까 전역 상태관리 써야지"라고 생각했는데, Context만으로도 충분히 최적화할 수 있다는 걸 알게 됐습니다. 앞으로 Context 설계할 때 기본으로 적용할 것 같습니다.

학습 효과 분석

이번 과제에서 가장 크게 배운 점은 "렌더링이 왜 일어나는지"를 정확히 이해하는 것이었습니다.

처음에는 memo로 감싸면 리렌더링이 막힐 줄 알았는데, useContext를 쓰는 순간 memo가 무력화된다는 걸 몰랐습니다.

// 이렇게 해도 context가 바뀌면 리렌더링됨
const Component = memo(() => {
  const value = useContext(SomeContext);  // memo 우회
  return <div>{value}</div>;
});

왜 이런 일이 발생할까?

React의 리렌더링은 크게 3가지 경우에 발생합니다:

  1. 컴포넌트의 state가 바뀔 때
  2. 부모 컴포넌트가 리렌더링될 때 (→ memo로 방지 가능)
  3. 구독 중인 context 값이 바뀔 때 (→ memo로 방지 불가능)

React 공식 문서에서도 이렇게 명시하고 있습니다:

"Skipping re-renders with memo does not prevent the children receiving fresh context values."

즉, memoprops 변경만 체크합니다. context는 props가 아니라 컴포넌트 내부에서 구독하는 값이기 때문에, context가 바뀌면 memo와 상관없이 리렌더링됩니다.

해결법: useContext를 쓰는 컴포넌트와 실제 UI를 분리해야 memo 효과를 볼 수 있음

// useContext를 쓰는 컴포넌트 (리렌더링 불가피)
const DraggableSchedule = memo(({ id, data }: Props) => {
  const { setNodeRef, listeners } = useDraggable({ id });  // 내부에서 useContext 사용
  const { left, top } = calculatePosition(data);

  // ScheduleItem은 props가 안 바뀌면 리렌더링 안됨
  return <ScheduleItem left={left} top={top} ... />;
});

// 순수 UI 컴포넌트 (useContext 없음 → memo 효과 있음)
const ScheduleItem = memo(({ left, top, ... }: Props) => {
  return <Box position="absolute" left={left} top={top}>...</Box>;
});

이렇게 분리하면:

  • DraggableSchedule: context 변경 시 함수는 호출되지만, props 계산만 하고 끝
  • ScheduleItem: props(left, top 등)가 안 바뀌면 리렌더링 안됨 → 실제 DOM 업데이트 없음

리뷰 받고 싶은 내용

@yoonhihi97 yoonhihi97 changed the title [5팀 이윤지] Chapter 4-2. 코드 관점의 성능 최적화 [3팀 이윤지] Chapter 4-2. 코드 관점의 성능 최적화 Dec 22, 2025
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