성능
Lighthouse
웹 페이지의 성능, 접근성, SEO, PWA 등을 분석해서 점수와 함께 개선할 수 있는 인사이트를 제공해주는 오픈소스 자동화 도구
항목 | 설명 |
Performance(성능) | 페이지 로딩 속도, 인터랙티브 속도 등 |
Accessibility(접근성) | 다양한 사용자(장애인 포함)가 콘텐츠를 이용 가능한지 |
Best Practices(모범 사례) | 웹 표준, 보안 관련 권장 사항 준수 여부 |
SEO(검색 엔진 최적화) | 검색 엔진에 잘 노출될 수 있도록 구조화됐는지 |
PWA(Progressive Web App) | 앱처럼 동작할 수 있는 웹앱인지 |
🛠️ 실행 방법
Chrome DevTools 사용
- 페이지에서 개발자 도구 열기
- 상단 탭에서 Lighthouse 클릭
- 원하는 항목 체크 후 Analyze 버튼 클릭
- 리포트 생성됨 ✅
강사님 참고 자료: https://somyclass.notion.site/Lighthouse-1d6973f5e0fe801da2e6ffd4662f7a47
Lighthouse: 웹 성능 최적화 도구 | Notion
Lighthouse는 Google에서 개발한 오픈소스 웹 성능 분석 도구입니다. 웹사이트의 품질을 측정하고 개선할 수 있는 방법을 제공합니다.
somyclass.notion.site
Lazy & Suspense
🔹 React.lazy()
어떤 컴포넌트를 처음부터 불러오지 않고, 사용자가 실제로 그 페이지나 기능을 열 때 로딩하는 방식
import { lazy, Suspense } from 'react'
const MainPage = lazy(() => import('./pages/MainPage'))
const AboutPage = lazy(() => import('./pages/AboutPage'))
const ShopPage = lazy(() => {
// 메인 페이지가 로드된 후 1초 후에 Shop 페이지 미리 로드
const preloadShop = () => import('./pages/ShopPage')
// 중요한 페이지라면 Suspense가 표시되지 않도록 미리 로드
if (typeof window !== 'undefined') {
window.setTimeout(preloadShop, 1000)
}
return preloadShop()
})
const BlogPage = lazy(() => import('./pages/BlogPage'))
const CartPage = lazy(() => import('./pages/CartPage'))
import()는 동적 임포트로, Webpack이 자동으로 별도의 chunk 파일로 분리해줌
그래서 메인 번들 크기가 줄어들고, 초기에 더 빠르게 로딩 가능
참고해 볼 만한 자료: https://velog.io/@ksa199653/React-%EC%A7%80%EC%97%B0%EB%A1%9C%EB%94%A9lazy-loading
[React] 지연로딩(lazy loading)
리엑트 애플리케이션을 배포하기전에 많은 테스트와 최적화를 이루기도 하지만, 브라우저로 불러오는 자바스크립트 파일들의 로딩되는 부분을 수동으로 제어할 수 있는 기능이 있다.애플리케
velog.io
🔸 Suspense
Lazy 컴포넌트가 로딩되는 동안 보여줄 fallback UI를 정의해주는 컴포넌트
{/* Suspense를 사용한 로딩 상태 처리 */}
<div className="content">
<Suspense fallback={<Loading />}>
{tab === 'dashboard' && <LazyDashboard />}
{tab === 'analytics' && <LazyAnalytics />}
{tab === 'lazy' && <LazyComponent />}
</Suspense>
</div>
💡 사용하는 경우
- 페이지 단위 라우팅 컴포넌트에서 자주 사용
- 대형 컴포넌트를 작은 청크로 나눠서 퍼포먼스 최적화할 때
- 초기 로딩을 빠르게 하고 싶을 때
강사님 참고자료: https://somyclass.notion.site/lazy-Suspense-1d6973f5e0fe819388e9d79265db6e5e
lazy, Suspense | Notion
lazy 개념
somyclass.notion.site
Promise로 감싸서 await로 비동기 함수 처리
await new Promise(resolve => setTimeout(resolve, 1500))
- setTimeout(() => {}, 1500)은 1.5초 후에 어떤 일을 하라는 거지, 기다려주지 않음
- await은 "기다릴 수 있는 함수(Promise)"여야 사용 가능
- setTimeout을 Promise로 감싸서 await를 사용하면 딜레이 주기 가능
디바운싱(Debouncing)
짧은 시간 동안 여러 번 발생하는 이벤트를 제어해서, 일정 시간 동안 아무런 추가 이벤트가 없을 때만 함수가 실행되도록 하는 기법
⏱️ 작동 원리
- 사용자가 이벤트를 발생시킬 때마다 타이머를 리셋
- 마지막 이벤트 발생 후 일정 시간(delay)이 지나도록 아무 이벤트가 없을 때만 함수 실행
🧨 문제 상황: 리사이즈 이벤트 너무 자주 발생함
const handleResize = () => {
if (window.innerWidth > 1100) {
setIsOn(false)
}
}
useEffect(() => {
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
→ 사용자가 창을 조절할 때 resize 이벤트는 엄청 자주 발생함
→ 그때마다 handleResize가 실행되고, 조건에 따라 setIsOn(false)가 계속 호출됨
→ 불필요한 상태 업데이트가 발생할 수 있음
→ 리렌더링이 계속 일어나 성능 저하 발생 가능성 있음
✅ 디바운싱 적용
- resize 이벤트가 멈춘 뒤 150ms 동안 추가 이벤트 없을 때만 handleResize 실행
// 디바운싱된 리사이즈 핸들러
const debouncedResize = useMemo(
() =>
debounce(() => {
if (window.innerWidth > 1100) {
setIsOn(false)
}
}, 150),
[]
)
useEffect(() => {
window.addEventListener('resize', debouncedResize)
return () => {
window.removeEventListener('resize', debouncedResize)
debouncedResize.cancel() // 디바운싱 타이머 클리어
}
}, [debouncedResize, isOn])
React.memo / useMemo / useCallback
✅ React.memo
컴포넌트 자체를 메모이제이션
📌 특징
- 컴포넌트가 같은 props로 재호출될 때 리렌더링을 막아줌
- 성능을 높이는 데 좋지만, 모든 컴포넌트에 쓰는 건 아니고
- 자주 렌더링되는데 props가 자주 안 바뀌는 컴포넌트에 효과적
- 특히 네비게이션 같은 건 거의 바뀌지 않기 때문에 잘 맞는 케이스
const CustomNavLink = React.memo(({ to, label }) => (
<NavLink className={({ isActive }) => (isActive ? `${css.active}` : '')} to={to}>
{label}
</NavLink>
))
const CustomIconLink = React.memo(({ to, icon }) => (
<NavLink className={({ isActive }) => (isActive ? `${css.active}` : '')} to={to}>
<i className={`bi ${icon}`}></i>
</NavLink>
))
export default React.memo(Header)
✅ useMemo
값(value)을 메모이제이션
📌 특징
- 비싼 계산 결과를 기억해둠
- 의존성 배열([a, b])이 바뀔 때만 다시 계산
- 렌더링마다 계산되는 걸 막고 싶을 때 사용
const filteredList = useMemo(() => {
return list.filter(item => item.active)
}, [list])
✅ useCallback
함수(function)를 메모이제이션
📌 특징
- 함수의 재생성을 막음
- 컴포넌트가 리렌더링될 때마다 함수가 새로 만들어지는 걸 방지
- React.memo된 컴포넌트에 함수를 props로 전달할 때 필수
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
Hook / 함수 | 무엇을 캐싱? | 언제 사용? | 주요 용도 |
React.memo | 컴포넌트 | props가 바뀌지 않을 때 리렌더 막기 | 컴포넌트 최적화 |
useMemo | 값 | 무거운 연산 결과 재사용 | 계산 비용 절감 |
useCallback | 함수 | 함수가 불필요하게 재생성되는 것 방지 | 자식에게 props로 함수 줄 때 |
헤더 반응형 디자인
isOn 값에 따라 body 스크롤 잠그기 / 풀기
- isOn === true일 때 👉 body의 스크롤을 막음
- isOn === false일 때 👉 스크롤 다시 가능하게 설정
// Header.jsx
useEffect(() => {
// 모바일 메뉴가 열려있을 때 body 스크롤 방지
if (isOn) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [isOn])
이미지 최적화
이미지가 로드되는 동안 사용자한테 비어있는 상태나 로딩 상태, 스켈레톤 UI 등을 보여주고, 이미지가 다 로드되면 그때 바꿔주는 방식
import React, { useState, useEffect } from 'react'
const LazyImage = ({ src, alt, className, width, height }) => {
const [imageSrc, setImageSrc] = useState(
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzIDIiPjwvc3ZnPg=='
)
const [imageLoaded, setImageLoaded] = useState(false)
useEffect(() => {
const img = new Image()
img.src = src
img.onload = () => {
setImageSrc(src)
setImageLoaded(true)
}
}, [src])
return (
<img
src={imageSrc}
alt={alt}
className={`${className} ${!imageLoaded ? 'image-loading' : 'image-loaded'}`}
width={width}
height={height}
loading="lazy"
/>
)
}
export default LazyImage
🎯 최적화 포인트
포인트 | 효과 |
로딩 전 빈 이미지 | 초기 로딩 상태 제어 가능 |
loading="lazy" | 브라우저 레벨 lazy loading (최적화) |
JS 로드 체크 후 이미지 교체 | 완전 로드된 이후에만 보여주기 때문에 플리커, 깜빡임 방지 |
클래스 변경 | CSS로 자연스러운 전환, 애니메이션 등 가능 |
쇼핑몰 Hero 페이지 만들기
Swiper를 이용해서 슬라이더 구현
- main.jsx
// Swiper
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
- HeroSlider.jsx
import React, { useEffect, useState } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'
import { Pagination } from 'swiper/modules'
import css from './HeroSlider.module.css'
import { Link } from 'react-router-dom'
const HeroSlider = () => {
return (
<section>
<h2 hidden>Banner Event</h2>
<Swiper pagination={{ clickable: true }} modules={[Pagination]} className={css.mainSlider}>
<SwiperSlide>
<div className={css.imgWrap}>
<img src="" alt="" />
</div>
<div className={css.textWrap}>
<p className={css.title}>제목</p>
<p className={css.desc}>설명</p>
<Link to={'/'} className={css.more}>
자세히보기
</Link>
</div>
</SwiperSlide>
</Swiper>
</section>
)
}
export default HeroSlider
- HeroSlider.module.css
브라우저 창 크기가 변해도 이미지 비율 유지
.imgWrap {
padding-top: 40%;
position: relative;
overflow: hidden;
border-radius: var(--fs16);
}
.imgWrap img {
position: absolute;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.textWrap {
position: absolute;
width: 50%;
top: 50%;
transform: translateY(-50%);
left: var(--fs33);
color: white;
line-height: 1.4;
}
.textWrap > * {
margin-bottom: var(--fs12);
}
.title {
font-size: var(--fs20);
}
.more {
display: inline-block;
border: 1px solid; /* color에 의해 border-color 상속됨 */
transition: 0.3s;
padding: var(--fs8) var(--fs12);
border-radius: var(--fs8);
}
.more:hover {
background-color: var(--dark-colors-white-dark);
color: var(--dark-colors-black-dark);
border-color: var(--light-colors-accent-light);
}
json-server 세팅 후 데이터 통신
- 설치
npm i -D json-server concurrently
- package.json
"scripts": {
"watch:json-server": "json-server db.json --port 3000",
"watch:vite": "vite",
"dev": "concurrently npm:watch:*",
"build": "vite build",
"lint": "eslint . --config eslint.config.js --fix",
"format": "prettier --write \"src/**/*.{js,jsx,css,json}\"",
"preview": "vite preview"
},
- db.json
{
"products": [
{
"id": 1,
"title": "14K/18K 허니 벌집2 헥사곤 심플 반지",
"img": "image1.jpg",
"price": 185000,
"category": "new",
"discount": 5
},
{
"id": 2,
"title": "14K/18K 마닐드 하트 드롭 큐빅 원터치 귀걸이",
"img": "image2.jpg",
"price": 126250,
"category": "top",
"discount": 31
},
{
"id": 3,
"title": "14k 데일리 심플 볼귀걸이 시리즈",
"img": "image3.jpg",
"price": 30000,
"category": "top",
"discount": 45
},
// ... 생략
],
"banners": [
{
"id": 1,
"img": "/public/img/Img_bg1.jpg",
"title": "이벤트 제목1",
"description": "1배너 리스트 영역의 배너 상세 내용이 들어갑니다.",
"link": "/shop"
},
{
"id": 2,
"img": "/public/img/Img_bg2.jpg",
"title": "이벤트 제목2",
"description": "22배너 리스트 영역의 배너 상세 내용이 들어갑니다.",
"link": "/shop"
},
{
"id": 3,
"img": "/public/img/Img_bg3.jpg",
"title": "이벤트 제목2",
"description": "333배너 리스트 영역의 배너 상세 내용이 들어갑니다.",
"link": "/shop"
}
]
}
- HeroSlider.jsx
axios를 통한 데이터 통신 수행
const [banner, setBanner] = useState([])
useEffect(() => {
const fetchBanner = async () => {
const res = await axios.get('http://localhost:3000/banners/')
setBanner(res.data)
}
fetchBanner()
}, [])
리액트에서 fetch보다 axios를 더 잘 사용하는 이유
1️⃣ 자동으로 JSON 변환
- axios는 서버에서 데이터를 가져오면, 기본적으로 자동으로 JSON 데이터를 변환해줌. 즉, response.data로 바로 접근 가능
- fetch는 데이터가 응답으로 오면 response.json()을 호출하여 명시적으로 JSON 변환을 해줘야 함
2️⃣ 다양한 요청 처리 및 편리한 기능
- axios는 GET, POST, PUT, DELETE와 같은 HTTP 요청 메서드 외에도 요청 취소, 타임아웃 설정, 헤더 설정 등 다양한 기능을 제공
- axios는 기본적으로 요청과 응답 인터셉터를 설정하여 요청/응답을 가로채는 등의 고급 기능도 쉽게 구현 가능
3️⃣ then()과 catch()의 체이닝 문제
- fetch는 .then()과 .catch()를 통해 체이닝을 할 때, 응답 데이터를 처리하기 위해 추가적인 .then() 호출이 필요할 수 있음
비동기 함수 수행 원리
👉🏻 콜스택 (Call Stack)
- 콜스택은 함수 호출이 LIFO 방식으로 처리되는 곳
- 동기 함수는 하나씩 순차적으로 실행되며 콜스택에 쌓임
👉🏻 비동기 함수 처리 (Background & Task Queue)
- 비동기 함수(예: setTimeout, fetch, axios 등)는 백그라운드에서 실행
- 비동기 함수가 완료되면 Task Queue에 해당 함수가 FIFO 방식으로 쌓임
- 콜스택이 비어지면, 그때 Task Queue에서 이벤트 루프가 비동기 함수들을 콜스택으로 밀어넣어 실행
비동기 처리와 async/await
- async/await는 비동기 코드를 동기적으로 작성할 수 있게 도와줌
- await는 Promise가 해결될 때까지 기다리며, 콜스택을 비우고 비동기 작업이 완료될 때까지 콜스택에서 다른 작업을 처리할 수 있도록 함
try-catch로 에러 처리
- 외부 API 호출 등과 같은 비동기 작업은 실패할 수 있기 때문에 try-catch를 사용하여 에러를 처리해야 함
- 비동기 함수 내에서 await을 사용할 때, 해당 코드가 실패하면 catch로 예외를 처리할 수 있음
useEffect(() => {
const fetchBanner = async () => {
try {
const res = await axios.get('http://localhost:3000/banners/')
setBanner(res.data)
} catch (err) {
console.log('error', err)
}
}
fetchBanner()
}, [])
API 호출 모듈화
- bannerApi.js
export const getBannerData = async () => {
try {
const res = await axios.get(`${BASE_URL}`)
return res.data
} catch (err) {
console.log('error', err)
// throw err // 호출한 쪽에서 에러 처리하게 할 수도 있음
}
}
- HeroSlider.jsx
import React, { useEffect, useState } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'
import { Pagination } from 'swiper/modules'
import css from './HeroSlider.module.css'
import { Link } from 'react-router-dom'
import { getBannerData } from '../api/bannerApi'
const HeroSlider = () => {
const [banner, setBanner] = useState([])
useEffect(() => {
const fetchBanner = async () => {
try {
const data = await getBannerData()
setBanner(data)
} catch (err) {
console.log('error', err)
}
}
fetchBanner()
}, [])
return (
<section>
<h2 hidden>Banner Event</h2>
<Swiper pagination={{ clickable: true }} modules={[Pagination]} className={css.mainSlider}>
{banner.map(item => (
<SwiperSlide key={item.id}>
<div className={css.imgWrap}>
<img src={item.img} alt={item.title} />
</div>
<div className={css.textWrap}>
<p className={css.title}>{item.title}</p>
<p className={css.desc}>{item.description}</p>
<Link to={item.link} className={css.more}>
자세히보기
</Link>
</div>
</SwiperSlide>
))}
</Swiper>
</section>
)
}
export default HeroSlider
'LG 유플러스 유레카 SW > React' 카테고리의 다른 글
[#56] React 쇼핑몰 - 상품 상세 페이지 (1) | 2025.04.17 |
---|---|
[#55] React 쇼핑몰 - 스켈레톤 UI + 반응형 메인 리스트 페이지 (0) | 2025.04.16 |
[#53] React 쇼핑몰 - 헤더 반응형으로 만들어보기 (1) | 2025.04.14 |
[#52] React Router (2) | 2025.04.11 |
[#51] 가위바위보 게임 (feat. React) (0) | 2025.04.10 |