항해플러스 프론트엔드 5기 후기(10주차) - 코드 관점의 성능 최적화

유한별·2025년 6월 9일
0
post-thumbnail

이번 챕터에서는 성능 최적화를 주제로 두 가지 방향에서 과제를 진행했다.

첫 번째는 웹 페이지의 초기 렌더링 성능을 개선하는 것이었고,
두 번째는 React 컴포넌트 렌더링 성능과 상태 구조 최적화에 집중하는 과제였다.

처음엔 단순히 성능 점수를 높이는 것에만 집중했지만, 과제를 진행하면서
"왜 이 부분에서 비용이 발생하는지",
"렌더링 최적화는 어떤 흐름으로 접근해야 하는지",
"상태 구조는 어떻게 설계해야 이후 최적화가 쉬운지"
등을 더 깊게 고민해볼 수 있는 시간이었다.

이 글에서는 두 가지 최적화 과제를 진행하면서 발견한 문제와 개선 과정을 정리해보려 한다.

💡 Lighthouse 기반 웹 성능 최적화

최초 성능 보고서

lighthouse(CI) 성능 보고서

pagespeed 성능 보고서

주요 개선 내용

1. 대규모 레이아웃 변경 피하기 (CLS) & 오프스크린 이미지 지연

CLS (Cumulative Layout Shift)

  • 페이지가 렌더링 중에 요소가 위치를 바꾸는 현상을 측정하는 지표

초기 구조

<section class="hero">
  <img class="desktop" src="images/Hero_Desktop.webp" />
  <img class="tablet" src="images/Hero_Tablet.webp" />
  <img class="mobile" src="images/Hero_Mobile.webp" />
</section>

이슈 원인

  • 기존 구조에서는 모든 imgDOM 에 남아 있었고, CSS media querydisplay: none 처리
  • 브라우저는 초기 layout 계산 시 모든 img 를 고려
  • display: noneheight 공간 계산 안 됨 → 초기 레이아웃이 불안정하게 됨 → CLS 발생
  • 또한 각 img 요소마다 명시적인 크기가 없음 → 브라우저가 이미지 크기를 추정해야 함 → 이미지 로드 시 크기가 변경될 수 있음 → CLS 발생

개선 구조

<picture>
  <source srcset="images/Hero_Mobile.webp" media="(max-width: 576px)" />
  <source srcset="images/Hero_Tablet.webp" media="(max-width: 960px)" />
  <img
    src="images/Hero_Desktop.webp"
    sizes="100vw"
    alt="Hero Image"
    class="hero-image"
  />
</picture>
<img src="images/vr1.webp" alt="product: Penom Case" width="128" height="128" />

개선 효과

  • picture + source 구조에서는 브라우저가 조건에 맞는 source 만 선택해서 로드
  • 다른 이미지들은 아예 고려 하지 않음 (display: none 이 아님, DOM 에서 제외) → 초기 layout 계산이 안정적 → CLS 현상이 사라짐
  • img 요소에 widthheight 를 명시적으로 설정해 브라우저가 이미지 크기를 추정하지 않도록 함

2. 차세대 형식을 사용해 이미지 제공 & 효율적으로 이미지 인코딩하기

초기 이미지 파일

  • 각 이미지 파일은 jpg 및 png 포맷으로 제공
  • 파일 포맷 특성상 압축 효율이 낮고, 동일 품질 대비 파일 크기가 큼

개선 이미지 파일

  • 모든 이미지 파일을 WebP 포맷으로 변환하여 제공
  • WebP는 손실 및 무손실 압축 모두 지원하며, 동일 품질 기준으로 jpg, png 대비 훨씬 더 높은 압축 효율 제공

개선 효과

  • WebP 형식은 동일한 이미지 대비 jpg 및 png 형식보다 파일 크기가 더 작음
  • 이미지 파일 크기 감소로 네트워크 전송량이 줄어듦

3. 이미지 크기 적절하게 설정하기

초기 이미지 파일

  • 각 이미지 파일의 원본 해상도(원본 px 단위)가 렌더링 시 실제 표시 크기에 비해 과도하게 큼
  • 브라우저는 필요한 렌더링 크기에 맞게 다운스케일링 처리하지만, 원본 전송량(네트워크 비용)은 그대로 발생
  • 프로젝트 구조상 로컬 정적 파일(images 디렉토리 내 저장) 기반으로 관리되어 있어, 초기에는 별도의 사이즈 최적화가 적용되지 않은 상태였음

개선 이미지 파일

  • 각 이미지 파일을 실제 렌더링 영역(visible size)을 고려한 적정 해상도로 리사이징
  • 만약 CDN 서비스를 도입했다면 이미지 리사이징 API(예: width/quality 파라미터)를 통해 자동 최적화를 적용할 수 있었겠지만, 이번 프로젝트에서는 로컬 파일 기반인 점을 고려해 수동 리사이징 및 WebP 압축 최적화 방식으로 대응
  • 결과적으로 렌더링 크기에 적합한 해상도로 이미지를 재생성하여 전송량을 줄임

개선 효과

  • 이미지 크기를 줄임으로써 네트워크 전송량 감소 및 렌더링 속도 향상

4. 콘텐츠가 포함된 최대 페인트 요소 (LCP)

LCP (Largest Contentful Paint)

  • 페이지가 렌더링 중에 가장 큰 요소가 렌더링 되는 시간을 측정하는 지표

문제 상황

  • Lighthouse 분석 결과, Hero Image가 콘텐츠가 포함된 최대 페인트 요소 (LCP)로 측정됨
  • 측정 시 전체 LCP 타이밍: 약 2470ms
  • 구성 비율:
단계비율시간
TTFB7%170ms
로드 지연1%20ms
로드 시간 (다운로드)71%1760ms
렌더링 지연21%520ms

개선 내용

  • Hero Image preload 적용

    • Hero Image (Desktop / Tablet / Mobile) 각각에 대해 태그 적용
    • 브라우저가 이미지를 우선적으로 로드하도록 개선
<link
  rel="preload"
  as="image"
  href="images/Hero_Desktop.webp"
  media="(min-width: 961px)"
/>
<link
  rel="preload"
  as="image"
  href="images/Hero_Tablet.webp"
  media="(min-width: 577px) and (max-width: 960px)"
/>
<link
  rel="preload"
  as="image"
  href="images/Hero_Mobile.webp"
  media="(max-width: 576px)"
/>
  • Hero Image 구조 개선 (picture + source 사용)

    • 기존 img + display: none 방식에서 → <picture> + <source> 구조로 변경
    • 브라우저가 조건에 맞는 이미지 source 만 로드하도록 구조 변경 → CLSLCP 개선
<picture>
  <source srcset="images/Hero_Mobile.webp" media="(max-width: 576px)" />
  <source srcset="images/Hero_Tablet.webp" media="(max-width: 960px)" />
  <img
    src="images/Hero_Desktop.webp"
    sizes="100vw"
    alt="Hero Image"
    class="hero-image"
  />
</picture>
  • Critical CSS 적용 (aspect-ratio 적용)

    • Hero Image 에 aspect-ratio 적용하여 초기 레이아웃 시 height 공간 확보
    • 브라우저가 이미지 크기를 추정하지 않도록 함
<style>
  .hero-image {
    width: 100%;
    height: auto;
    filter: brightness(50%);
    aspect-ratio: 2160 / 1005;
  }

  @media screen and (max-width: 960px) {
    .hero-image {
      aspect-ratio: 960 / 770;
    }
  }

  @media screen and (max-width: 576px) {
    .hero-image {
      aspect-ratio: 1 / 1;
    }
  }
</style>
  • Cookie Consent JS defer 적용

    • defer 속성 추가 (HTML 파싱이 끝난 후 JS 실행)
    • HTML 파싱 후 실행 → main thread block 완화
<script
  type="text/javascript"
  src="//www.freeprivacypolicy.com/public/cookie-consent/4.1.0/cookie-consent.js"
  charset="UTF-8"
  defer
></script>

개선 사후 보고서

lighthouse 성능 보고서

최초 성능 보고서와 비교

지표최초 성능 보고서개선 사후 보고서
LCP13.96s2.44s
INPN/AN/A
CLS0.0110.011

LCP 지표 개선 효과

  • LCP 지표가 2.44s로 초기 13.96s 대비 82.5% 개선됨
  • 이는 초기 렌더링 시간이 크게 줄어들었음을 의미
  • 브라우저가 이미지를 더 빠르게 로드하고 렌더링할 수 있음

Performance (성능) 지표 개선 효과

  • Performance 지표가 95점으로 개선됨
  • 이는 성능 관련 모든 지표가 최적화되어 있음을 의미
  • 사용자 경험 향상

pagespeed 성능 보고서

개선 후 PageSpeed 성능 보고서

최초 성능 보고서와 비교

항목개선 전개선 후변화
총점65점100점+35점 상승
First Contentful Paint (FCP)0.7초0.6초개선 (0.1초 빠름)
Largest Contentful Paint (LCP)2.5초0.6초개선 (1.9초 빠름)
Total Blocking Time (TBT)110ms0ms완전 해소
Cumulative Layout Shift (CLS)0.4770.016안정화 (대폭 개선)
Speed Index1.0초0.6초개선 (0.4초 빠름)

Largest Contentful Paint (LCP) 개선

  • 개선 전: 2.5초 → 개선 후: 0.6초
  • Hero Imagepreload 적용 + WebP 변환 + Critical CSS 적용 + JS defer 적용 등의 영향
  • Hero Imagepaint 시점이 크게 앞당겨짐

Cumulative Layout Shift (CLS) 개선

  • 개선 전: 0.477 (기준 초과, 매우 나쁨) → 개선 후: 0.016 (안정 영역)
  • Hero Imageaspect-ratio 적용 + img 구조 변경 (<picture> + <source> 사용)
  • 레이아웃 시프트가 발생하지 않음

Total Blocking Time (TBT) 개선

  • 개선 전: 110ms → 개선 후: 0ms
  • Cookie Consent JSdefer 처리 → main thread blocking 원인 제거됨

First Contentful Paint (FCP), Speed Index 개선

  • FCP: 0.7초 → 0.6초
  • Speed Index: 1.0초 → 0.6초
  • 이미지 최적화 + preload + 레이아웃 안정화 영향 → 초기 렌더링 빠르게 진행됨

🚀 React 렌더링 최적화

Lighthouse 기반 최적화가 '페이지 초기 렌더링' 관점이었다면,
이번에는 실제 사용자 상호작용 중 발생하는 렌더링 비용을 줄이는 것이 목표였다.

특히 React Devtool의 Profiler를 활용해 렌더링을 확인해보니, SearchDialog 페이지네이션과 DnD 시스템에서 불필요한 렌더링과 연산 비용이 눈에 띄게 발생하고 있었다.

SearchDialog 최적화

API 호출 최적화

기존에는 내부에서 잘못된 Promise.all 사용으로fetchAllLectures 함수에서 API 요청이 직렬로 실행되고 있었다.
또한 동일 데이터를 여러 번 요청하는 구조였기 때문에 불필요한 API 요청이 존재했다.

const fetchMajors = () => axios.get<Lecture[]>('/schedules-majors.json');
const fetchLiberalArts = () => axios.get<Lecture[]>('/schedules-liberal-arts.json');

const fetchAllLectures = async () => await Promise.all([
  (console.log('API Call 1', performance.now()), await fetchMajors()),
  (console.log('API Call 2', performance.now()), await fetchLiberalArts()),
  (console.log('API Call 3', performance.now()), await fetchMajors()),
  (console.log('API Call 4', performance.now()), await fetchLiberalArts()),
  (console.log('API Call 5', performance.now()), await fetchMajors()),
  (console.log('API Call 6', performance.now()), await fetchLiberalArts()),
]);

이를 개선하기 위해

  • useRef 기반 캐싱을 적용하고
  • fetch 로직을 useCallback으로 감싸주었으며,
  • Promise.all 사용 시 await 제거 → 병렬 요청 가능하도록 변경하였다.

import { useCallback, useRef } from "react";
import { Lecture } from "./types";
import axios from "axios";

export const useLectureFetcher = () => {
  const majorsCacheRef = useRef<Lecture[] | null>(null);
  const liberalArtsCacheRef = useRef<Lecture[] | null>(null);

  const fetchMajors = useCallback(async () => {
    if (majorsCacheRef.current) {
      return majorsCacheRef.current;
    }
    const response = await axios.get<Lecture[]>("/schedules-majors.json");
    majorsCacheRef.current = response.data;
    return majorsCacheRef.current;
  }, []);

  const fetchLiberalArts = useCallback(async () => {
    if (liberalArtsCacheRef.current) {
      return liberalArtsCacheRef.current;
    }
    const response = await axios.get<Lecture[]>("/schedules-liberal-arts.json");
    liberalArtsCacheRef.current = response.data;
    return liberalArtsCacheRef.current;
  }, []);

  const fetchAllLectures = useCallback(async () => {
    return await Promise.all([
      (console.log("API Call 1", performance.now()), fetchMajors()),
      (console.log("API Call 2", performance.now()), fetchLiberalArts()),
      (console.log("API Call 3", performance.now()), fetchMajors()),
      (console.log("API Call 4", performance.now()), fetchLiberalArts()),
      (console.log("API Call 5", performance.now()), fetchMajors()),
      (console.log("API Call 6", performance.now()), fetchLiberalArts()),
    ]);
  }, [fetchMajors, fetchLiberalArts]);

  return { fetchAllLectures };
};

결과적으로

  • 중복 API 요청을 방지하고,
  • 동일한 요청은 한 번만 실행되며
  • 병렬로 빠르게 응답을 받을 수 있는 구조로 개선되었다.

불필요한 연산 줄이기

초기 코드에는 해당 컴포넌트에 불필요한 연산이 존재했다.
getFilteredLectures 함수가 매 렌더링마다 실행되며, 내부에서 parseSchedule이 중복 실행되는 구조였다.

const getFilteredLectures = () => {
  const { query = "", credits, grades, days, times, majors } = searchOptions;

  return lectures
    .filter((lecture) =>
      lecture.title.toLowerCase().includes(query.toLowerCase()) ||
      lecture.id.toLowerCase().includes(query.toLowerCase())
    )
    .filter((lecture) => grades.length === 0 || grades.includes(lecture.grade))
    .filter((lecture) => majors.length === 0 || majors.includes(lecture.major))
    .filter((lecture) => !credits || lecture.credits.startsWith(String(credits)))
    .filter((lecture) => {
      const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : [];
      return days.length === 0 || schedules.some((s) => days.includes(s.day));
    })
    .filter((lecture) => {
      const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : [];
      return times.length === 0 || schedules.some((s) =>
        s.range.some((time) => times.includes(time))
      );
    });
};

위 코드처럼 parseSchedule이 중복 호출되고 있었고, 필터링 연산 또한 렌더링마다 다시 실행되어 페이지네이션 시 성능 저하가 발생했다.

이를 개선하기 위해

  • parseSchedule 호출 조건을 정리하여 한 번만 호출하도록 수정하고
  • getFilteredLectures 결과를 useMemo로 메모이제이션 처리하고
  • filteredLectures, visibleLectures, allMajors 계산에도 useMemo를 적용하였다.
export const getFilteredLectures = (
  lectures: Lecture[],
  searchOptions: SearchOption
) => {
  const { query = "", credits, grades, days, times, majors } = searchOptions;

  return lectures
    .filter(
      (lecture) =>
        lecture.title.toLowerCase().includes(query.toLowerCase()) ||
        lecture.id.toLowerCase().includes(query.toLowerCase())
    )
    .filter((lecture) => grades.length === 0 || grades.includes(lecture.grade))
    .filter((lecture) => majors.length === 0 || majors.includes(lecture.major))
    .filter((lecture) => !credits || lecture.credits.startsWith(String(credits)))
    .filter((lecture) => {
      if (days.length === 0 && times.length === 0) {
        return true;
      }

      const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : [];

      const matchDay =
        days.length === 0 || schedules.some((s) => days.includes(s.day));

      const matchTime =
        times.length === 0 ||
        schedules.some((s) => s.range.some((time) => times.includes(time)));

      return matchDay && matchTime;
    });
};
const filteredLectures = useMemo(() => {
  return getFilteredLectures(lectures, searchOptions);
}, [lectures, searchOptions]);

const lastPage = useMemo(() => {
  return Math.ceil(filteredLectures.length / PAGE_SIZE);
}, [filteredLectures]);

const visibleLectures = useMemo(() => {
  return filteredLectures.slice(0, page * PAGE_SIZE);
}, [filteredLectures, page]);

const allMajors = useMemo(() => {
  return [...new Set(lectures.map((lecture) => lecture.major))];
}, [lectures]);

결과적으로 중복 연산이 제거되고, 필터링 연산이 변경된 경우에만 수행되며
페이지네이션 시 연산 비용이 눈에 띄게 줄어든 구조로 개선되었다.

불필요한 렌더링 최적화

또한 초기 코드에서는 상위 컴포넌트의 상태 변경 시, 전공 목록(MajorFilterSection)과 강의 목록(LectureRow) 모든 요소가 리렌더링되는 문제가 있었다.

대표적인 예시는 전공 필터 토글 시 MajorItem들이 전부 리렌더링되고, 페이지네이션 시 LectureRow 전체가 다시 렌더링되는 상황이었다.

const allMajors = [...new Set(lectures.map((lecture) => lecture.major))];

const toggleMajor = (major: string) => {
  setSearchOptions((prev) => {
    const newMajors = prev.majors.includes(major)
      ? prev.majors.filter((m) => m !== major)
      : [...prev.majors, major];
    return { ...prev, majors: newMajors };
  });
};

문제 원인은

  • allMajors가 매 렌더링마다 재생성되고
  • toggleMajor 핸들러도 매번 새로 만들어지며
  • 자식 컴포넌트(MajorItem, LectureRow)가 React.memo 처리 없이 그대로 전달되고 있었기 때문이었다.

이를 개선하기 위해

  • allMajorsuseMemo로 메모이제이션
  • toggleMajoruseCallback으로 메모이제이션
  • MajorItem, LectureRow 등에 React.memo 적용
  • visibleLecturesuseMemo 적용하여 페이지 변경 시에만 최소 렌더링을 수행하도록 변경했다.
// useMemo 적용
const allMajors = useMemo(() => {
  return [...new Set(lectures.map((lecture) => lecture.major))];
}, [lectures]);


// useCallback 적용
const toggleMajor = useCallback((major: string) => {
  setSearchOptions((prev) => {
    const newMajors = prev.majors.includes(major)
      ? prev.majors.filter((m) => m !== major)
      : [...prev.majors, major];
    return { ...prev, majors: newMajors };
  });
  setPage(1);
  loaderWrapperRef.current?.scrollTo(0, 0);
}, []);
// React.memo 적용
export const MajorItem = React.memo(({ major, isSelected, onToggle }) => {
  return (
    <Box key={major}>
      <Checkbox
        size="sm"
        value={major}
        isChecked={isSelected}
        onChange={() => onToggle(major)}
      >
        {major.replace(/<p>/gi, " ")}
      </Checkbox>
    </Box>
  );
});

이렇게 개선한 결과

  • 전공 필터 토글 시 MajorItem은 변경된 요소만 리렌더링
  • 페이지네이션 시 LectureRow는 새로 추가되는 강의만 렌더링
  • 전체 렌더링 비용이 감소했고, 해당 컴포넌트 내부에서 스크롤 성능도 자연스럽게 개선되었다.

DND 렌더링 최적화

또한 초기 코드에서는 DnD(드래그 앤 드롭) 기능 사용 시, 드래그 시작 시점부터 거의 모든 ScheduleTableDraggableSchedule 요소가 리렌더링되고, 드롭 이후에도 모든 테이블이 리렌더링되는 문제가 있었다.

원인은

  • ScheduleTableschedulesMap 전체를 의존하고 있었고
  • 드래그 상태를 useDndContext로 구독하면서 드래그 상태 변화 시마다 테이블 전체가 리렌더링되고
  • DraggableSchedule 역시 별도 메모이제이션 없이 매번 렌더링될 뿐만 아니라
  • 드롭 시 handleDragEnd에서 setSchedulesMap 전체 업데이트로 인해 관련 없는 테이블까지 리렌더링이 발생되는 구조였기 때문이었다.
export const ScheduleProvider = ({ children }: PropsWithChildren) => {
  const [schedulesMap, setSchedulesMap] =
    useState<Record<string, Schedule[]>>(dummyScheduleMap);

  return (
    <ScheduleContext.Provider value={{ schedulesMap, setSchedulesMap }}>
      {children}
    </ScheduleContext.Provider>
  );
};
const ScheduleTable = ({ tableId, schedules, onScheduleTimeClick, onDeleteButtonClick }) => {
  const dndContext = useDndContext();

  return (
    <TableContainer>
      <TableGrid onTimeClick={handleTimeClick} />
      {schedules.map((schedule, index) => (
        <DraggableSchedule
          key={`${schedule.lecture.id}-${index}`}
          id={`${tableId}:${index}`}
          data={schedule}
        />
      ))}
    </TableContainer>
  );
};

이를 개선하기 위해

  • ScheduleTable 단위로 필요한 tableId의 데이터만 가져오는 getSchedulesByTableId 함수를 context에서 제공하여 ScheduleTable이 전체 schedulesMap을 직접 구독하지 않도록 변경
  • DraggableScheduleReact.memo 적용
  • ScheduleTable 내부에서 useMemo, useCallback 활용해 렌더 비용 최소화
  • 드래그 중인 스케줄만 transform 변화에 반응
  • 드롭 시 handleDragEnd에서 updateSchedulesByTableId 함수 사용 → 변경된 테이블만 업데이트하여 관련 없는 테이블 리렌더링 방지하도록 최적화를 진행했다.
export const ScheduleProvider = ({ children }: PropsWithChildren) => {
  const [schedulesMap, setSchedulesMap] =
    useState<Record<string, Schedule[]>>(dummyScheduleMap);

  const getSchedulesByTableId = useCallback(
    (tableId: string) => {
      return schedulesMap[tableId] || [];
    },
    [schedulesMap]
  );

  const updateSchedulesByTableId = useCallback((tableId: string, updater) => {
    setSchedulesMap((prev) => ({
      ...prev,
      [tableId]: updater(prev[tableId] || []),
    }));
  }, []);

  return (
    <ScheduleContext.Provider
      value={{
        getSchedulesByTableId,
        updateSchedulesByTableId,
      }}
    >
      {children}
    </ScheduleContext.Provider>
  );
};
const ScheduleTableMemo = React.memo(
  ({ tableId, onScheduleTimeClick, onDeleteButtonClick }) => {
    const { getSchedulesByTableId } = useScheduleContext();

    const schedules = useMemo(
      () => getSchedulesByTableId(tableId),
      [getSchedulesByTableId, tableId]
    );

    return (
      <ScheduleTable
        tableId={tableId}
        schedules={schedules}
        onScheduleTimeClick={onScheduleTimeClick}
        onDeleteButtonClick={onDeleteButtonClick}
      />
    );
  }
);
export const DraggableSchedule = React.memo(
  ({ id, data, bg, onDeleteButtonClick }) => {
    // transform 변화만 적용
    const transformStyle = useMemo(() => {
      return CSS.Translate.toString(transform);
    }, [transform]);

    return (
      <Box
        ref={setNodeRef}
        style={{
          transform: transformStyle,
          // 기타 스타일
        }}
        {...listeners}
        {...attributes}
      >
        {/* 콘텐츠 */}
      </Box>
    );
  }
);
const handleDragEnd = useCallback(
  (event: any) => {
    const { active, delta } = event;
    const [tableId, index] = active.id.split(":");

    updateSchedulesByTableId(tableId, (schedules) => {
      const updatedSchedules = [...schedules];
      updatedSchedules[index] = {
        ...schedules[index],
        day: newDay,
        range: newRange,
      };
      return updatedSchedules;
    });
  },
  [updateSchedulesByTableId]
);

결과적으로

  • 드래그 중에는 필요한 테이블과 스케줄만 리렌더링
  • 드롭 시에도 변경된 테이블만 업데이트되어 불필요한 리렌더링 방지
  • DnD 전체 UX가 매우 부드럽게 개선되었다.

🧠 회고

이번 과제를 하면서 성능 최적화라는 걸 여러 각도에서 체감해볼 수 있었다.

Lighthouse 기반 최적화는 처음엔 그냥 점수 올리는 느낌으로 접근했는데, 지표 하나하나를 뜯어보면서 LCP, CLS 같은 렌더링 안정성이나 초기 로딩 속도가 왜 중요한지 조금 감이 잡혔다.
특히 이미지와 같은 정적 자산을 어떻게 구조적으로 처리해야 성능에 영향이 적을지 고민해본 건 꽤 의미 있었다.

SearchDialogDnD 쪽에서는 그동안 당연하게 쓰던 useCallback, useMemo, React.memo들에 대해 단순히 “써야 한다”가 아니라 어디서 써야 진짜 효과가 나는지를 직접 느낄 수 있었다.
뿐만 아니라 이를 통해 DnD에서 스케줄 테이블 리렌더링을 잡아가는 과정에서는 성능뿐 아니라 인터랙션 자체가 훨씬 부드러워지는 걸 체감할 수 있었다.

사실 엄청 고난도 기술을 쓴 건 아니지만, 기본적인 설계랑 기초적인 최적화만으로도 결과가 꽤 달라진다는 걸 배운 과제였다. 덕분에 성능 쪽을 좀 더 재미있게 바라볼 수 있는 계기가 됐다.

profile
세상에 못할 일은 없어!

0개의 댓글