1
개요
최적화 기법
- 이미지 사이즈 최적화
- 너무 큰 사이즈의 이미지는 네트워크 트래픽이 증가해 서비스 로딩이 오래걸림.
- 너무 작은 사이즈의 이미지는 화질이 저하되어 서비스 이용이 불편해짐
- 코드 분할
- 첫페이지 진입 시 당장 사용하지 않는 코드는 코드분할을 통해 따로 로드
- 텍스트 압축
- HTML, CSS ,JS 등을 다운로드 전에 서버에서 미리 압축.
- 병목 코드 최적화
- 너무 느리게 다운로드 되거나 느리게 실행되는 특정 자바스크립트 코드를 찾아 최적화
최적화 툴
- 크롬 개발자 도구
- Network 패널
- Performance 패널
- Lighthouse 패널
- webpack-bundle-analyzer
최적화
Lighthouse 이용한 페이지 검사 및 개선
모든 최적화 포인트를 외우고 있으면 좋겠지만 현실적으로 불가능하므로 Lighthouse의 도움을 받는다. 검사 지표를 보고 Lighthouse가 제공한 Opportunity와 Diagnotics를 적용한다
개선 이전 검사결과

web vitals
- FCP(First): DOM 컨텐츠의 첫번째 부분을 렌더링 하는데 걸리는 시간
- Speed Index(SI): 페이지의 점진적 완성 과정의 속도. 시간에 따른 시각적 완성도 곡선 아래의 면적을 계산
- LCP(Largest): 페이지 내 가장 큰 이미지나 텍스트 요소가 렌더링되기까지 걸리는 시간.
- TTI(Time To Interactive): 사용자가 페이지와 상호 작용이 가능한 시점까지 걸리는 시간
- TBT(Total Blocking Time): 페이지가 사용자 입력에 응답하지 않도록 차단된 시간을 총합한 지표. FCP,TTI 사이의 시간동안 발생
- CLS(Cumulative Layout Shift): 페이지 로드과정에서 발생한 예기치 못한 레이아웃 이동
이미지 사이즈 최적화
비효율적인 이미지 분석
Diagnotics 항목을 보면 다음과 같은 비효율적인 이미지가 로드되는 상황이 나타난다.


병목 코드 최적화
Perfomance 패널에서 메인스레드의 작업을 상세하게 살펴보고 느린 작업이 무엇인지 확인. (Start profiling and reload page) 버튼 클릭.
CPU 차트
어느 타이밍에 어떤작업이 진행되는지 알 수 있다.

- 노란색: 자바스크립트 실행
- 보라색: 렌더링/레이아웃
- 초록색: 페인팅
- 회색: 기타
- 빨간색: 병목지점. 특정 작업이 메인 스레드를 오랫동안 잡고 있을때
Network 타임라인
서비스 로드 과정에서 네트워크 요청을 시간 순서에 따라 보여줌. 오래걸리는 작업은 클릭하여 선택후 하단 탭의 summary를 통해 문제 파악 가능.

(여기서 JS 파일의 크기는 1.0MB)
플레임 차트
어떤 작업이 오래 걸리는지 파악 가능.

하단 탭
- Summary: 선택 영역에서 발생한 작업 시간의 총합과 각 작업이 차지하는 비중
- Bottom up: 가장 최하위에 있는 작업부터 상위 작업까지 보여줌
- Call Tree: 가장 상위에 있는 작업부터 하위 작업 순으로 보여줌
- Event Log: 브라우저 이벤트(ex. Loading, Experience Scripting, Rendering, Paint)
분석 방법
병목이 걸리는 Task를 선택후 하단 탭의 Bottom up 섹션을 통해 어떤 작업의 Self time이 가장 큰지 확인. 이 경우 removeSpecialCharacter의 작업이 병목을 유발함.

function removeSpecialCharacter(str) {
const removeCharacters = ['#', '_', '*', '~', '&', ';', '!', '[', ']', '`', '>', '\n', '=', '-']
let _str = str
let i = 0,
j = 0
for (i = 0; i < removeCharacters.length; i++) {
j = 0
while (j < _str.length) {
if (_str[j] === removeCharacters[i]) {
_str = _str.substring(0, j).concat(_str.substring(j + 1))
continue
}
j++
}
}
return _str
}
실제로 보면 replace 함수를 사용하면 될것을 for 루프를 돌고있다.
코드 분할
분할 기준
- Network 타임라인를 통해 유난히 크고 다운로드가 오래걸리는 자바스크립트 파일을 확인.
- webpack-bundle-analyzer를 통해 해당 자바스크립트 파일이 어떤 코드로 이루어져 있는지 확인.

3. refractor 패키지의 용량이 너무 크다. 직접 설치한 패키지가 아니라면 lock.json파일을 확인하여 어느 패키지가 의존성을 가지고 있는지 확인. -> react-syntax-highlighter
4. react-syntax-highlighter 패키지는 Code Block 컴포넌트에서만 사용되니 굳이 처음 진입시 로드할 필요는 없다. 따라서 lazy load

5. 결과확인

Page별 분할
- 기존 Route설정파일에서 일반 import (List->Detail 페이지로 이동)

- Page별 lazy import (List->Detail 페이지로 이동

- 차이점
- List 페이지 진입 시 진입 속도가 빨라졌다(8ms->2ms)
- Detail 페이지 진입 시 또다시 js 파일을 로드하게 된다.(분할을 했기 때문에)
텍스트 압축
서버에서 gzip으로 압축해보내주자!
2
개요
최적화 기법
- CSS 애니메이션 최적화
- 컴포넌트 지연 로딩
- 이전에는 페이지를 분할했지만 이제는 단일 컴포넌트를 분할하여 컴포넌트가 쓰이는 순간에 로드
- 컴포넌트 사전 로딩
- 분할된 코드를 preload하여 필요한 시점보다 먼저 다운로드.
- 이미지 사전로딩
- 이미지를 필요한 시점보다 먼저 다운로드하고 필요할때 로드
최적화 툴
- 크롬 개발자 도구
- Network 패널
- Performance 패널
- webpack-bundle-analyzer
최적화
애니메이션 최적화
Jank 현상
웹페이지나 앱에서 발생하는 시각적 끊김 현상. 브라우저가 정상적으로 60FPS로 화면을 그리지 못하기때문에 발생. Jank 현상은 주로 reflow, repaint로 인해 발생함.
- reflow: 요소의 크기나 위치가 변경되어 레이아웃을 다시 계산하는 과정. width, height, margin, padding, position 변경 시 발생
- repaint: 요소의 시각적 스타일만 변경되어 다시 그리는 과정. color, background-color, visibility, outline 변경 시 발생.
Jank 현상을 피하는 방법
- transform, opacity, will-change 같은 속성을 사용. 해당 Element를 벌도의 레이어로 분리하고 작업을 GPU에 위임함으로써 레이아웃 단계와 페인트 단계를 건너뜀. (=하드웨어 가속)
하드웨어 가속
현재 width 로 애니메이션을 조절하다보니 리플로우 작업(layout 시작~paint끝)이 1frame(1/60초)동안 끝나질 않는다.

이를 transform으로 변환해보면 결과는 다음과 같다.

컴포넌트 지연 로딩
서비스 첫화면부터 필요하지 않는 컴포넌트의 경우 지연로딩을 통해 chunk를 분리해준다. 여기서는 import한 Modal 컴포넌트의 경우 코드 분할해준다.
※ 컴포넌트를 lazy load 할때에는 suspense를 감싸주자.
컴포넌트 사전 로딩
모달을 지연로딩시키면 모달을 열었을때 네트워크를 통해 모달 코드를 새로 로드해야했다. 그러니 당연하게도 다음과 같이 모달이 뜨기전까지 지연이 발생한다.(click event ~ evaluate script)

이러한 지연을 해결하기 위해 렌더링이 완료된 직후 여유가 있을때 Modal 컴포넌트를 가져온다.

function App() {
const [showModal,setShowModal] = useState(false)
const [ImageModal, setImageModal] = useState(null);
useEffect(() => {
import('./components/ImageModal').then(res=>{
// res.default는 함수형이기 때문에 setState 함수가 state 업데이트 함수로 오해함
setImageModal(()=>res.default)
})
}, []);
return (
<div className="App">
<Header />
<InfoTable />
<ButtonModal onClick={() => { setShowModal(true) }}>올림픽 사진 보기</ButtonModal>
<SurveyChart />
<Footer />
{showModal ? <Suspense fallback={null}><ImageModal closeModal={() => { setShowModal(false) }} /></Suspense> : null}
</div>
)
}
이미지 사전 로딩
모달을 열때 이미지가 제때 뜨지 않는다. 이로인해 CLS 이슈가 발생한다.

Modal을 로드할때 Modal 내 필요한 이미지들을 모두 로드한다.
function App() {
const [showModal,setShowModal] = useState(false)
const [ImageModal, setImageModal] = useState(null);
useEffect(() => {
import('./components/ImageModal').then(res=>{
// res.default는 함수형이기 때문에 setState 함수가 state 업데이트 함수로 오해함
setImageModal(()=>res.default)
images.forEach(image=>{
const img = new Image()
img.src=image.original
})
})
}, []);
return (
<div className="App">
<Header />
<InfoTable />
<ButtonModal onClick={() => { setShowModal(true) }}>올림픽 사진 보기</ButtonModal>
<SurveyChart />
<Footer />
{showModal ? <Suspense fallback={null}><ImageModal closeModal={() => { setShowModal(false) }} /></Suspense> : null}
</div>
)
}
3
개요
최적화 기법
- 이미지 지연 로딩
- 첫화면에 당장 필요하지 않은 이미지가 로드 되지않도록 지연
- 이미지 사이즈 최적화
- 이전에는 CDN에서 로드된 이미지의 크기를 수정했지만 이번에는 정적 이미지를 최적화
- 폰트 최적화
- 커스텀 폰트 최적화
- 캐시 최적화
- 불필요한 CSS 제거
- 불필요한 CSS 코드를 제거하여 파일사이즈 줄이기
최적화 툴
- 크롬 개발자 도구
- Coverage 패널: 웹페이지를 렌더링하는 과정에서 어떤 코드가 실행되었는지, 얼마나 실행되었는지(비율) 확인.
- Squoosh: 이미지 압축 도구
- PurgeCSS: 사용하지 않는 CSS 제거
최적화
이미지 지연로딩
이미지 영역이 화면에 보이는 순간 또는 그 직전에 이미지를 로드.
- Scroll Event 감지 방식: scroll이 이동할때마다 콜백함수가 실행되기 때문에 내부에 무거운 로직이 들어가면 브라우저 메인스레드에 부담이감.
- IntersectionObserver 방식: 스크롤 할때마다 콜백함수가 실행되는 것이 아님.
function Card(props) {
const img = useRef(null)
useEffect(()=>{
const intersectionObserver = new IntersectionObserver((e)=>{
if(e[0].isIntersecting){
console.log(e[0].target)
const imgComponent= img.current.querySelector('img')
imgComponent.src = imgComponent.dataset.src
intersectionObserver.unobserve(e[0].target)
}
},{
root: document,
rootMargin: '0px',
threshold:1
})
intersectionObserver.observe(img.current)
return ()=>intersectionObserver.disconnect()
},[])
return (
<div ref={img} className="Card text-center">
<img data-src={props.image} />
<div className="p-5 font-semibold text-gray-700 text-xl md:text-lg lg:text-xl keep-all">
{props.children}
</div>
</div>
)
}
※ img 컴포넌트에 대한 display:none->display:block 의 변경으로 위 문제를 해결 할 수 없다. 무조건 src를 넣는 시점을 제어해야한다.
이미지 사이즈 최적화
Card 내 이미지의 크기가 커 로딩속도가 너무 느리다.
- 사이즈: PNG > JPG > WebP
- 화질: PNG = WebP > JPG
- 호환성: PNG = JPG > WebP
JPG/PNG 포맷의 이미지를 WebP 포맷으로 변환하여 고화질, 저용량 이미지로 최적화.
WebP를 지원하지 못하는 브라우저의 경우 JPG/PNG로 로드하게 해줘야한다. 이를 대비하기 위해 <picture> 를 이용한다.(https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_picture)
# 뷰포트 크기에 따라 구분
<picture>
<source media="(min-width:650px)" srcset="img_pink_flowers.jpg">
<source media="(min-width:450px)" srcset="img_white_flowers.jpg">
<img src="img_orange_flowers.jpg" alt="Flowers" style="width:auto;">
</picture>
# 이미지 포맷에 따라 구분
<picture>
<source type="image/avif" srcset="img_pink_flowers.avif">
<source type="image/webp" srcset="img_pink_flowers.webp">
<img src="img_pink_flowers.jpg" alt="Flowers" style="width:auto;">
</picture>
동영상 최적화
동영상 압축 웹 사이트에서 동영상 너비,높이,Bitrate, Audio 유무 등을 조정하여 메모리 최적화.
단, 화질이 저화 되므로 영상이 중요하지 않다면 이를 보완하기위해 CSS 상으로 blur등의 필터를 씌운다
폰트 최적화
페이지 로드 후 폰트가 적용되어 텍스트 스타일이 변하는 모습이 관측된다.

브라우저에 따라 FOUT(텍스트 출력 후 폰트스타일 적용)/FOIT(폰트스타일 준비 완료 후 텍스트 출력) 방식이 다르다
폰트 적용 시점 제어
@font-face를 통해 font를 등록할때 font-display에 폰트 표현방식 설정
@font-face{
font-family: BMYEONGSUNG;
src: url('./assets/fonts/BMYEONGSUNG.ttf');
font-display: fallback;
}
- auto: 브라우저 기본 동작
- block: FOIT (timeout=3s)
- swap: FOUT
- fallback: FOIT (timeout=0.1s) // 3초 후에도 불러오지 못한경우 기본 폰트로 유지
- optional: FOIT // 네트워크 상태에 따라 기본 폰트로 유지할지 결정
※ block방식을 선택할 경우 3초후 갑자기 텍스트가 나타날 수 있으니 fade-in 애니메이션을 함께 넣어주면 좋다
폰트 사이즈 감소
- 압축률
- EOT < TTF/OTF < WOFF < WOFF2
- Transfonter라는 서비스에서 확장자 변환.
- 우선순위에 순서로 src 설정
-
@font-face{ font-family: BMYEONGSUNG; src: url('./assets/fonts/BMYEONGSUNG.woff2') format('woff2'), url('./assets/fonts/BMYEONGSUNG.woff') format('woff'), url('./assets/fonts/BMYEONGSUNG.ttf') format('ttf'), font-display: fallback; } - 서브셋 폰트 사용
- 특정 영역에서 특정 문자의 폰트 정보만 가지고 있으면 된다. 이를 서브셋 폰트라고 한다.
- Transfonter 서비스에서 원하는 문자를 넣고 폰트를 생성하면 서브셋 폰트를 생성할 수 있다.
@font-face{ font-family: BMYEONGSUNG; src: url('./assets/fonts/subset-BMYEONGSUNG.woff2') format('woff2'), url('./assets/fonts/subset-BMYEONGSUNG.woff') format('woff'), url('./assets/fonts/subset-BMYEONGSUNG.ttf') format('ttf'), font-display: fallback; } - Data-URI
- transfonter 서비스에서 Base64로 encoding에서 이 결과물을 src에 넣으면 폰트 로드시간을 획기적으로 줄일 수 있다.
캐시최적화
Lighthouse Diagnostics를 보면 다음과 같은 진단이 있다.

실제로 저중에 하나의 결과를 보면 Response Header에 Cache-Control헤더가 없다

- 웹 캐시 종류
- 메모리 캐시: RAM에 저장하는 방식
- 디스크 캐시: 파일 형태로 디스크에 저장하는 방식
캐시 최적화
Lighthouse Diagnostics 섹션에서 Serve static assets with an efficient cache policy항목을 펼쳐 캐시가 적용되지 않은 파일을 확인. 해당 파일들은 실제로 응답 헤더에 cache-control 속성이 없다.

- memory cache: RAM에 저장하는 방식. 브라우저를 끄지않고 페이지 이동, 새로고침 했을때 많이 나타남.
- disk cache: 파일 형태로 디스크에 저장하는 방식. 브라우저를 껐다 켰을때 나타남.
어떤 캐시를 사용할지는 브라우저가 특정 알고리즘에 의해 알아서 처리. (사용자 제어 불가)
Cache-Control
FE 서버에서 설정해줘야한다!
- cache 전략
- no-cache: 캐시 써도 되는데, 매번 서버에 확인. revalidation에서 304 응답받으면 cache 사용.
- no-store: 캐시 사용 금지. 매번 새로 받아올것
- public: CDN, 프록시 서버에서도 캐시 가능
- private: 사용자 브라우저에만 캐시 허용
적절한 캐시 유효시간
- HTML: no-cache. 항상 최신버전의 웹 서비스를 제공하기 위함.
- JS,CSS, Font: public, max-age=31536000. 해시값을 가지고 있어 파일이 변경되면 자동으로 캐시가 풀림
- default: no-store
불필요한 CSS 제거
Lighthouse를 통해 상황을 파악하고 Coverage 패널을 통해 얼마나 불필요한 코드가 들어있는지 상세하게 알 수 있음.



하지만 위의 경우 모두 tailwind css라이브러리에서 추가한 것이다.
이를 해결하기위해 purgeCSS를 사용한다. (하지만 요즘 tailwind는 이런 문제를 자체적으로 해결해준다. v4 기준) 텍스트를 추출하여 사용여부를 판단하는 원리로 작동한다.
npm install -D purgecss
npx purgecss --css ./build/static/css/*.css --output ./build/static/css/ --content ./build/index.html ./build/static/js/*.js

4
개요
최적화 기법
- 이미지 지연 로딩
- IntersectionObserver 대신 react-lazy-load-image-component 라이브러리 사용
- 레이아웃 이동 피하기
- 리덕스 렌더링 최적화
최적화 툴
- 크롬 개발자 도구
- Network 패널
- Performance 패널
- Lighthouse 패널
- React Developer Tools(Profiler)
최적화
레이아웃 이동 피하기
요소의 사이즈를 미리 예측하여 해당 사이즈만큼 공간을 확보
리덕스 리렌더링 최적화
React Developer Tools를 설치후 Highlight updates when components render옵션 활성화. 불필요한 리렌더링 발생현황을 파악.
Font 로딩 최적화 학습 자료
개요
이 문서는 웹 폰트 로딩 최적화와 관련된 핵심 개념들을 정리한 학습 자료입니다. 최근 커밋에서 적용된 font preload 최적화를 바탕으로 작성되었습니다.
1. <link> 태그란?
<link> 태그는 HTML 문서와 외부 리소스 간의 관계를 정의하는 HTML 요소입니다.
주요 특징
- 빈 요소(void element): 닫는 태그가 없음
- 외부 리소스 연결: CSS, 폰트, 아이콘 등의 외부 파일을 문서에 연결
- 메타데이터 제공: 브라우저에게 리소스의 성격과 처리 방법을 알려줌
기본 문법
<link rel="관계" href="파일경로" type="MIME타입" />
2. <link> 태그의 주요 속성과 속성값
rel 속성 (relationship)
문서와 연결된 리소스 간의 관계를 정의합니다.
속성값설명예시
| stylesheet | CSS 스타일시트 연결 | <link rel="stylesheet" href="style.css"> |
| preload | 리소스를 미리 로드 (높은 우선순위) | <link rel="preload" href="font.woff2" as="font"> |
| preconnect | 외부 도메인과 미리 연결 | |
| dns-prefetch | DNS 조회를 미리 수행 | <link rel="dns-prefetch" href="//example.com"> |
| icon | 파비콘 설정 | <link rel="icon" href="favicon.ico"> |
| canonical | 정규 URL 지정 |
🤔 헷갈리기 쉬운 3가지: canonical, dns-prefetch, preconnect 상세 비교
1. rel="canonical" - SEO를 위한 정규 URL 지정
목적: 검색엔진에게 "이 페이지의 공식적인 URL은 이것이다"라고 알려주는 것
사용 상황:
- 동일한 콘텐츠가 여러 URL로 접근 가능한 경우
- URL 파라미터로 인한 중복 페이지 문제 해결
- SEO 최적화
<!-- 예시 1: 파라미터가 있는 URL의 정규화 -->
<link rel="canonical" href="https://shop.com/product?id=123" />
<!-- 예시 2: 모바일/데스크톱 버전 통합 -->
<link rel="canonical" href="https://www.example.com/article" />
<!-- 예시 3: HTTPS 버전을 정규 URL로 지정 -->
<link rel="canonical" href="https://example.com/page" />
실제 효과:
❌ canonical 없을 때:
Google 검색 결과에 중복 페이지들이 모두 나타남
- https://shop.com/product?id=123
- https://shop.com/product?id=123&ref=google
- https://shop.com/product?id=123&utm_source=ad
✅ canonical 있을 때:
Google이 정규 URL만 검색 결과에 표시
- https://shop.com/product?id=123 (정규 URL만 노출)
2. rel="dns-prefetch" - DNS 조회 미리 실행
목적: 외부 도메인의 DNS 조회를 미리 수행하여 나중에 해당 도메인의 리소스를 빠르게 로드
동작 과정:
- 브라우저가 dns-prefetch를 발견
- 백그라운드에서 DNS 조회 실행 (IP 주소 확인)
- 나중에 해당 도메인의 리소스가 필요할 때 DNS 조회 시간 절약
<!-- 예시: 외부 CDN에서 폰트를 로드할 예정 -->
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//cdn.jsdelivr.net" />
<!-- 나중에 실제 리소스 로드 시 DNS 조회 시간 절약됨 -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter" />
타이밍 비교:
❌ dns-prefetch 없을 때:
1. 발견
2. fonts.googleapis.com DNS 조회 시작 (20-100ms)
3. DNS 조회 완료 후 연결 시작
4. 리소스 다운로드
✅ dns-prefetch 있을 때:
1. 발견
2. 백그라운드에서 DNS 조회 미리 완료
3. 나중에 발견
4. DNS 조회 생략하고 즉시 연결 시작 → 더 빠름!
3. rel="preconnect" - 연결 과정 전체를 미리 실행
목적: DNS 조회 + TCP 연결 + TLS 핸드셰이크까지 모든 연결 과정을 미리 완료
동작 과정:
- DNS 조회 (IP 주소 확인)
- TCP 연결 설정
- TLS/SSL 핸드셰이크 (HTTPS의 경우)
- 연결 준비 완료 상태로 대기
<!-- 예시: Google Fonts에서 폰트를 확실히 로드할 예정 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- 나중에 실제 리소스 로드 시 연결 과정 전체가 생략됨 -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter" />
dns-prefetch vs preconnect 비교:
dns-prefetch (가벼움):
1. DNS 조회만 미리 실행 (20-100ms 절약)
2. 연결이 확실하지 않을 때 사용
3. 여러 도메인에 대해 사용해도 부담 적음
preconnect (강력함):
1. DNS + TCP + TLS 모두 미리 실행 (100-500ms 절약)
2. 연결이 확실할 때만 사용 (리소스 소모가 더 큼)
3. 중요한 2-3개 도메인에만 사용 권장
4. 실제 사용 시나리오별 선택 가이드
🎯 시나리오 1: Google Fonts 사용
<!-- ✅ 권장: 확실히 폰트를 로드할 예정이므로 preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter" />
🎯 시나리오 2: 조건부로 외부 리소스 로드 가능
<!-- ✅ 권장: 확실하지 않으므로 dns-prefetch -->
<link rel="dns-prefetch" href="//cdn.example.com" />
<link rel="dns-prefetch" href="//analytics.google.com" />
<script>
// 특정 조건에서만 로드
if (userConsent) {
loadScript('https://analytics.google.com/analytics.js')
}
</script>
🎯 시나리오 3: 여러 URL로 접근 가능한 상품 페이지
<!-- ✅ 권장: SEO를 위한 canonical -->
<!-- 현재 URL: /product?id=123&color=red&size=large -->
<link rel="canonical" href="/product?id=123" />
5. 성능 측정으로 보는 실제 효과
DNS 조회 시간 측정:
// 성능 측정 코드
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.name.includes('fonts.googleapis.com')) {
console.log(
'DNS 조회 시간:',
entry.domainLookupEnd - entry.domainLookupStart
)
console.log('연결 시간:', entry.connectEnd - entry.connectStart)
console.log('전체 시간:', entry.responseEnd - entry.startTime)
}
}
})
observer.observe({ entryTypes: ['navigation', 'resource'] })
실제 측정 결과 예시:
dns-prefetch 없음: DNS 조회 50ms + 연결 100ms = 150ms
dns-prefetch 적용: DNS 조회 0ms + 연결 100ms = 100ms (33% 개선)
preconnect 적용: DNS 조회 0ms + 연결 0ms = 0ms (100% 개선)
href 속성
연결할 리소스의 URL 또는 경로를 지정합니다.
<!-- 절대 경로 -->
<link rel="stylesheet" href="https://cdn.example.com/style.css" />
<!-- 상대 경로 -->
<link rel="stylesheet" href="/css/style.css" />
<link rel="stylesheet" href="./style.css" />
type 속성
리소스의 MIME 타입을 명시합니다.
MIME 타입설명예시
| text/css | CSS 파일 | <link rel="stylesheet" type="text/css" href="style.css"> |
| font/woff2 | WOFF2 폰트 | <link rel="preload" type="font/woff2" href="font.woff2"> |
| font/ttf | TrueType 폰트 | <link rel="preload" type="font/ttf" href="font.ttf"> |
as 속성 (preload와 함께 사용)
preload할 리소스의 타입을 지정합니다.
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="image.jpg" as="image" />
<link rel="preload" href="script.js" as="script" />
crossorigin 속성
교차 출처 요청 시 CORS 설정을 지정합니다.
<!-- 자격 증명 없이 요청 -->
<link rel="preload" href="font.woff2" as="font" crossorigin="anonymous" />
<!-- 자격 증명과 함께 요청 -->
<link rel="preload" href="font.woff2" as="font" crossorigin="use-credentials" />
기타 유용한 속성들
media 속성
특정 미디어 조건에서만 리소스를 로드합니다.
<link rel="stylesheet" href="print.css" media="print" />
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)" />
sizes 속성
아이콘의 크기를 지정합니다.
<link rel="icon" href="icon-32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="icon-192.png" sizes="192x192" type="image/png" />
3. @font-face와 <link>의 관계
@font-face 란?
CSS에서 웹 폰트를 정의하고 사용할 수 있게 해주는 CSS at-rule입니다.
@font-face {
font-family: 'MyCustomFont';
src:
url('/fonts/MyCustomFont.woff2') format('woff2'),
url('/fonts/MyCustomFont.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
<link preload>와 @font-face의 협력
1. 문제 상황
/* @font-face만 사용할 경우 */
@font-face {
font-family: 'MyFont';
src: url('/fonts/MyFont.woff2');
}
.title {
font-family: 'MyFont', sans-serif; /* 이 시점에서야 폰트 다운로드 시작 */
}
문제점: 폰트가 실제로 사용되는 순간에야 다운로드가 시작됨 → FOUT/FOIT 발생
2. 해결책: preload + @font-face
<!-- HTML: 페이지 로드 즉시 폰트 다운로드 시작 -->
<link
rel="preload"
href="/fonts/MyFont.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous" />
/* CSS: 폰트 정의 및 사용 */
@font-face {
font-family: 'MyFont';
src: url('/fonts/MyFont.woff2') format('woff2');
font-display: swap; /* 로딩 중 fallback 폰트 표시 */
}
.title {
font-family: 'MyFont', sans-serif; /* 이미 로드된 폰트 즉시 적용 */
}
3. 실제 프로젝트 적용 예시 (커밋 기준)
<!-- _document.tsx -->
<link
rel="preload"
href="/fonts/HyundaiSansHeadPro-Regular.ttf"
as="font"
type="font/ttf"
crossorigin="anonymous" />
/* globals.css */
@font-face {
font-family: 'HyundaiSansHeadPro';
src: url('/fonts/HyundaiSansHeadPro-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
로딩 타임라인 비교
❌ preload 없이
- HTML 파싱 완료
- CSS 파싱 완료
- 레이아웃 계산
- 폰트가 필요한 요소 발견
- 폰트 다운로드 시작 ← 늦음
- 폰트 적용
✅ preload 사용
- HTML 파싱 시작
- preload 발견 → 폰트 다운로드 시작 ← 빠름
- CSS 파싱 완료
- 레이아웃 계산
- 폰트가 필요한 요소 발견
- 이미 로드된 폰트 즉시 적용
4. @font-face 상세 가이드
기본 구조와 속성
@font-face {
font-family: 'FontName'; /* 필수: 폰트 패밀리 이름 */
src: url('font.woff2'); /* 필수: 폰트 파일 경로 */
font-weight: normal; /* 선택: 폰트 굵기 */
font-style: normal; /* 선택: 폰트 스타일 */
font-display: swap; /* 선택: 로딩 동작 */
unicode-range: U+0000-00FF; /* 선택: 유니코드 범위 */
}
주요 속성 상세 설명
font-family
- 폰트의 이름을 정의
- CSS에서 이 이름으로 폰트를 참조
@font-face {
font-family: 'MyCustomFont';
}
/* 사용 */
.text {
font-family: 'MyCustomFont', Arial, sans-serif;
}
src
폰트 파일의 위치와 형식을 지정합니다.
/* 단일 형식 */
src: url('/fonts/font.woff2') format('woff2');
/* 여러 형식 (fallback) */
src:
url('/fonts/font.woff2') format('woff2'),
url('/fonts/font.woff') format('woff'),
url('/fonts/font.ttf') format('truetype');
/* 로컬 폰트 우선 확인 */
src:
local('Arial Bold'),
local('Arial-Bold'),
url('/fonts/arial-bold.woff2') format('woff2');
font-weight
폰트의 굵기를 지정합니다.
/* 숫자 값 */
font-weight: 400; /* normal */
font-weight: 700; /* bold */
/* 키워드 */
font-weight: normal;
font-weight: bold;
/* 범위 지정 (가변 폰트) */
font-weight: 100 900;
font-style
폰트의 스타일을 지정합니다.
font-style: normal; /* 기본값 */
font-style: italic; /* 이탤릭 */
font-style: oblique; /* 기울어진 */
font-display
폰트 로딩 중 표시 방식을 제어합니다.
값설명사용 사례
| auto | 브라우저 기본 동작 | 기본값 |
| block | 폰트 로드까지 텍스트 숨김 (최대 3초) | 아이콘 폰트 |
| swap | 즉시 fallback 폰트 표시, 로드 후 교체 | 일반 텍스트 (권장) |
| fallback | 100ms 후 fallback 표시, 3초 내 로드 시 교체 | 성능 중시 |
| optional | 100ms 후 fallback 표시, 캐시된 경우만 사용 | 매우 빠른 로딩 필요 |
/* 권장: 즉시 텍스트 표시, 폰트 로드 후 교체 */
@font-face {
font-family: 'MyFont';
src: url('/fonts/MyFont.woff2') format('woff2');
font-display: swap;
}
unicode-range
특정 문자 범위에만 폰트를 적용합니다.
/* 영문만 */
@font-face {
font-family: 'EnglishFont';
src: url('/fonts/english.woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}
/* 한글만 */
@font-face {
font-family: 'KoreanFont';
src: url('/fonts/korean.woff2');
unicode-range: U+AC00-D7AF;
}
고급 패턴
1. 동일 패밀리 내 다양한 weight/style
/* Regular */
@font-face {
font-family: 'MyFont';
src: url('/fonts/MyFont-Regular.woff2');
font-weight: 400;
font-style: normal;
}
/* Bold */
@font-face {
font-family: 'MyFont';
src: url('/fonts/MyFont-Bold.woff2');
font-weight: 700;
font-style: normal;
}
/* Italic */
@font-face {
font-family: 'MyFont';
src: url('/fonts/MyFont-Italic.woff2');
font-weight: 400;
font-style: italic;
}
/* 사용 */
.text {
font-family: 'MyFont', sans-serif;
}
.text.bold {
font-weight: 700; /* Bold 폰트 자동 선택 */
}
.text.italic {
font-style: italic; /* Italic 폰트 자동 선택 */
}
2. 가변 폰트 (Variable Font)
@font-face {
font-family: 'VariableFont';
src: url('/fonts/variable-font.woff2') format('woff2-variations');
font-weight: 100 900;
font-stretch: 75% 125%;
}
/* 사용 */
.text {
font-family: 'VariableFont';
font-weight: 350; /* 정확한 굵기 */
font-stretch: 110%; /* 폭 조절 */
}
5. <link> 태그의 위치: <head> vs <body>
<head> 내 <link> (표준이자 권장사항)
장점
- 빠른 로딩: HTML 파싱 초기에 발견되어 즉시 다운로드 시작
- 렌더링 블로킹 방지: CSS는 렌더링을 블로킹하지만, 빠른 발견으로 지연 최소화
- 표준 준수: HTML 명세에 따른 올바른 사용법
- SEO 친화적: 검색엔진이 메타데이터를 빠르게 인식
<!DOCTYPE html>
<html>
<head>
<!-- ✅ 권장: head 내 위치 -->
<link rel="stylesheet" href="/css/critical.css" />
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
</head>
<body>
<h1>Content</h1>
</body>
</html>
로딩 타임라인 (head 내 link)
0ms HTML 파싱 시작
1ms <link> 발견 → CSS/폰트 다운로드 시작
... HTML 계속 파싱
100ms <body> 파싱
150ms CSS 다운로드 완료
200ms 렌더링 시작 (스타일 적용됨)
<body> 내 <link> (특수한 경우)
사용 가능한 케이스
- Progressive Enhancement: 점진적 개선
- 조건부 로딩: 특정 상황에서만 필요한 리소스
- 지연 로딩: 중요하지 않은 스타일의 지연 로딩
<body>
<header>메인 콘텐츠</header>
<!-- 📱 모바일에서만 필요한 스타일 -->
<script>
if (window.innerWidth < 768) {
document.head.appendChild(
Object.assign(document.createElement('link'), {
rel: 'stylesheet',
href: '/css/mobile-only.css',
})
)
}
</script>
<!-- 🎨 중요하지 않은 장식적 스타일 -->
<section class="decorative-section">
<link rel="stylesheet" href="/css/decorations.css" />
<!-- 이 섹션의 스타일만 로드 -->
</section>
</body>
로딩 타임라인 (body 내 link)
0ms HTML 파싱 시작
100ms <body> 파싱
150ms <link> 발견 → CSS 다운로드 시작 (늦음!)
200ms 첫 렌더링 (스타일 없음)
250ms CSS 다운로드 완료
300ms 리렌더링 (스타일 적용) → FOUC 발생!
성능 비교 및 권장사항
✅ 권장: Critical CSS는 head에
<head>
<!-- 필수 스타일 -->
<link rel="stylesheet" href="/css/critical.css" />
<!-- 폰트 preload -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />
<!-- Above-the-fold 콘텐츠에 필요한 모든 리소스 -->
</head>
💡 고급 패턴: 하이브리드 접근
<head>
<!-- Critical CSS 인라인 -->
<style>
/* 핵심 스타일만 인라인으로 포함 */
body {
font-family: system-ui;
}
.header {
background: #000;
}
</style>
<!-- 폰트 preload -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />
</head>
<body>
<header class="header">즉시 스타일 적용됨</header>
<!-- 나머지 CSS는 비동기 로드 -->
<link
rel="preload"
href="/css/main.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="/css/main.css" /></noscript>
</body>
6. 최적화 Best Practices
1. 폰트 로딩 최적화 체크리스트
✅ 필수 사항
- 중요한 폰트는 <link rel="preload"> 사용
- @font-face에 font-display: swap 설정
- crossorigin="anonymous" 속성 추가
- WOFF2 형식 우선 사용
✅ 권장 사항
- 폰트 서브셋팅으로 파일 크기 줄이기
- unicode-range로 필요한 문자만 로드
- 시스템 폰트를 fallback으로 설정
- 폰트 로딩 전략 설정
2. 실제 구현 예시 (프로젝트 기준)
<!-- _document.tsx -->
<head>
<!-- 1. 핵심 폰트 preload -->
<link
rel="preload"
href="/fonts/HyundaiSansTextPro-Regular.ttf"
as="font"
type="font/ttf"
crossorigin="anonymous" />
<link
rel="preload"
href="/fonts/HyundaiSansTextPro-Bold.ttf"
as="font"
type="font/ttf"
crossorigin="anonymous" />
</head>
/* globals.css */
/* 2. @font-face 정의 */
@font-face {
font-family: 'HyundaiSansTextPro';
src: url('/fonts/HyundaiSansTextPro-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap; /* 3. FOUC 방지 */
}
@font-face {
font-family: 'HyundaiSansTextPro';
src: url('/fonts/HyundaiSansTextPro-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
font-display: swap;
}
/* 4. 시스템 폰트 fallback과 함께 사용 */
body {
font-family:
'HyundaiSansTextPro',
-apple-system,
BlinkMacSystemFont,
sans-serif;
}
3. 성능 측정 및 디버깅
Chrome DevTools에서 확인하기
- Network 탭: 폰트 로딩 타이밍 확인
- Performance 탭: 렌더링 블로킹 확인
- Lighthouse: 폰트 최적화 제안 확인
- Coverage 탭: 사용되지 않는 폰트 확인
중요 메트릭
- FCP (First Contentful Paint): 첫 콘텐츠 표시 시간
- LCP (Largest Contentful Paint): 가장 큰 콘텐츠 표시 시간
- FOUT/FOIT: 폰트 교체/숨김 현상
7. 주의사항 및 트러블슈팅
흔한 실수들
❌ 잘못된 예시
<!-- 1. crossorigin 누락 -->
<link rel="preload" href="/fonts/font.woff2" as="font" />
<!-- 2. type 속성 누락 -->
<link rel="preload" href="/fonts/font.woff2" as="font" crossorigin />
<!-- 3. @font-face와 경로 불일치 -->
<link rel="preload" href="/fonts/font.woff2" as="font" crossorigin />
@font-face {
font-family: 'MyFont';
src: url('/fonts/font.ttf'); /* ❌ 다른 파일! */
}
✅ 올바른 예시
<!-- 1. 모든 필수 속성 포함 -->
<link
rel="preload"
href="/fonts/font.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous" />
@font-face {
font-family: 'MyFont';
src: url('/fonts/font.woff2') format('woff2'); /* ✅ 동일한 파일 */
font-display: swap;
}
디버깅 팁
1. 폰트가 로드되지 않을 때
// 폰트 로딩 상태 확인
document.fonts.ready.then(() => {
console.log('모든 폰트 로드 완료')
})
// 특정 폰트 로딩 확인
if (document.fonts.check('16px MyFont')) {
console.log('MyFont 사용 가능')
} else {
console.log('MyFont 아직 로드되지 않음')
}
2. 네트워크 오류 확인
- CORS 에러: crossorigin 속성 확인
- 404 에러: 파일 경로 확인
- MIME 타입 에러: 서버 설정 확인
요약
Font 로딩 최적화의 핵심은 <link rel="preload">와 @font-face의 조합입니다:
- <link rel="preload">: 페이지 로드 즉시 폰트 다운로드 시작
- @font-face: 폰트 정의 및 fallback 설정
- font-display: swap: 로딩 중 텍스트 숨김 방지
- crossorigin="anonymous": CORS 문제 해결
이를 통해 FOUT/FOIT를 방지하고 사용자 경험을 크게 개선할 수 있습니다.
웹 폰트 로딩 문제: FOUT vs FOIT
FOUT (Flash of Unstyled Text)
"스타일이 적용되지 않은 텍스트의 깜빡임"
동작 과정:
- 페이지 로드 시 즉시 fallback 폰트로 텍스트 표시
- 웹 폰트 다운로드 완료 후 웹 폰트로 교체
- 폰트 교체 시 깜빡임/레이아웃 변화 발생
FOIT (Flash of Invisible Text)
"보이지 않는 텍스트의 깜빡임"
동작 과정:
- 페이지 로드 시 텍스트를 숨김 (투명 처리)
- 웹 폰트 다운로드 완료까지 빈 공간만 표시
- 폰트 로드 후 갑자기 텍스트 나타남



우선도 구분은 다음 API를 사용하여 우선도가 높은 상태변화와 낮은 상태변화를 구분한다. 


