안녕하세요 프론트엔드 개발자 Garden, 오소현입니다:)
저는 요즘 항해 플러스 프론트엔드 3기 코스 과정에 참여하면서 공부하고 있는데요!
오늘은 그 마지막 발제 10주차의 회고를 진행해보려고 합니다 !
이번주차는 지난주에 이어서 실제 간단한 프로젝트로 성능 최적화를 진행하는 주차였습니다. 기본 과제로는 제가 지난주에 사이드 프로젝트로 진행한 것 처럼 간단하게 이미지 최적화, 자체 폰트 호스팅, 등등 라이트 하우스 지표를 통해 점점 성능을 개선해보는 주차였습니다.
심화과제는 3주차에 진행했던것 처럼 리액트 리렌더링 방지,메모제이션으로 dnd 성능을 개선해보는 과제였습니다
개인적으로 마지막 주차 심화과제가 정말 어려워서 눈물이,,, ㅠㅠ
AS - IS | TO - BE |
---|---|
성능 | 62 |
접근성 | 82 |
권장사항 | 93 |
검색엔진 최적화 | 45 |
AS-IS | TO-BE |
![]() |
![]() |
FCP: 1.9초 | FCP: 0.4초 |
LCP: 1.9초 | LCP: 0.8초 |
TBT: 910밀리초 | TBT: 0밀리초 |
CLS: 0.516 | CLS: 0.001 |
Speed Index: 2.7초 | Speed Index: 0.5초 |
지표 | 초기 값 | 점수 등급 |
---|---|---|
성능 점수 | 62 | 🔴 낮음 (0-49) |
접근성 | 82 | 🟡 중간 (50-89) |
권장사항 | 93 | 🟢 높음 (90-100) |
검색엔진 최적화 | 45 | 🔴 낮음 (0-49) |
지표 | 값 | 상태 |
---|---|---|
Largest Contentful Paint (LCP) | 14.63초 | 🔴 불량 (목표: 2.5초 이하) |
First Contentful Paint (FCP) | N/A | 🟢 개선 필요 (목표: 1.8초 이하) |
Cumulative Layout Shift (CLS) | 0.011 | 🟢 양호 (목표: 0.1 이하) |
앞선 결과로 인해 LCP(Largest Contentful Paint)와 검색엔진 최적화(SEO) 개선에 우선순위를 두고 성능 최적화 작업을 진행해야 할 예정입니다.
특히, LCP가 14.63초로 불량 상태이므로 이미지 최적화, 레이아웃 변경을 최소화, 렌더링 차단 리소스를 제거,자바스크립트 실행 최적화, 서버 응답 시간 개선 등이 주요 개선 작업의 목표가 될 것 같습니다.
개선 항목 | 개선 이유 | 개선 방법 | 향상된 지표 |
---|---|---|---|
서비스 Hero 이미지 최적화 1차 | 이미지 리사이징 | 서비스 내 큰 이미지들에 대해 비율 맞춰 리사이징 | 이미지 요청 용량이 줄어듦 |
제품 이미지 형식 변환 최적화 | LCP 개선 | JPG에서 WebP, avif 형식으로 변환 | 1220KiB 절감 |
Google Heebo 폰트 자체 호스팅 | FCP, CLS 개선 | ttf 파일이 아닌 최적화에 좋은 woff2 파일로 사용 | 용량 절감 |
제품 이미지 내부 Lazy Loading | 첫 초기 로딩 시간 단축 | 이미지 요소에 loading='lazy' 옵션 추가 | 사용자가 필요한 이미지만 로드 |
태그 사용 | 반응형 이미지 최적화 | 다양한 크기와 이미지가 깨지지 않도록 제공 | 다양한 화면 크기에 맞춰 적절한 이미지 소스를 선택할 수 있음 |
중요하지 않은 스크립트 지연 로딩 | 초기 로딩 속도 개선 | 중요하지 않은 스크립트 지연 로딩 | TBT 350ms → 720ms로 증가 |
국가 배너 레이아웃 시프트 제거 | 랜더링 속도 최적화 | 국가 배너 렌더링 로직 최적화 | 페이지 로드 시 안정성 향상 |
명시적 이미지 크기 설정 | CLS 개선, 레이아웃 안정화 | 모든 이미지에 width와 height 속성 추가 | 레이아웃 시프트 방지 |
제품 로딩 및 무거운 연산 최적화 1차 | TBT 개선, 전반적 성능 향상 | 로딩 최적화, 무거운 연산 개선 | 사용자의 지연 대폭 감소 |
이미지 사이즈 명확히 지정 | 접근성 지표 향상 | 제품 데이터 로딩 최적화, 무거운 연산 개선 | 사용자의 지연 대폭 감소 |
파비콘 이슈 대응 | 접근성 지표를 떨어뜨리는 네트워크 에러 대응 | 파비콘 아이콘 추가 | 오류를 해결해 접근성 지표 향상 |
포그라운드 색상의 대비율 최적화 | 접근성 지표 향상 | 포그라운드 색상의 대비율 최적화 | 접근성 지표 향상 |
cookieconsent 스크립트 대응 | 접근성 지표 향상 | cookieconsent 스크립트를 올바르게 로드 할 수 있도록 추가 | 접근성 지표 향상 |
메타 태그 추가하여 SEO 개선 | SEO 지표 향상 | 메타 태그 추가 | SEO 지표 향상 |
서비스 이미지 최적화 2차 | 이미지 형식 변환 | WebP로 변환되었던 이미지를 더 최적화된 AVIF로 변환 | 1280KiB 절감 |
특정 요소가 뷰포트에 들어올 때만 이벤트를 발생시키는 방식을 도입하여 최적화 | 초기 로딩 속도 개선 | 특정 요소가 뷰포트에 들어올 때만 이벤트를 발생시키는 방식을 도입 | TBT 320ms → 700ms로 증가 |
최적화 개요
WebP 및 AVIF와 같은 차세대 이미지 형식은 전통적인 PNG나 JPEG 형식에 비해 훨씬 높은 압축률을 제공합니다. 이로 인해 파일 크기가 줄어들어 다운로드 속도가 빨라지고, 데이터 소비량이 감소하여 사용자 경험이 향상됩니다.
변환 과정
저는 기존의 jpg 형식 이미지를 먼저 webp 형식으로 변환한 후, 추가적으로 AVIF 형식으로 변환하여 최적화를 완료했습니다. WebP는 JPEG보다 약 30% 더 작은 크기를 제공하고, AVIF는 WebP보다도 더 높은 압축률을 자랑합니다.
How to?
이미지 파일을 압축하고, viewport에 맞게 이미지 크기를 조정함으로써 페이지 로딩 속도를 개선했습니다.
AS-IS | TO-BE | 개선율 |
---|---|---|
LCP: 14.4초 | 10.0초 | ⬇️ 4.4초 |
TBT: 910 밀리초 | 700 밀리초 | ⬇️ 210 밀리초 |
이미지 리소스 1,240KiB | 330 KiB | ⬇️ 910 KiB |
최적화 개요
기존에는 구글 폰트 링크를 통해 외부에서 폰트를 불러왔으나, 성능 최적화를 위해 폰트를 자체 호스팅하는 방식으로 변경했습니다. 특히, woff2 파일 형식은 ttf 형식에 비해 더 높은 압축률을 제공하여 파일 크기를 줄이고, 로딩 시간을 단축시킵니다.
변환 과정
저는 woff2 형식으로 변환하여 폰트 자체 호스팅으로 인한 최적화를 완료했습니다. 이 과정에서 폰트의 품질은 유지되면서도 파일 크기가 현저히 감소했습니다.
How to?
폰트를 변환한 후, 웹 서버에 호스팅하여 CSS 파일에서 직접 참조하도록 설정했습니다. 이를 통해 외부 요청을 줄이고, 페이지 로딩 속도를 개선했습니다.
최적화 개요
중요한 리소스의 로드가 모두 완료된 후, 오프스크린 및 숨겨진 이미지를 지연 로드하는 것은 페이지의 상호작용 시작 시간을 줄이는 데 도움을 줍니다. 이를 통해 사용자는 페이지가 더 빠르게 반응하는 느낌을 받을 수 있습니다.
변환 과정
저는<picture>
태그를 사용하여 다양한 화면 크기에 맞는 이미지를 조건부로 로드하도록 설정했습니다. 각<source>
태그의media
속성을 선언하여 특정 화면 크기에 맞는 이미지만 불러오도록 변경했습니다.
How to?
아래와 같이<picture>
태그를 사용하여 이미지 소스를 설정했습니다.
<picture>
<source media="(max-width: 575px)" srcset="images/Hero_Mobile.avif">
<source media="(min-width: 576px) and (max-width: 960px)" srcset="images/Hero_Tablet.avif">
<img src="images/Hero_Desktop.avif" alt="Hero">
</picture>
AS-IS | TO-BE |
---|---|
LCP: 9.1초 | 4.5초 |
불필요한 리소스 : 2,800KiB | 0 KiB |
이렇게 구현함으로써 페이지의 로딩 성능을 크게 개선할 수 있었습니다. 지연 로드를 통해 사용자가 실제로 필요로 하는 이미지 리소스만을 로드하게 되어, 전체적인 사용자 경험과 성능을 향상했습니다.
최적화 개요
페이지의 첫 페인트를 차단하는 리소스는 사용자 경험에 부정적인 영향을 미칩니다. 이를 해결하기 위해 중요한 JavaScript 및 CSS를 인라인으로 전달하고, 중요하지 않은 모든 JavaScript 및 스타일을 지연 로드하였습니다.
개선 방식
저는 Google Tag Manager(GTM)와 쿠키 동의 스크립트에defer
속성을 추가하여 스크립트가 비동기적으로 다운로드되도록 했습니다. HTML 파싱이 완료된 후에 스크립트가 실행되어 렌더링 차단을 최소화했습니다.
How to?
아래와 같이 스크립트에defer
속성을 추가하여 구현했습니다.
<script defer type="text/javascript" src="//www.freeprivacypolicy.com/public/cookie-consent/4.1.0/cookie-consent.js" charset="UTF-8"></script>
<script defer type="text/javascript" charset="UTF-8">
document.addEventListener("DOMContentLoaded", function () {
cookieconsent.run({
notice_banner_type: "simple",
consent_type: "express",
palette: "light",
language: "en",
page_load_consent_levels: ["strictly-necessary"],
notice_banner_reject_button_hide: false,
preferences_center_close_button_hide: false,
page_refresh_confirmation_buttons: false,
website_name: "Performance Course",
});
});
</script>
AS-IS | TO-BE |
---|---|
LCP: 8.1초 | 3.5초 |
결국 렌더링 차단 리소스를 제거하고, 필요한 리소스만을 그때 로드하여 웹 성능을 올렸습니다.
제목 요소를 내림차순으로 표시
최적화 개요
heading 태그는 내림차순으로 표시되어야 한다. 건너뛰는 단계 없이 순차적으로 나타나야 합니다. 이는 검색 엔진 최적화(SEO)와 사용자 경험을 향상시키는 데 중요한 요소이다.
개선 방식: 각 정보에 맞는 마크업을 내림차순으로 표시했습니다.
AS-IS<h5>Some Subheading</h5> <h4>Another Subheading</h4> <h3>Main Heading</h3> <p>Some paragraph text.</p>
TO-BE
<h2>Main Heading</h2>
<p>Some paragraph text.</p>
| 검색엔진 최적화 | 81점 | 92점 |
이미지 alt 속성 지정
최적화 개요
alt
속성은 사용자가 느린 네트워크 환경이나 이미지 로드 오류, 시각 장애인을 위한 스크린 리더 사용 등 다양한 이유로 이미지를 볼 수 없을 때 대체 정보를 합니다 접근성과 검색 엔진 최적화를 위해 필수적으로 지정해야하는 옵션입니다.
개선 방식:
img
태그를 사용할 때alt
속성을 필수적으로 선언했습니다.
AS-IS<img src="images/vr1.avif">
TO-BE
<img src="images/vr1.avif" alt="Apple Headset">
접근성 점수 | 82점 | 92점 |
---|---|---|
검색엔진 최적화 점수 | 82점 | 94점 |
메타 설명 추가
최적화 개요
<meta name="description">
요소는 검색 엔진이 검색 결과에 포함하는 페이지 콘텐츠의 요약을 제공하고 있습니다. 고품질의 고유한 메타 설명을 사용하면 페이지의 관련성을 높이고 검색 트래픽을 늘릴 수 있어요.
개선 방식: 서비스에 대한 설명을 서술적으로 메타 정보에 추가했습니다.
AS-IS
TO-BE
<meta name="description" content="Discover top-quality VR headsets from leading brands. Shop our best-selling virtual reality devices for immersive gaming and entertainment experiences." />
검색엔진 최적화 점수 | 92점 | 100점 |
백그라운드 및 포그라운드 색상의 대비율 조정
글자색과 배경색의 대비가 떨어지면 가독성에 문제가 생길 수 있습니다. 따라서 적절한 색상 대비를 유지하는 것이 중요합니다.
개선 방식: 색상 대비 분석기 사이트를 사용하여 백그라운드에 맞는 대비 색상을 찾아 수정했습니다.
AS-ISbody { background-color: #f0f0f0; color: #ccc; }
TO-BE
body {
background-color: #ffffff;
color: #333333;
}
| 접근성 점수 | 92점 | 100점 |
앞선 개선 사항을 통해 웹 페이지의 접근성과 검색 엔진 최적화를 크게 향상시킬 수 있었습니다. 각 요소의 최적화는 사용자 경험을 개선하고, 검색 엔진에서의 가시성을 높이는 데 기여했습니다!
코드 변경 및 커밋:
개발자는 로컬 환경에서 코드를 수정하고, 변경 사항을 Git의 main 브랜치에 커밋합니다.
푸시(Push):
커밋된 변경 사항을 원격 저장소(GitHub 등)의 main 브랜치에 푸시합니다.
GitHub Webhook:
Vercel은 GitHub와 연결되어 있으며, main 브랜치에 푸시가 발생하면 GitHub에서 Vercel로 웹훅(Webhook) 알림을 보냅니다.
Vercel의 배포 프로세스 시작:
Vercel은 웹훅 알림을 수신하고, 해당 브랜치의 최신 코드를 가져옵니다.
Vercel은 자동으로 빌드 프로세스를 시작합니다. 이 과정에서 Next.js 애플리케이션이 빌드되고, 필요한 종속성이 설치됩니다.
배포:
빌드가 완료되면 Vercel은 새로운 버전을 생성하고, 이를 배포합니다.
배포가 완료되면 Vercel은 새로운 URL을 생성하거나 기존 URL을 업데이트하여 사용자가 최신 버전의 애플리케이션에 접근할 수 있도록 합니다.
구분 | 평균 소요 시간 | 최소 시간 | 최대 시간 | 평균 응답 시간 | 최대 응답 시간 | 성능 개선율 |
---|---|---|---|---|---|---|
개선 전 | 215.0 ms | 210 ms | 218 ms | 215.0 ms | 218 ms | - |
개선 후 | 113.0 ms | 89 ms | 125 ms | 113.0 ms | 125 ms | 47.4% ⬇️ |
클로저를 사용하여 API 호출 결과를 캐시하고, 이후 호출 시 캐시된 데이터를 반환하도록 변경했습니다. 클로저는 아래와 같이 구현했어요!
const createCachedApiFetcher = <T>(apiCall: () => Promise<AxiosResponse<T>>) => {
let cachedDataPromise: Promise<T> | null = null;
return () => {
if (cachedDataPromise) {
return { data: cachedDataPromise };
}
const apiResponse = apiCall();
cachedDataPromise = apiResponse.then((response) => response.data);
return apiResponse;
};
};
아래 사진으로 네트워크 탭에서 개선 된 호출은 1번 씩만 호출 되는 것을 알 수 있습니다 :)
SearchDialog의 불필요한 연산을 방지하기 위해서 아래와 같은 방법을 도입해 최적화하고자 노력했습니다 :)
useMemo
를 통한 메모이제이션강의 목록을 계산하고 , 강의 목록에서 전공을 추출하는 로직에, useMemo를 사용하여 page, lectures가 변경될 때만 재계산하도록 최적화하였습니다!
const visibleLectures = useMemo(
() => filteredLectures.slice(0, page * PAGE_SIZE),
[filteredLectures, page]
);
..
const allMajors = useMemo(
() => [...new Set(lectures.map((lecture) => lecture.major))],
[lectures]
);
useCallback
을 통한 함수 메모이제이션addSchedule 함수가 handleAddSchedule, searchInfo, onClose에 의존하여 메모이제이션 되도록 했습니다. 동일한 함수 인스턴스를 재사용하여 불필요한 재생성을 방지했어요!
const addSchedule = useCallback(
(lecture: Lecture) => {
// ...
},
[handleAddSchedule, searchInfo, onClose]
);
useEffect를 사용하여 searchInfo가 변경될 때만 상태를 초기화하게 하게 끔 구현해줬어요 상위 컴포넌트에서 랜더링해도 상태를 변경하지 않도록 구현해줬습니다..
useEffect(() => {
setInitialState({
days: searchInfo?.day ? [searchInfo.day] : [],
times: searchInfo?.time ? [searchInfo.time] : [],
});
setPage(1);
}, [setInitialState, searchInfo]);
useFetchLectures 훅으로 강의 데이터를 가져오고, useLectureFilter 훅을 사용하여 필터링 로직을 분리했는데 이 각 커스텀 훅에도 캐시를 사용하게 해서 가져오거나, useCallback, useMemo를 사용해서 불필요한 연산을 최대한 막도록 구현해보았습니다!
따라서 다음과 같이 불 필요한 연산이 개선되어서 최초에 한 번 검색한 이후에는 검색을 다시 하지 않도록 되었습니다 :)
저는 드래그 앤 드롭 기능에 랜더링 최적화를 위하여 DragLectureBlock 컴포넌트로 따로 리팩토링하였고, 이 컴포넌트 내에서 memo
사용하고, useDraggable
훅을 사용하여 드래그 가능한 요소를 설정해 상태를 제어할 수 있도록 했습니다. 이 과정에서 사용하는 컴포넌트에 다 메모제이션을 우선 추가해주었습니다.
그리고 해당 컴포넌트를 사용하는 상위 컴포넌트인 ScheduleTables에서 useDndMonitor
를 사용하여 드래그 앤 드롭 이벤트를 드래그 시작 및 종료 시에만 상태를 업데이트하여 불필요한 렌더링을 방지할 수 있도록 하였습니다.
따라서 개선 전에는 97ms -> 개선 후에는 39ms 로 개선되었습니다!
마지막 과제인 기본, 심화 과제 둘다 best를 받을 수 있엇습니다 ㅠㅠ
이번주에는 해먼드코치님께 받았는데 진짜 최고였습니다,,, 역시 항플의 연예인 코치님,,,!!
항해의 마지막 과제였던 만큼 힘들었지만 정말 아쉬웠습니다..
마무리는 전체 회고로 작성해보겠습니다!
저는 현재 항해 프로그램을 마쳤지만 10주간 아주 불태웠는데요!
바로 다음 기수인 항해 플러스 프론트엔드 코스 4기를 모집하고 있다고 하여 공유드립니다!
저도 3기 입과할때 슈퍼 얼리버드 기간에 합류해 추천인 할인까지해서 제일 최대 할인된 가격에 합류할 수 있었습니다 ㅎㅎ
또한 추천인 제도로 [추천인] 코드에 “fWHY9o”를 입력하시면 20만 원 추가 할인 혜택이 있으니 결제하실때 꼭 추천인 할인 코드도 함께 입력해주세요!
제 항해 플러스 프론트엔드 후기 글을 보고 궁금한 사항이 있으시다면 댓글이나, 벨로그 프로필 이메일, 링크드인으로 문의주세요 :)