원문: https://www.developerway.com/posts/initial-load-performance
핵심 웹 바이탈, 성능 개발 도구, 초기 로드 성능이 무엇인지, 어떤 지표가 이를 측정하는지, 캐시 제어 및 다양한 네트워킹 조건이 성능에 어떤 영향을 미치는지 살펴보세요.
최근 AI 기반 코드 생성이 급성장하면서 리액트 코드 작성의 중요성이 줄어들고 있습니다. 이제 누구나, 무엇이든 리액트로 앱을 작성할 수 있습니다. 하지만 코드 작성은 항상 퍼즐의 한 부분일 뿐입니다. 우리는 여전히 앱을 어딘가에 배포하고, 사용자에게 보여주고, 견고하게 만들고, 빠르게 만드는 등 수많은 작업을 수행해야 합니다. 인공지능은 이러한 작업을 대신할 수 없습니다. 적어도 아직은 말이죠.
오늘은 앱을 빠르게 만드는 데 집중해 봅시다. 그러기 위해서는 잠시 리액트를 벗어나야 합니다. 왜냐하면 무언가를 빠르게 만들기 전에 먼저 "빠름"이 무엇인지, 어떻게 측정해야 하는지, 무엇이 "빠름"에 영향을 미칠 수 있는지 알아야 하기 때문입니다.
스포일러 주의: 이 글에서는 스터디 프로젝트 외에는 리액트에 대해 언급하지 않습니다. 오늘은 성능 도구 사용 방법, 핵심 웹 바이탈 소개, 크롬 성능 패널, 초기 로드 성능의 정의, 측정 지표, 캐시 제어 및 다양한 네트워킹 조건이 성능에 미치는 영향 등 기본적인 내용에 대해 설명합니다.
브라우저를 열고 즐겨찾는 웹사이트로 이동하려고 하면 어떻게 되나요? 주소창에 "http://www.my-website.com"를 입력하면 브라우저가 서버에 GET 요청을 보내고 HTML 페이지를 받습니다.
이 작업을 수행하는 데 걸리는 시간을 "첫 번째 바이트까지의 시간"(TTFB)이라고 하며, 이는 요청을 보낸 후 그에 대한 결과가 처음으로 도착하기까지 걸리는 시간입니다. HTML을 수신한 후 브라우저는 가능한 한 빨리 이 HTML을 사용 가능한 웹사이트로 변환해야 합니다.
"중요 경로"는 사용자에게 보여줄 수 있는 최소한의 가장 중요한 콘텐츠이며, 이를 화면에 렌더링하는 것으로 시작됩니다.
중요 경로에 정확히 무엇이 포함되어야 하는지는 복잡한 문제입니다. 이상적으로는 사용자가 완전한 경험을 바로 접할 수 있도록 모든 것이 포함되어야 합니다. 하지만 "중요" 경로이기 때문에 되도록 빨라야 하므로 아무것도 포함하지 않는 것도 좋습니다. 두 가지를 동시에 달성하는 것은 불가능하므로 타협이 필요합니다.
타협안은 다음과 같습니다. 브라우저가 "중요 경로"를 구축하려면 최소한 해당 유형의 리소스가 반드시 필요하다고 가정합니다.
브라우저는 서버로부터 초기 요청에서 첫 번째 파일(HTML)을 받습니다. 브라우저는 이를 파싱하기 시작하고, 파싱하는 동안 "중요 경로"를 완성하는 데 필요한 CSS 및 JS 파일에 대한 링크를 추출합니다. 그런 다음 서버에 요청을 전송하고 다운로드될 때까지 기다렸다가 처리하고 이 모든 것을 결합한 후 마지막에 화면에 "중요 경로" 픽셀을 그립니다.
브라우저는 이러한 중요한 리소스 없이는 초기 렌더링을 완료할 수 없으므로 이러한 리소스를 "렌더링 차단 리소스"라고 합니다. 물론 모든 CSS 및 JS 리소스가 렌더링 차단 리소스는 아닙니다. 일반적으로는 아래와 같습니다.
<link>
태그를 통한 대부분의 CSS.async
또는 deferred
가 아닌 <head>
태그 내부의 자바스크립트 리소스."중요 경로"를 렌더링하는 전체 프로세스는 대략 다음과 같습니다.
<head>
태그에서 CSS 및 JS 리소스에 대한 링크를 추출합니다.이 시점을 First Paint(FP)라고 합니다. 사용자가 화면에서 무언가를 볼 수 있는 첫 번째 기회입니다. 표시 여부는 서버가 전송한 HTML에 따라 달라집니다. 텍스트나 이미지와 같이 의미 있는 내용이 포함되어 있다면 이 시점이 First Contentful Paint(FCP)가 발생한 시점이기도 합니다. HTML이 빈 div에 불과하다면 FCP는 나중에 발생합니다.
First Contentful Paint(FCP) 는 인지된 초기 로드를 측정하기 때문에 가장 중요한 성능 지표 중 하나입니다. 기본적으로 웹사이트가 얼마나 빠른지에 대한 사용자의 첫인상입니다.
이 순간까지 사용자들은 빈 화면을 바라보며 손톱을 물어뜯고 있을 뿐입니다. 구글에 따르면 좋은 FCP 수치는 1.8초 미만입니다. 그 이후에는 사용자가 웹사이트가 제공하는 콘텐츠에 흥미를 잃고 이탈하기 시작할 수 있습니다.
하지만 FCP는 완벽하지 않습니다. 웹사이트가 스피너나 로딩 화면으로 로딩을 시작하는 경우 FCP 지표가 이를 나타냅니다. 하지만 사용자가 단지 멋진 로딩 화면을 확인하기 위해 웹사이트를 방문했을 가능성은 거의 없습니다. 대부분의 경우 콘텐츠에 접근하려는 것이 목적입니다.
이를 위해 브라우저는 시작한 작업을 완료해야 합니다. 나머지 논블로킹 자바스크립트를 기다렸다가 실행하고, 자바스크립트에서 발생한 변경 사항을 화면의 DOM에 적용하고, 이미지를 다운로드하고, 사용자 경험을 다듬는 등의 작업을 수행합니다.
이 프로세스 중 어딘가에서 Largest Contentful Paint(LCP) 시간이 발생합니다. FCP와 같은 첫 번째 요소 대신 페이지의 주요 콘텐츠 영역, 즉 뷰포트에 표시되는 가장 큰 텍스트, 이미지 또는 동영상을 나타냅니다. 구글에 따르면 이 수치는 이상적으로는 2.5초 미만이어야 합니다. 그 이상이면 사용자는 웹사이트가 느리다고 생각할 것입니다.
이러한 모든 지표는 페이지의 사용자 경험을 나타내는 일련의 지표인 구글 웹 바이탈의 일부입니다. LCP는 사용자 경험의 다양한 부분을 나타내는 세 가지 지표인 핵심 웹 바이탈 중 하나입니다. LCP는 로딩 성능 을 담당합니다.
이러한 지표은 라이트하우스로 측정할 수 있습니다. 라이트하우스는 크롬 개발자도구에 통합된 구글 성능 측정 도구로 셸 스크립트, 웹 인터페이스 또는 노드 모듈을 통해 실행할 수도 있습니다. 노드 모듈 형태로 사용하면 빌드 내에서 실행하여 프로덕션에 적용되기 전에 회귀를 감지할 수 있습니다. 로컬 디버깅 및 테스트에는 통합 개발 도구 버전을 사용하세요. 웹 버전은 경쟁사의 성능을 확인할 수 있습니다.
위의 내용은 모두 프로세스에 대한 매우 간략하고 간단한 설명입니다. 하지만 이미 많은 약어와 이론으로 머릿속이 복잡해집니다. 개인적으로 이런 글을 읽는 것은 아무 소용이 없습니다. 직접 눈으로 보고 손으로 만져보지 않으면 금방 잊어버리기 때문이죠.
이 특정 주제의 경우 개념을 완전히 이해하는 가장 쉬운 방법은 거의 실제와 같은 페이지에서 다양한 시나리오를 시뮬레이션하고 결과가 어떻게 달라지는지 확인하는 것입니다. 그러니 더 많은 이론을 공부하기 전에 정확히 그렇게 해봅시다(그 외에도 훨씬 더 많은 것이 있습니다!).
원한다면 아래의 모든 시뮬레이션을 자체 프로젝트에서 수행할 수 있으며, 결과는 거의 동일할 것입니다. 그러나 좀 더 통제되고 단순화된 환경을 원한다면 이 글을 위해 준비한 스터디 프로젝트를 사용하는 것이 좋습니다. 여기 레포지토리에서 확인할 수 있습니다.
모든 의존성을 설치하는 것으로 시작합니다.
npm install
프로젝트를 빌드합니다.
npm run build
그리고 서버를 실행합니다.
npm run start
"http://localhost:3000"에 접속하면 멋진 대시보드 페이지가 표시됩니다.
크롬에서 분석하려는 웹사이트를 열고 크롬 개발자 도구를 엽니다. 거기에서 "성능" 및 "라이트하우스" 패널을 찾아 서로 가깝게 이동합니다. 두 패널이 모두 필요합니다.
또한 이 글의 다른 작업을 수행하기 전에 "캐시 사용 안 함(Disable cache)" 확인란이 활성화되어 있는지 확인하세요. 이 체크박스는 맨 위에 있는 네트워크 패널에 있어야 합니다.
이는 웹사이트에 처음 방문하고 아직 브라우저에 캐시된 리소스가 없는 첫 번째 방문자를 모방하기 위한 것입니다.
지금 라이트하우스 패널을 엽니다. 몇 가지 설정과 "페이지 로드 분석(Analyze page load)" 버튼이 표시됩니다.
이 섹션에서는 페이지의 초기 로딩에 대한 자세한 분석을 실행하는 "탐색(Navigation)" 모드에 관심이 있습니다. 보고서에는 다음과 같은 점수가 표시됩니다.
로컬 성능은 완벽합니다. 모든 것이 항상 "내 컴퓨터에서 작동"하니 놀랄 일도 아닙니다.
다음과 같은 지표도 있습니다.
이 글에 필요한 FCP 및 LCP 값은 바로 상단에 있습니다.
아래에서 점수 향상에 도움이 될 수 있는 제안 목록을 확인할 수 있습니다.
모든 제안을 확장할 수 있으며, 여기에서 더 자세한 정보를 확인할 수 있고 때로는 특정 주제를 설명하는 링크도 찾을 수 있습니다. 모든 제안을 실행할 수 있는 것은 아니지만, 성능에 대해 시작하고 성능을 개선할 수 있는 다양한 사항에 대해 자세히 알아볼 수 있는 놀라운 도구입니다. 이러한 보고서와 관련 링크를 읽는 데만 몇 시간을 소비할 수도 있습니다.
하지만 라이트하우스는 표면적인 정보만 제공하며 느린 네트워크나 CPU 부족과 같은 다양한 시나리오를 시뮬레이션할 수 없습니다. 다만 시간 경과에 따른 성능 변화를 추적할 수 있는 훌륭한 입문용 도구일 뿐입니다. 어떤 일이 일어나고 있는지 더 자세히 알아보려면 "성능" 패널이 필요합니다.
초기 로드시 성능 패널은 다음과 같이 표시됩니다.
여기에는 세 가지 핵심 웹 바이탈 지표이 표시되며, 그 중 하나인 LCP는 느린 네트워크 및 CPU를 시뮬레이션하고 시간 경과에 따른 성능 세부 정보를 기록할 수 있는 기능을 제공합니다.
패널 맨 위에 있는 "스크린샷" 확인란을 찾아 체크한 다음 "기록 및 재로드" 버튼을 클릭하고 웹사이트가 재로드되면 기록을 중지하세요. 이것은 초기 로드 중에 페이지에서 일어나는 일에 대한 자세한 보고서가 될 것입니다.
이 보고서에는 몇 가지 섹션이 있습니다.
맨 위에는 일반적인 "타임라인 개요(timeline overview)" 섹션이 있습니다.
여기에서 웹사이트에서 어떤 일이 일어나고 있다는 것을 확인할 수 있지만 그 이상은 확인할 수 없습니다. 마우스를 가져가면 무슨 일이 일어나고 있는지 스크린샷이 표시되며 특정 범위를 선택하고 확대하여 자세히 살펴볼 수 있습니다.
그 아래에는 네트워크 섹션이 있습니다. 이 섹션을 확장하면 다운로드 중인 모든 외부 리소스와 정확한 시간을 타임라인에서 확인할 수 있습니다. 특정 리소스 위로 마우스를 가져가면 다운로드의 어느 단계에서 얼마나 많은 시간이 소요되었는지에 대한 자세한 정보를 볼 수 있습니다. 빨간색 모서리가 있는 리소스는 차단 리소스를 나타냅니다.
스터디 프로젝트를 진행 중이라면 정확히 같은 그림을 볼 수 있으며, 이 그림은 이전 섹션에 대해 설명한 내용과 일치합니다.
지금 스터디 프로젝트 코드를 열고 dist
폴더를 들여다보면 소스 코드가 이 동작과 일치합니다.
assets
폴더 안에는 index.html
파일과 .css
및 .js
파일이 있습니다.<head>
섹션의 index.html
파일 안에는 CSS 파일을 가리키는 <link>
태그가 있습니다. 아시다시피 <head>
에 있는 CSS 리소스는 렌더링 차단이므로 체크 아웃됩니다.<head>
안에는 assets
폴더 안에 있는 자바스크립트 파일을 가리키는 <script>
태그가 있습니다. 이 태그는 지연되거나 비동기화되지는 않지만 type="module"
을 갖습니다. 이는 자동 연기되므로 이 역시 체크 아웃됩니다. 패널의 자바스크립트 파일은 차단되지 않습니다.추가 연습
작업 중인 프로젝트가 있는 경우 해당 프로젝트의 초기 로드 성능을 기록하고 네트워크 패널을 살펴보세요. 더 많은 리소스가 다운로드된 것을 볼 수 있을 것입니다.
- 렌더링 차단 리소스가 몇 개나 있나요? 모두 필요한가요?
- 프로젝트의 "진입" 지점이 어디인지, 차단 리소스가 -
<head />
섹션에 어떻게 표시되는지 알고 있나요? 변형된npm build
로 프로젝트를 빌드하고 해당 리소스를 검색해 보세요. 힌트.
- 순수 웹팩 기반 프로젝트인 경우
webpack.config.js
파일을 찾아보세요. HTML 항목에 대한 경로 포인트가 안에 있어야 합니다.- Vite를 사용하는 경우 스터디 프로젝트와 동일하게
dist
폴더를 살펴봅니다.- Next.js 앱 라우터를 사용하는 경우
.next/server/app
폴더를 살펴봅니다.
네트워크 섹션 아래에서 프레임(Frames) 및 타이밍(Timing) 섹션을 찾을 수 있습니다.
두 섹션은 꽤나 유용합니다. 타이밍 섹션에서는 앞서 설명한 모든 지표(FP, FCP, LCP)과 아직 설명하지 않은 몇 가지 지표을 볼 수 있습니다. 지표 위로 마우스를 가져가면 정확한 소요 시간을 확인할 수 있습니다. 지표을 클릭하면 맨 아래에 있는 "요약(summary)" 탭이 업데이트되며, 여기에서 해당 지표에 대한 정보와 자세한 내용을 볼 수 있는 링크를 확인할 수 있습니다. 요즘 개발 도구는 사람들을 교육하는 데 중점을 두고 있습니다.
마지막으로 메인 섹션입니다. 타임라인이 기록되는 동안 메인 스레드에서 일어나는 일입니다.
여기서 "HTML 구문 분석" 또는 "레이아웃"과 같은 항목과 소요 시간을 확인할 수 있습니다. 노란색 항목은 자바스크립트와 관련된 것으로, 압축된 자바스크립트가 포함된 프로덕션 빌드를 사용하고 있기 때문에 다소 쓸모가 없습니다. 하지만 이 상태에서도 예를 들어 HTML 파싱 및 레이아웃 그리기와 비교하여 자바스크립트 실행에 걸리는 시간을 대략적으로 파악할 수 있습니다.
특히 네트워크와 메인을 모두 열고 확대하여 전체 화면을 볼 때 성능 분석에 유용합니다.
여기에서 엄청나게 빠른 서버와 빠르고 작은 번들이 있다는 것을 알 수 있습니다. 어떤 네트워크 작업도 병목 현상을 일으키지 않고 시간이 크게 걸리지 않으며, 그 사이에도 브라우저는 조용히 자체 작업을 수행합니다. 따라서 여기서 초기 로딩 속도를 높이려면 그래프에서 가장 긴 작업인 HTML 파싱이 왜 그렇게 느린지 살펴봐야 합니다.
또는 절대적인 수치를 보면 성능 측면에서 볼 때 여기서 아무것도 하지 않아야 합니다. 전체 초기 로드는 200ms 미만이 걸리며 구글의 권장 임계값보다 훨씬 낮습니다 🙂 하지만 저는 이 테스트를 매우 빠른 노트북과 매우 기본적인 서버를 사용하는 로컬 환경(실제 네트워크 비용이 들지 않음)에서 실행하고 있기 때문에 이런 일이 발생하고 있습니다.
실제 상황을 시뮬레이션할 시간입니다.
우선 서버를 좀 더 현실적으로 만들어 봅시다. 현재 첫 번째 "파란색" 단계는 약 50ms가 소요되며, 그 중 40ms는 대기 중입니다.
실제로 서버는 작업을 수행하고, 권한을 확인하고, 작업을 생성하고, 권한을 두 번 더 확인하는 등(레거시 코드가 많아서 세 번 확인하는 것이 손실되었기 때문에), 바쁘게 움직입니다.
스터디 프로젝트의 backend/index.ts
파일로 이동합니다(링크). 그리고 주석 처리된 // await sleep(500)
을 찾아 주석 처리를 해제합니다. 이렇게 하면 서버가 HTML을 반환하기까지 500ms의 지연이 발생하는데, 오래되고 복잡한 서버라고 생각하면 적절합니다.
프로젝트를 다시 빌드하고(npm run build
), 다시 시작하고(npm run start
) 성능 기록을 다시 실행합니다.
타임라인에서 초기 파란색 선을 제외하고는 아무것도 변경된 것이 없습니다. 다른 항목에 비해 엄청나게 길어졌습니다.
이 상황은 성능 최적화를 수행하기 전에 전체 상황을 살펴보고 병목 현상을 파악하는 것이 중요하다는 것을 강조합니다. LCP 값은 ~650밀리초이며, 이 중 ~560밀리초는 초기 HTML을 기다리는 데 소요됩니다. 이 중 리액트 부분은 약 50ms입니다. 어떻게든 절반으로 줄여 25ms로 줄이더라도 전체 그림에서 보면 4%에 불과합니다. 그리고 이를 절반으로 줄이려면 많은 노력이 필요합니다. 훨씬 더 효과적인 전략은 서버에 집중하여 서버가 왜 그렇게 느린지 파악하는 것입니다.
모든 사람이 1기가비트 연결의 세계에 살고 있는 것은 아닙니다. 예를 들어 호주에서는 초당 50메가비트는 초고속 인터넷 연결 중 하나이며, 한 달에 약 90호주달러의 비용이 듭니다. 물론 전 세계 많은 사람들이 사용하고 있는 3G는 아닙니다. 하지만 여전히 유럽에서 초당 1기가비트 또는 10유로짜리 인터넷 요금제에 대해 자랑하는 사람들의 이야기를 들을 때마다 눈물이 납니다.
어쨌든. 좋지 않은 호주의 인터넷을 모방하여 성능 지표이 어떻게 될지 살펴봅시다. 이를 위해 성능 탭(다시 로드 및 기록 근처의 버튼)에서 기존 기록을 지우세요. 네트워크 설정이 있는 패널이 나타납니다.
사용 중인 크롬 버전에 해당 설정이 없는 경우 네트워크 탭에서 동일한 설정을 사용할 수 있습니다.
"네트워크" 드롭다운에서 새 프로필을 추가할 때 다음 숫자를 입력합니다.
이제 드롭다운에서 해당 프로필을 선택하고 성능 녹화를 다시 실행합니다.
무엇이 보이나요? 저에게는 다음과 같이 보입니다.
LCP 값은 거의 변하지 않았으며 640ms에서 700ms로 약간 증가했습니다. 초기 파란색 "서버" 부분에는 변화가 없는데, 이는 최소한의 HTML만 전송하므로 다운로드하는 데 시간이 오래 걸리지 않기 때문입니다.
하지만 다운로드 가능한 리소스와 메인 스레드 간의 관계는 크게 바뀌었습니다.
이제 렌더링 차단 CSS 파일의 영향을 분명히 알 수 있습니다. HTML 구문 분석 작업은 이미 완료되었지만 브라우저는 차갑게 식어 CSS가 다운로드될 때까지 아무것도 그릴 수 없습니다. 브라우저가 HTML을 파싱하는 동안 리소스가 거의 즉시 다운로드된 이전 그림과 비교해 보세요.
그 후 기술적으로 브라우저는 무언가를 그릴 수 있었지만 실제로는 아무 것도 없고, HTML 파일에 빈 div만 전송하고 있습니다. 따라서 브라우저는 자바스크립트 파일을 다운로드하고 실행할 수 있을 때까지 계속 기다립니다.
이 약 60밀리초의 대기 시간이 바로 제가 보고 있는 LCP의 증가입니다.
속도를 더 낮추면 어떻게 되는지 확인해 봅시다. 다운로드 및 업로드에 대해 10mbps/1mbps의 새 네트워크 프로필을 만들고 대기 시간을 40으로 유지한 다음 이름을 "Low Internet bandwidth(낮은 인터넷 대역폭)"으로 지정합니다.
그리고 테스트를 다시 실행합니다.
이제 LCP 값이 거의 500ms로 증가했습니다. 자바스크립트 다운로드는 거의 300ms가 걸립니다. 그리고 상대적으로 HTML 구문 분석 작업과 자바스크립트 실행 작업의 중요성이 줄어들고 있습니다.
추가 연습
자체 프로젝트가 있는 경우 이 테스트를 실행해 보세요.
- 모든 중요 경로 리소스를 다운로드하는 데 시간이 얼마나 걸리나요?
- 모든 자바스크립트 파일을 다운로드하는 데 시간이 얼마나 걸리나요?
- HTML 구문 분석 작업 후 다운로드에 얼마나 많은 공백이 발생하나요?
- 리소스 다운로드에 비해 메인 스레드에서 HTML 구문 분석 및 자바스크립트 실행 작업은 얼마나 큰가요?
- LCP 지표에 어떤 영향을 미치나요?
리소스 표시줄 내부에서 일어나는 일도 매우 흥미롭습니다. 노란색 자바스크립트 막대 위로 마우스를 가져가 보세요. 다음과 같은 내용이 표시됩니다.
여기서 가장 흥미로운 부분은 "요청 전송 및 대기 중(Request sent and waiting)"으로, 약 40ms가 소요됩니다. 나머지 네트워크 리소스 위로 마우스를 가져가면 모두 이 리소스가 표시됩니다. 이것이 바로 40으로 설정한 네트워크 딜레이인 지연 시간(Latency)입니다. 지연 시간 수치에는 여러 가지 요소가 영향을 미칠 수 있습니다. 네트워크 연결 유형도 그중 하나입니다. 예를 들어 평균 3G 연결의 대역폭은 10/1 Mbps이고 지연 시간은 100~300 밀리초입니다.
이를 에뮬레이트하려면 새 네트워크 프로필을 만들고 이름을 "평균 3G(verage 3G)"라고 지정한 다음 "낮은 인터넷 대역폭(Low Internet bandwidth)" 프로필에서 다운로드/업로드 수치를 복사하고 지연 시간을 300ms로 설정합니다.
프로파일링을 다시 실행합니다. 모든 네트워크 리소스의 "요청 전송 및 대기 중"이 약 300ms로 증가해야 합니다. 이렇게 하면 LCP 수치가 더 높아집니다. 저의 경우 1.2초가 증가했습니다.
이제 재미있는 부분을 살펴볼까요? 대역폭을 초고속으로 되돌리고 지연 시간을 낮게 유지하면 어떻게 될까요? 이 설정을 시도해 보겠습니다.
서버가 노르웨이 어딘가에 있지만 클라이언트가 부유한 호주인인 경우 쉽게 발생할 수 있습니다.
이것이 결과입니다.
LCP 수치는 약 960ms입니다. 이전에 시도했던 가장 느린 인터넷 속도보다 더 느립니다! 이 시나리오에서는 번들 크기는 크게 중요하지 않으며 CSS 크기는 전혀 중요하지 않습니다. 둘 다 절반으로 줄여도 LCP 지표는 거의 움직이지 않습니다. 높은 지연 시간이 모든 것을 압도합니다.
이제 아직 구현하지 않았다면 모든 사람이 가장 먼저 구현해야 할 성능 개선 사항을 소개합니다. 바로 "정적 리소스가 항상 CDN을 통해 제공되도록 하는 것"입니다.
CDN은 기본적으로 코드 분할이나 서버 컴포넌트와 같은 더 멋진 것들을 생각하기 전에 프론트엔드 성능과 관련된 모든 것에서 0단계입니다.
모든 CDN(콘텐츠 전송 네트워크)의 주요 목적은 지연 시간을 줄이고 최종 사용자에게 콘텐츠를 최대한 빠르게 전송하는 것입니다. 이를 위해 여러 가지 전략을 구현합니다. 이 글에서 가장 중요한 두 가지 전략은 "분산 서버"와 "캐싱"입니다.
CDN 제공업체는 여러 지리적 위치에 여러 대의 서버를 보유합니다. 이러한 서버는 정적 리소스의 복사본을 저장하고 브라우저가 요청할 때 사용자에게 전송할 수 있습니다. CDN은 기본적으로 원본 서버를 외부의 영향으로부터 보호하고 외부와의 상호 작용을 최소화하는 소프트 레이어입니다. 마치 내성적인 사람을 위한 인공지능 비서와 같아서 실제 사람이 개입할 필요 없이 일반적인 대화를 처리할 수 있습니다.
노르웨이에 서버가 있고 호주에 클라이언트가 있는 위의 예시에서는 이런 그림이 나왔습니다.
CDN이 중간에 있으면 그림이 달라집니다. CDN은 사용자와 더 가까운 곳, 예를 들어 호주 어딘가에 서버를 두게 됩니다. 어느 시점에서 CDN은 원본 서버로부터 정적 리소스의 복사본을 받게 됩니다. 그렇게 되면 호주 또는 호주와 가까운 곳에 있는 모든 사용자는 노르웨이에 있는 서버의 원본이 아닌 해당 복사본을 받게 됩니다.
이를 통해 두 가지 중요한 사항을 달성할 수 있습니다. 첫째, 사용자가 더 이상 원본 서버에 직접 액세스할 필요가 없으므로 원본 서버의 부하가 줄어듭니다. 둘째, 사용자는 더 이상 자바스크립트 파일을 다운로드하기 위해 바다를 건너갈 필요가 없으므로 리소스를 훨씬 더 빠르게 얻을 수 있습니다.
그리고 위 시뮬레이션의 LCP 값은 960ms에서 640ms로 다시 떨어집니다 🎉.
지금까지는 웹사이트에 한 번도 방문한 적이 없는 사람들의 성과인 첫 방문 성과에 대해서만 이야기했습니다. 하지만 웹사이트가 너무 좋아서 첫 방문자 대부분이 단골이 되었으면 좋겠습니다. 또는 적어도 첫 로딩 후 이탈하지 않고 몇 페이지를 탐색한 후 무언가를 구매하면 좋겠죠. 이 경우 일반적으로 브라우저는 CSS 및 JS와 같은 정적 리소스를 캐싱하여 항상 다운로드하지 않고 로컬에 사본을 저장합니다.
이 시나리오에서 성능 그래프와 수치가 어떻게 변하는지 살펴봅시다.
스터디 프로젝트를 다시 엽니다. 개발 도구에서 네트워크를 앞서 생성한 "평균 3G"로 설정하고 지연 시간이 길고 대역폭이 낮은 상태로 설정하여 차이를 바로 확인할 수 있도록 합니다. 그리고 "네트워크 캐시 비활성화(disable network cache)" 확인란이 선택 해제되어 있는지 확인합니다.
먼저 브라우저를 새로고침하여 첫 번째 방문자 상황을 제거합니다. 그런 다음 새로고침하고 성능을 측정합니다.
스터디 프로젝트를 사용하는 경우 최종 결과는 다음과 같이 보일 것이므로 약간 놀랄 것입니다.
네트워크 탭에서 CSS와 자바스크립트 파일은 여전히 매우 눈에 띄며, "평균 3G" 프로필의 지연 시간 설정인 "요청 전송 및 대기 중"에서 두 파일 모두에 대해 약 300ms가 표시됩니다. 결과적으로 LCP가 낮지 않고 브라우저가 차단 CSS를 기다릴 때 300ms의 간격이 생깁니다.
어떻게 된 걸까요? 브라우저가 캐싱을 해야 하는 것 아닌가요?
이제 무슨 일이 일어나고 있는지 이해하기 위해 네트워크 패널을 사용해야 합니다. 패널을 열고 거기서 CSS 파일을 찾습니다. 다음과 같이 보일 것입니다:
여기서 가장 흥미로운 것은 "상태(Status)" 열과 "크기(Size)"입니다. "크기"는 전체 CSS 파일의 크기가 아닙니다. 너무 작습니다. 그리고 "상태"에서는 일반적인 200의 "모두 괜찮음" 상태가 아니라 304의 다른 상태입니다.
여기서 두 가지 질문이 있습니다. 왜 200이 아닌 304인지, 그리고 왜 요청이 아예 전송되지 않았을까요? 캐싱이 작동하지 않은 이유는 무엇인가요?
우선 304 응답입니다. 이 응답은 잘 구성된 서버가 다양한 규칙에 따라 응답이 달라지는 조건부 요청에 대해 보내는 응답입니다. 이와 같은 요청은 브라우저 캐시를 제어하는 데 자주 사용됩니다.
예를 들어 서버가 CSS 파일에 대한 요청을 받으면 파일이 마지막으로 수정된 날짜를 확인할 수 있습니다. 이 날짜가 브라우저 측의 캐시된 파일과 같으면 빈 본문이 있는 304를 반환합니다(그래서 223 B에 불과합니다). 이는 브라우저에 이미 가지고 있는 파일을 다시 사용해도 안전하다는 것을 나타냅니다. 대역폭을 낭비하고 다시 다운로드할 필요가 없습니다.
성능 그림에서 "요청 전송 및 대기 중"이라는 큰 숫자가 표시되는 이유는 브라우저가 서버에 CSS 파일이 최신 상태인지 확인하도록 요청하기 때문입니다. 서버가 "304 수정되지 않음"으로 응답하고 브라우저가 이전에 다운로드한 파일을 다시 사용했기 때문에 "콘텐츠 다운로드"가 0.33ms인 것입니다.
추가 연습
- 스터디 프로젝트에서
dist/assets
폴더로 이동하여 CSS 파일의 이름을 바꿉니다.dist/index.html
파일로 이동하여 이름이 변경된 CSS 파일의 경로를 업데이트합니다.- 네트워크 탭을 열어 이미 열려 있는 페이지를 새로고침하면 새 이름, 200 상태, 적절한 크기(다시 다운로드된)의 CSS 파일이 표시됩니다. 이를 "캐시 버스팅(cache-busting)"이라고 하는데, 브라우저가 캐시했을 수 있는 리소스를 강제로 다시 다운로드하는 방법입니다.
- 페이지를 다시 새로고침하면 304 상태로 돌아가 캐시된 파일을 다시 사용할 수 있습니다.
이제 두 번째 질문인 왜 이 요청이 전송되었는지에 대해 알아봅시다.
이 동작은 서버가 응답에 설정하는 Cache-Control 헤더에 의해 제어됩니다. 요청/응답의 세부 정보를 보려면 네트워크 패널에서 CSS 파일을 클릭합니다. "응답 헤더" 블록의 "헤더" 탭에서 "Cache-Control" 값을 찾습니다.
이 헤더 안에는 쉼표로 구분된 다양한 조합의 지시문을 여러 개 넣을 수 있습니다. 이 경우에는 두 가지가 있습니다.
따라서 기본적으로 이 헤더가 브라우저에 알려주는 것은 다음과 같습니다.
결과적으로 브라우저는 항상 서버를 확인하며 캐시를 바로 사용하지 않습니다.
max-age
을 0에서 31536000(1년, 허용되는 최대 초) 사이로 변경하기만 하면 쉽게 변경할 수 있습니다. 이렇게 하려면 스터디 프로젝트에서 backend/index.ts
파일로 이동하여 max-age=0
이 설정된 위치를 찾아 31536000(1년)으로 변경합니다. 페이지를 몇 번 새로고침하면 네트워크 탭의 CSS 파일에 이 내용이 표시될 것입니다.
이제 Status
열이 회색으로 표시되고 Size
에 '(memory cache)'가 표시되는 것을 확인할 수 있습니다. 이제 CSS 파일이 브라우저의 캐시에서 제공되며 앞으로도 계속 그렇게 될 것입니다. 페이지를 몇 번 새로고침하여 변경되지 않는지 확인합니다.
이제 캐시 헤더를 엉망으로 만든 본론으로 들어가서 페이지의 성능을 다시 측정해 봅시다. "평균 3G" 프로필 설정을 설정하고 ‘캐시 비활성화’ 설정은 선택하지 않은 상태로 유지하는 것을 잊지 마세요.
결과는 다음과 같아야 합니다.
"요청 전송 및 대기" 부분은 높은 지연 시간에도 불구하고 거의 0에 가까워졌고, ‘HTML 파싱’과 자바스크립트 평가 사이의 간격이 거의 사라졌으며, LCP 값은 650밀리초대로 돌아갔습니다.
추가 연습
max-age
값을 지금 10(10초)으로 변경합니다.- '캐시 비활성화' 확인란을 체크한 상태에서 페이지를 새로고침하여 캐시를 삭제합니다.
- 확인란을 선택 해제하고 페이지를 다시 새로 고치면 이번에는 메모리 캐시에서 제공되어야 합니다.
- 10초간 기다렸다가 페이지를 다시 새로고침합니다.
max-age
이 10초에 불과하므로 브라우저는 리소스를 다시 확인하고 서버는 304를 다시 반환합니다.- 페이지를 즉시 새로고침 - 메모리에서 다시 제공되어야 합니다.
캐시가 성능의 만병통치약이며 가능한 한 모든 것을 공격적으로 캐시해야 한다는 것을 의미할까요? 절대 그렇지 않습니다! 다른 경우는 논외로 하더라도, "기술에 익숙하지 않은 고객"과 "브라우저 캐시를 지우는 방법을 전화로 설명해야 하는 고객"에게 문제가 생긴다면 가장 노련한 개발자에게 공황 발작을 일으킬 수 있습니다.
캐시를 최적화하는 방법은 수백만 가지가 있으며, 캐시의 수명에 영향을 줄 수도 있고 그렇지 않을 수도 있는 다른 헤더와 함께 Cache-Control 헤더의 지시어를 조합하는 방법은 서버의 구현에 따라 달라질 수도 있고 그렇지 않을 수도 있습니다. 아마 이 주제만 해도 책 몇 권 분량의 정보를 쓸 수 있을 것입니다. 캐시의 달인이 되고 싶다면 https://web.dev/ 및 MDN 리소스의 기사부터 시작하여 발자취를 따라가 보세요.
안타깝게도 "이것이 모든 것을 위한 최고의 캐시 전략 5가지"라고 말할 수 있는 사람은 아무도 없습니다. 기껏해야 이렇게 대답할 수 있을 뿐입니다. "이런 사용 사례가 있다면 이것, 이것, 이것과 함께 이런 캐시 설정 조합을 사용하는 것이 좋지만, 이런 문제점을 염두에 두어야 합니다." 정도입니다. 모든 것은 리소스, 빌드 시스템, 리소스 변경 빈도, 리소스를 캐시하는 것이 얼마나 안전한지, 잘못했을 때 어떤 결과가 초래되는지를 파악하는 데 달려 있습니다.
하지만 여기에는 한 가지 예외가 있습니다. 명확한 '모범 사례'가 있다는 점에서 예외입니다. 최신 툴로 구축된 웹사이트용 자바스크립트 및 CSS 파일입니다. Vite, Rollup, Webpack 등과 같은 최신 번들러는 "변경 불가능한" JS 및 CSS 파일을 생성할 수 있습니다. 물론 진정한 의미의 "불변"은 아닙니다. 하지만 이러한 도구는 파일 콘텐츠에 따라 해시 문자열로 파일 이름을 생성합니다. 파일 콘텐츠가 변경되면 해시가 변경되고 파일 이름도 변경됩니다. 결과적으로 웹사이트가 배포되면 브라우저는 캐시 설정에 관계없이 완전히 새로운 파일 사본을 다시 가져옵니다. 이전 연습에서 CSS 파일의 이름을 수동으로 바꿨을 때와 마찬가지로 캐시가 "버스트"됩니다.
예를 들어 스터디 프로젝트의 dist/assets
폴더를 살펴보세요. js 파일과 CSS 파일 모두 index-[hash]
파일 이름이 있습니다. 이 이름을 기억하고 npm run build
를 몇 번 실행하세요. 해당 파일의 내용은 변경되지 않았으므로 이름은 정확히 동일하게 유지됩니다.
이제 src/App.tsx
파일로 이동하여 console.log("bla")
같은 것을 추가합니다. npm run build
를 다시 실행하고 생성된 파일을 확인합니다. CSS 파일 이름은 이전과 동일하게 유지되지만 JS 파일 이름이 변경된 것을 볼 수 있습니다. 이 웹사이트가 배포되면 다음에 반복 사용자가 방문할 때 브라우저는 이전에 캐시에 나타나지 않았던 완전히 다른 JS 파일을 요청합니다. 캐시가 손상된 것입니다.
추가 연습
프로젝트의dist
폴더에 해당하는 폴더를 찾아 빌드 명령을 실행합니다.
- 파일 이름이 어떻게 생겼나요? 해시를 사용하나요, 아니면 일반
index.js
,index.css
등과 비슷하나요?- 빌드 명령을 다시 실행하면 파일 이름이 변경되나요?
- 코드의 어딘가에서 간단한 변경을 하면 파일 이름이 몇 개나 바뀌나요?
빌드 시스템이 이런 식으로 구성되어 있다면 운이 좋다고 볼 수 있습니다. 생성된 에셋의 max-age
헤더를 설정하도록 서버를 안전하게 구성할 수 있습니다. 모든 이미지의 버전을 비슷하게 설정하는 경우 더 좋은 방법은 목록에 이미지를 포함시키는 것입니다.
웹사이트와 사용자 및 사용자의 행동에 따라 초기 로드 시 무료로 꽤 좋은 성능 향상을 얻을 수 있습니다.
이쯤 되면 "너무 과해요. 저는 주말에 Next.js로 간단한 웹사이트를 만들어서 2분 만에 Vercel/Netlify/HottestNewProvider에 배포했습니다. 이런 최신 도구가 이 모든 것을 처리해 주지 않을까요?"라고 생각하실 수도 있습니다. 충분히 그럴 만합니다. 저도 그렇게 생각했습니다. 하지만 실제로 확인해 보니 깜짝 놀랐습니다.
내 프로젝트 중 두 개가 max-age=0
이고 CSS 및 JS 파일에 대해 must-revalidate
를 수행해야 했습니다. 알고 보니 CDN이 제공하는(🤷🏻♀️)의 기본값이었습니다. 물론 이 기본값에는 이유가 있습니다. 다행히도 이 기본값은 쉽게 재정의할 수 있으므로 큰 문제는 없습니다. 하지만 여전히, 요즘은 누구도, 무엇도 믿을 수 없습니다 😅.
호스팅/CDN 제공 업체는 어떻습니까? 캐시 헤더 구성에 대해 얼마나 확신하십니까?
재미있는 조사였기를 바라며, 새로운 것을 배우고 프로젝트에서 한두 가지 문제를 해결했을 수도 있습니다. 여러분이 스터디 프로젝트를 진행하는 동안 저는 나머지 지표에 대한 작업을 진행할 것입니다(그러길 바랍니다). 곧 다시 뵙겠습니다!
I have come here looking for ideas for assignments, but I have ended up gaining this post on different kinds of leadership and how to reflect important competencies in a managerial subject and presentation. These are further explored with the assistance of cipdassignmenthelp.uk.com
That's a really interesting breakdown of the 304 response! I've often seen those in the network tab but never really dug into why they appear. It makes perfect sense that the server is checking if the locally cached version is still valid, saving bandwidth and download time. I wonder if this type of caching mechanism also plays a role in how web games are optimized. For example, when playing something like Slope Unblocked , the game assets must be heavily cached to keep the experience smooth. If the browser had to constantly re-download assets, it would definitely affect performance! So, understanding this 304 process is definitely useful for thinking about overall web performance, even in unexpected contexts.
좋은 글번역 해주셔서 너무너무 감사합니다.