개발 곡뢀/React

[nextJS] μ„±λŠ₯ μ΅œμ ν™” ν•˜κΈ°(lighthouse, λΉŒλ“œ μš©λŸ‰ κ°œμ„ )

YJzero 2023. 7. 25. 22:03

πŸ’ͺλ©”μΈνŽ˜μ΄μ§€ μ΅œμ ν™”ν•˜κΈ°

ν˜„μž¬ μ§„ν–‰ 쀑인 ν”„λ‘œμ νŠΈ λ©”μΈνŽ˜μ΄μ§€ μž‘μ—…μ„ ν•˜κ³  λ‚˜μ„œ μ„±λŠ₯ 검사λ₯Ό ν•˜λ‹ˆ 이미지가 λ§Žμ•„μ„œ κ·ΈλŸ°μ§€ μ’‹μ§€ μ•Šμ€ 점수λ₯Ό λ°›κ³  μžˆμ—ˆλ‹€.

μ΅œμ ν™” μ „ lighthouse 검사 κ²°κ³Ό

LCPμ—μ„œ μ’‹μ§€ μ•Šμ€ 점수λ₯Ό λ°›κ³  μžˆμ—ˆκ³ , TBT와 SI μ—­μ‹œ λΉ λ₯Έ μ†λ„λŠ” μ•„λ‹ˆμ—ˆλ‹€. 이에 λŒ€ν•΄ κ°œμ„ μ„ μœ„ν•œ μ΅œμ ν™” μž‘μ—…μ„ μ§„ν–‰ν•΄ λ³΄μ•˜λ‹€.

 

lighthouse μΈ‘μ • ν•­λͺ©

FCP(First Contentful Paint): λΈŒλΌμš°μ €κ°€ 첫 번째 DOM의 μ½˜ν…μΈ λ₯Ό λ Œλ”λ§ ν•˜λŠ” 데 κ±Έλ¦¬λŠ” μ‹œκ°„.
LCP(Largest Contenful Paint): λ·°ν¬νŠΈμ—μ„œ κ°€μž₯ 큰 μ½˜ν…μΈ  μš”μ†Œκ°€ 화면에 λ Œλ”λ§ 될 λ•ŒκΉŒμ§€ κ±Έλ¦¬λŠ” μ‹œκ°„
TBT(Total Blocking Time): μ›Ή νŽ˜μ΄μ§€κ°€ μ‚¬μš©μžμ˜ μž…λ ₯에 μ‘λ‹΅ν•˜μ§€ λͺ»ν•˜λ„둝 μ°¨λ‹¨λœ μ‹œκ°„
SI(Speed Index): μ½˜ν…μΈ κ°€ μ‹œκ°μ μœΌλ‘œ ν‘œμ‹œλ˜λŠ” λ°κΉŒμ§€ κ±Έλ¦¬λŠ” μ‹œκ°„
Cumulative Layout Shift: 느린 λ‘œλ”©, 비동기 λ™μž‘, 동적 DOM λ³€κ²½ λ“±μœΌλ‘œ λ ˆμ΄μ•„μ›ƒμ΄ λ³€ν•˜λŠ” μ‹œκ°„ 

 

 

1. Dynamic import μ μš©ν•˜κΈ°

전체 νŽ˜μ΄μ§€μ— 영ν–₯을 λ―ΈμΉ˜λŠ” _app.tsxλΆ€ν„° μ΅œμ ν™”λ₯Ό μ§„ν–‰ν•˜μ˜€λ‹€.

ν˜„μž¬ _appμ—μ„œλŠ” 미둜그인 μ‹œ 둜그인이 ν•„μš”ν•œ νŽ˜μ΄μ§€μ— μ ‘κ·Όν•˜λŠ” 경우 μ—΄λ¦¬λŠ” 둜그인 λͺ¨λ‹¬ μ»΄ν¬λ„ŒνŠΈμ™€ λ‘œλ”© μ‹œ λ‚˜νƒ€λ‚˜λŠ” λ‘œλ”© μ»΄ν¬λ„ŒνŠΈλ₯Ό import ν•˜κ³  μžˆλŠ” μƒνƒœμ˜€λ‹€. ν•΄λ‹Ή μ»΄ν¬λ„ŒνŠΈλ“€μ€ μ²« λ Œλ”λ§μ— ν•„μš”ν•œ μš”μ†Œκ°€ μ•„λ‹ˆκΈ° λ•Œλ¬Έμ— dynamic importλ₯Ό μ μš©ν•˜μ˜€λ‹€.

Dynamic Importλž€?

ES2020의 λ¬Έλ²•μœΌλ‘œ λͺ¨λ“ˆμ„ λΉŒλ“œ νƒ€μž„μ΄ μ•„λ‹Œ λŸ°νƒ€μž„μ— λΆˆλŸ¬μ˜€λ„λ‘ ν•œλ‹€. λ”°λΌμ„œ λ²ˆλ“€ νŒŒμΌμ„ λΆ„λ¦¬ν•˜λ©΄μ„œ 첫 λ Œλ”λ§μ˜ μ„±λŠ₯을 κ°œμ„ ν•  수 μžˆλ‹€. κ·Έλž˜μ„œ 보톡 첫 λ Œλ”λ§μ— 보여지지 μ•ŠλŠ” μ»΄ν¬λ„ŒνŠΈ(λͺ¨λ‹¬ λ“±)에 μ μš©ν•  수 μžˆλ‹€.

κΈ°μ‘΄μ—λŠ” promiseλ₯Ό μ‚¬μš©ν•˜μ—¬ 체이닝을 톡해 μ½”λ“œλ₯Ό μž‘μ„±ν•΄μ•Ό ν•˜μ§€λ§Œ nextμ—μ„œλŠ” dynamic importλ₯Ό μœ„ν•œ next/dynamic λͺ¨λ“ˆμ„ μ§€μ›ν•˜μ—¬ κ°„νŽΈν•˜κ²Œ μ‚¬μš©ν•  수 μžˆλ‹€.  

 

  • dynamic import 적용 μ „
//이전
import LoginModal from '@/components/auth/LoginModal';
import { LoadingSpinner } from '@/components/common/LoadingSpinner';

 

  • dynamic import 적용 ν›„
//이후
import dynamic from 'next/dynamic';
const LoginModal = dynamic(() => import('@/components/auth/LoginModal'));
const LoadingSpinner = dynamic(
  () => import('@/components/common/LoadingSpinner'),
);

 

λ˜ν•œ λ©”μΈνŽ˜μ΄μ§€μ— ν•΄λ‹Ήν•˜λŠ” page/index.tsxμ—μ„œ ν•˜λ‹¨μ— μœ„μΉ˜ν•˜μ—¬ λ·°ν¬νŠΈμ— λ°”λ‘œ λ“€μ–΄μ˜€μ§€ μ•ŠλŠ” μ»΄ν¬λ„ŒνŠΈλ₯Ό dynamic import ν•˜μ˜€λ‹€.

const RecordFeedList = dynamic(
  () => import('@/components/main/mainRecordFeed/RecordFeedList'),
);

 

μ΄λŸ¬ν•œ μž‘μ—…μ„ 톡해 λ²ˆλ“€ μ‚¬μ΄μ¦ˆλ₯Ό 쀄일 수 μžˆμ—ˆλ‹€!

메인 νŽ˜μ΄μ§€λŠ” μš©λŸ‰μ΄ 절반으둜 μ€„μ—ˆκ³  _appκ³Ό λ‹€λ₯Έ νŽ˜μ΄μ§€λ“€μ˜ 경우 First Load JS μ‹œκ°„μ΄ κ°œμ„ λœ 것을 λ³Ό 수 μžˆμ—ˆλ‹€.

μ™Όμͺ½: μ΅œμ ν™” μ „ / 였λ₯Έμͺ½: μ΅œμ ν™” ν›„

 

2. 이미지 lazy loading

μ„±λŠ₯ μ΅œμ ν™”λ₯Ό μœ„ν•΄ λ·°ν¬νŠΈμ— λ“€μ–΄μ˜€λŠ” μ΄λ―Έμ§€λ§Œ 뢈러올 수 μžˆλ„λ‘ lazy loading을 μ„€μ •ν•  수 μžˆλ‹€.

img νƒœκ·Έμ—μ„œ lazy loading 속성을 μ œκ³΅ν•˜κΈ° λ•Œλ¬Έμ— next/Imageμ—μ„œλ„ 적용이 κ°€λŠ₯ν•˜λ‹€. ν•˜μ§€λ§Œ λ”°λ‘œ loading 섀정을 ν•˜μ§€ μ•Šμ„ 경우 κΈ°λ³Έ κ°’μœΌλ‘œ lazyκ°€ λ“€μ–΄κ°€κΈ° λ•Œλ¬Έμ— λ”°λ‘œ μ§€μ •ν•΄μ£Όμ§€ μ•Šμ•˜λ‹€.

 

λ°˜λŒ€λ‘œ λ·°ν¬νŠΈμ— λ°”λ‘œ λ“€μ–΄μ˜€λŠ” 메인 이미지인 경우 lazy loading을 ν•˜λ©΄ μŠ€μΊλ„ˆμ—μ„œ 이미지λ₯Ό 숨기기 λ•Œλ¬Έμ— λΈŒλΌμš°μ €μ—μ„œ 이미지λ₯Ό 늦게 μš”μ²­ν•˜κΈ° λœλ‹€. κ·Έλ ‡κ²Œ 되면 LCPκ°€ λŠ¦μ–΄μ§€κΈ° λ•Œλ¬Έμ— λ·°ν¬νŠΈμ—μ„œ λ°”λ‘œ λ³΄μ΄λŠ” μ΄λ―Έμ§€μ˜ 경우 next/Image의 priority 섀정을 톡해 lazy loading을 ν•΄μ œν•˜κ³  μš°μ„ μˆœμœ„λ₯Ό 높일 수 μžˆλ‹€.

 

μ°Έκ³ )

https://yceffort.kr/2022/06/optimize-LCP#lcp-%EB%A5%BC-lazy-load-%ED%95%98%EC%A7%80-%EB%A7%90-%EA%B2%83

next/Image의 priority

true인 경우 이미지가 높은 μš°μ„ μˆœμœ„λ‘œ κ°„μ£Όλ˜μ–΄ 미리 λ‘œλ”©λ˜λ©° μ§€μ—°λ‘œλ”©μ΄ μžλ™μœΌλ‘œ λΉ„ν™œμ„±ν™”λœλ‹€. 뷰포트 μ‚¬μ΄μ¦ˆμ— 따라 LCP에 ν•΄λ‹Ήν•˜λŠ” 이미지가 λ‹€λ₯Ό 수 μžˆμœΌλ―€λ‘œ μ—¬λŸ¬ 이미지에 μ μš©ν•  수 μžˆλ„λ‘ κ³ λ €λ˜μ–΄μ•Ό ν•œλ‹€.

 

        <Image
          src='/images/main/main-banner.svg'
          width={390}
          height={274}
          alt='메인 μ•ˆλ‚΄ λ°°λ„ˆ'
          className='w-full'
          priority
        />

 

 

3. μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” 폰트 μ œκ±°ν•˜κΈ°

κΈ°λ³Έ ν°νŠΈλŠ” Noto_Sans_KR둜 next/font/googleμ—μ„œ κ°€μ Έμ˜¬ 수 μžˆμ§€λ§Œ λ‹€λ₯Έ νŠΉμ • ν°νŠΈλŠ” next/font/localμ—μ„œ 가져와야 ν–ˆλ‹€. κ·ΈλŸ¬λ‚˜ ν•΄λ‹Ή 폰트λ₯Ό woff2 파일둜 μ‚¬μš©ν–ˆμŒμ—λ„ λΆˆκ΅¬ν•˜κ³  μš©λŸ‰μ΄ 생각보닀 μ»€μ„œ λ°›μ•„μ˜€λŠ” μ‹œκ°„μ΄ κΈΈμ—ˆλ‹€. ν•˜μ§€λ§Œ μ‚¬μš©ν•˜λŠ” νŽ˜μ΄μ§€λŠ” λ§Žμ§€ μ•Šμ•˜κΈ° λ•Œλ¬Έμ— 기쑴에 _appμ—μ„œ μ „μ—­μœΌλ‘œ import ν•˜λ˜ 폰트λ₯Ό μ‚¬μš©ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈμ—μ„œ import ν•˜λ„λ‘ μˆ˜μ •ν•˜μ˜€λ‹€.

 

 

κ·ΈλŸ¬λ‚˜ μ»΄ν¬λ„ŒνŠΈλ§ˆλ‹€ 폰트 μ„€μ •ν•˜λ©΄ 쀑볡 μ½”λ“œκ°€ 생기고 이후에 μœ μ§€λ³΄μˆ˜λ„ μ–΄λ €μšΈ 것 κ°™μ•„μ„œ ν•˜λ‚˜μ˜ layout μ»΄ν¬λ„ŒνŠΈλ₯Ό λ§Œλ“€μ–΄μ„œ κ΄€λ¦¬ν•˜κΈ°λ‘œ ν•˜μ˜€λ‹€.

 

  • λ ˆμ΄μ•„μ›ƒ μ»΄ν¬λ„ŒνŠΈ 생성
import localFont from 'next/font/local';
import { ReactNode } from 'react';

const prettyNight = localFont({
  src: '../../public/fonts/Cafe24Oneprettynight-v2.0.woff2',
  weight: '400',
  variable: '--prettyNight',
});

function PrettyNightFontLayout({ children }: { children: ReactNode }) {
  return <div className={`${prettyNight.variable} h-full`}>{children}</div>;
}

export default PrettyNightFontLayout;

 

  • λ ˆμ΄μ•„μ›ƒ μ»΄ν¬λ„ŒνŠΈ 적용
OnboardingPage.getLayout = function getLayout(page: ReactElement) {
  return <PrettyNightFontLayout>{page}</PrettyNightFontLayout>;
};

 

이 μž‘μ—…μ„ 톡해 ν•΄λ‹Ή 폰트λ₯Ό μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” νŽ˜μ΄μ§€μ—μ„œλŠ” λ‘œλ“œν•˜μ§€ μ•ŠμŒμœΌλ‘œμ¨ LCP 속도λ₯Ό κ°œμ„ ν•  수 μžˆμ—ˆλ‹€.

 


μ΅œμ ν™” ν›„ μ„±λŠ₯ 검사

μ™Όμͺ½: μ΅œμ ν™” 이전 / 였λ₯Έμͺ½: μ΅œμ ν™” 이후

TBT와 SIκ°€ λΉ λ¦„μœΌλ‘œ μ„±λŠ₯이 ν–₯μƒλ˜μ—ˆκ³  LCPλŠ” μ΅œμ μ€ μ•„λ‹ˆμ§€λ§Œ μ•½ 5초 정도 쀄일 수 μžˆμ—ˆλ‹€!!

 

100μ κΉŒμ§„ μ•„λ‹ˆμ§€λ§Œ 점수λ₯Ό μ–΄λŠ 정도 μ•ˆμ •μ μΈ λ²”μœ„κΉŒμ§€ 올릴 수 μžˆμ–΄μ„œ 맀우 κΈ°μ˜λ‹€!! πŸŽ‰πŸŽ‰

κ³„μ†ν•΄μ„œ 더 μ΅œμ ν™”ν•  수 μžˆλŠ” 방법이 μžˆλŠ”μ§€ 곡뢀해 봐야겠닀πŸ’ͺ