웹 페이지를 최적화하면 방문자에게 더 빠르고 쾌적한 사이트를 제공할 수 있을 뿐만 아니라, 웹 서버와 인터넷 연결에 가해지는 부하도 크게 줄일 수 있어요. 이는 트래픽이 엄청나게 많은 사이트나, 속보 같은 예외적인 상황으로 인해 갑자기 트래픽이 폭주하는 사이트에서는 정말 결정적인 역할을 한답니다.
이 문서에서는 우리가 흔히 알고 있는 상식과 여러 실험들을 바탕으로 페이지 로딩 시간을 개선하는 방법들을 설명해 드릴게요.
페이지 로드 성능을 최적화하는 건 단순히 속도가 느린 전화선(다이얼업)이나 모바일 환경의 방문자만을 위한 게 아니에요. 초고속 인터넷 브로드밴드 환경에서도 똑같이 중요하며, 인터넷 연결이 가장 빠른 방문자들에게도 극적인 성능 향상을 체감하게 해줄 수 있답니다.
페이지 용량(무게)은 페이지 로드 성능에 있어서 단연코 가장 중요한 요소예요.
불필요한 공백과 주석을 제거하는 이른바 '최소화(Minimization/Minification)' 작업이나, HTML 안에 직접 작성된(inline) 자바스크립트와 CSS를 외부 파일로 분리하는 것만으로도 페이지 구조를 크게 바꾸지 않고 다운로드 성능을 향상시킬 수 있습니다.
HTML Tidy 같은 도구를 사용하면 유효한 HTML 소스에서 앞부분의 공백이나 불필요한 빈 줄을 자동으로 제거할 수 있어요. 또 다른 도구들은 소스 코드를 재구성하거나 난독화하고, 긴 변수명 등을 짧은 버전으로 교체해서 자바스크립트를 "압축(compress)"해 주기도 하죠.
💡 강사님의 실무 팁!
요즘 우리가 자주 쓰는 React나 Next.js, Vue 같은 프레임워크 환경에서는 보통 Webpack이나 Vite 같은 번들러(Bundler)를 사용하죠? 이런 빌드 도구들이 프로덕션 빌드 시에 위에서 말한 공백 제거, 난독화, 압축을 전부 자동으로 해준답니다! 프론트엔드 개발자라면 이런 번들러가 뒤에서 어떤 최적화 작업을 해주는지 원리를 이해하고 있는 것이 아주 중요해요.
웹 페이지에서 참조하는 파일의 개수를 줄이면, 페이지를 다운로드하는 데 필요한 HTTP 연결 횟수도 줄어들게 됩니다. 자연스럽게 요청을 보내고 응답을 받는 데 걸리는 전체 시간도 단축되겠죠?
브라우저의 캐시 설정에 따라 다르지만, 브라우저는 참조된 각 파일에 대해 If-Modified-Since 헤더를 포함한 요청을 보내서 "마지막으로 다운로드한 이후에 이 파일이 수정된 적이 있니?"라고 서버에 물어볼 수 있어요. 이렇게 참조된 파일들의 마지막 수정 시간을 확인하는 데 시간을 너무 많이 쓰게 되면, 브라우저가 페이지를 화면에 그리기(렌더링) 전에 이 모든 걸 확인해야 하기 때문에 웹 페이지의 초기 화면 표시가 지연될 수 있습니다.
만약 CSS에서 배경 이미지를 많이 사용한다면, 여러 이미지를 하나로 합치는 '이미지 스프라이트(Image sprite)' 기법을 사용해서 HTTP 조회 횟수를 줄일 수 있어요. 이렇게 하면 배경이 필요할 때마다 동일한 하나의 이미지를 적용하고 CSS의 x, y 좌표만 적절하게 조절해서 원하는 부분만 보여주면 됩니다. 이 기법은 크기가 제한된 요소에 가장 효과적이며, 모든 배경 이미지 상황에 쓸 수 있는 건 아니에요. 하지만 HTTP 요청을 줄이고 단일 이미지를 캐싱하는 것은 페이지 로드 시간을 줄이는 데 큰 도움이 됩니다.
💡 강사님의 실무 팁!
이미지 스프라이트는 과거에 정말 많이 썼던 기술이에요. 하지만 현대 프론트엔드 환경(HTTP/2 이상)에서는 여러 요청을 동시에(Multiplexing) 처리할 수 있어서 예전만큼 파일 개수를 병박적으로 줄여야 하는 건 아닙니다. 대신, 아이콘 같은 경우 요즘엔 SVG를 코드로 직접 삽입하거나, React 컴포넌트 형태로 만들어서 사용하는 방식을 더 추천드려요!
이 문서에서 말하는 CDN이란, 서버와 방문자 사이의 물리적인 거리를 줄이기 위한 수단을 의미해요. 원본 서버와 방문자 사이의 거리가 멀어질수록 로딩 시간은 길어질 수밖에 없거든요. 만약 여러분의 웹사이트 서버가 미국에 있고 방문자가 인도에 있다고 가정해 볼게요. 인도 방문자는 미국에 있는 방문자에 비해 페이지 로딩 시간이 훨씬 오래 걸릴 거예요.
CDN은 지리적으로 분산되어 사용자-웹사이트 간의 거리를 단축시켜 주는 협업 서버 네트워크입니다. CDN은 여러분의 웹사이트의 캐시된 복사본을 저장해 두고, 사용자와 가장 가까운 네트워크 노드를 통해 방문자에게 콘텐츠를 제공해요. 이를 통해 지연 시간(Latency)을 크게 줄여줍니다.
더 읽어볼 자료:
💡 강사님의 실무 팁!
글로벌 서비스를 준비하신다면 CDN은 선택이 아닌 필수예요. 특히 Next.js를 Vercel 같은 플랫폼에 배포해보셨다면, 여러분이 만든 프로젝트가 전 세계 Edge Network(CDN)에 자동으로 배포된다는 걸 알고 계셨나요? 프론트엔드 개발자라도 인프라 최적화가 어떻게 이루어지는지 알아두면 면접관들에게 아주 좋은 인상을 줄 수 있습니다.
각기 다른 도메인은 DNS 조회 시마다 시간을 소모하게 만들어요. 따라서 CSS의 link 태그나 자바스크립트, 이미지의 src 속성에 나타나는 서로 다른 도메인의 개수가 많아질수록 페이지 로드 시간도 길어집니다.
항상 가능한 건 아니겠지만, 여러분의 페이지에서 사용하는 각기 다른 도메인의 수를 항상 최소한으로 유지하도록 신경 쓰셔야 해요.
캐싱(저장)될 수 있는 모든 콘텐츠는 적절한 만료(expiration) 시간과 함께 반드시 캐싱되도록 만드세요.
특히 Last-Modified 헤더에 주목해야 합니다. 이 헤더는 효율적인 페이지 캐싱을 가능하게 해주는데요. 사용자 에이전트(브라우저)가 로드하려는 파일이 언제 마지막으로 수정되었는지에 대한 정보를 이 헤더를 통해 전달하게 됩니다. 대부분의 웹 서버는 파일 시스템에 저장된 마지막 수정 날짜를 기준으로 정적 페이지(.html, .css 등)에 Last-Modified 헤더를 자동으로 붙여줍니다. 반면 동적으로 생성되는 페이지(.php, .aspx 등)에서는 이 작업이 자동으로 이뤄질 수 없으므로 헤더가 전송되지 않아요.
따라서 동적으로 생성되는 페이지에 대해서는 이 주제에 대해 조금 더 연구해 보시는 게 좋습니다. 다소 복잡할 순 있지만, 일반적으로 캐싱이 불가능한 페이지들에서 발생하는 엄청난 양의 페이지 요청 부하를 줄여줄 수 있거든요.
더 자세한 정보:
1. RSS 해커들을 위한 HTTP Conditional Get
2. HTTP 304: 수정되지 않음 (Not Modified)
3. 위키백과의 HTTP ETag
4. HTTP에서의 캐싱
사용자가 페이지를 로딩하는 동안 가장 빠르게 반응을 시각적으로 체감할 수 있도록, 페이지의 핵심 콘텐츠와 그 초기 화면을 그리는 데 필요한 CSS나 자바스크립트를 먼저 다운로드하게 하세요. 이러한 콘텐츠는 대개 텍스트로 이루어져 있기 때문에 전송 중 텍스트 압축의 이점을 누릴 수 있고, 결과적으로 사용자에게 더 빠른 응답성을 제공합니다.
페이지 로드가 완전히 끝난 후에야 사용 가능한 동적 기능(인터랙션 등)은 처음에는 비활성화해두고, 페이지가 전부 로드된 이후에만 활성화되도록 해야 합니다. 이렇게 하면 자바스크립트가 페이지 콘텐츠 뒤에 로드되도록 유도하여 전반적인 페이지 로드 경험을 향상시킬 수 있습니다.
💡 강사님의 실무 팁!
이 부분은 '브라우저 렌더링 파이프라인(Critical Rendering Path)'과 관련된 매우 중요한 내용이에요! 프론트엔드 직무 면접에서 단골로 등장하죠. 웹 브라우저가 HTML을 파싱하다가<script>태그를 만나면 파싱을 멈추기 때문에 스크립트를 하단에 배치하는 것이 예전의 정석이었습니다. 요즘은<head>안에서defer나async속성을 주로 활용한답니다. (아래쪽에 관련 내용이 또 나오니 이어서 볼게요!)
인라인 스크립트는 페이지 로딩에 꽤나 큰 비용을 발생시킬 수 있습니다. 왜냐하면 브라우저의 파서(Parser)는 HTML을 파싱하는 도중에 인라인 스크립트가 페이지 구조를 마음대로 바꿔버릴 수도 있다고 가정해야 하기 때문이죠. 전반적인 인라인 스크립트 사용을 줄이고, 특히 화면에 콘텐츠를 출력하기 위해 document.write()를 사용하는 것을 지양하면 전체적인 페이지 로드 속도를 향상시킬 수 있습니다. document.write() 대신 DOM API를 사용하여 페이지 콘텐츠를 조작하세요.
최신 CSS를 사용하면 HTML 마크업의 양을 줄일 수 있고, 레이아웃을 잡기 위해 투명한 빈 이미지(spacer)를 써야 하는 상황을 줄일 수 있습니다. 또한 CSS와 텍스트를 조합하는 것보다 훨씬 비싼 "비용"이 드는 양식화된 텍스트 이미지들을 최신 CSS로 대체할 수 있는 경우가 아주 많습니다.
유효한 표준 마크업을 사용하는 것은 또 다른 장점이 있습니다. 첫째, 브라우저가 HTML을 파싱할 때 에러 수정 과정을 거칠 필요가 없어집니다. (이는 사용자가 입력한 다양한 형식을 허용하고 프로그램이 이를 "수정"하거나 정규화할 것인지, 아니면 엄격하게 무관용 입력 형식을 강제할 것인지에 대한 철학적인 문제는 논외로 합니다.)
더 나아가, 유효한 마크업을 사용하면 웹 페이지를 '전처리(pre-process)'할 수 있는 다양한 도구들을 자유롭게 쓸 수 있게 됩니다. 예를 들어 HTML Tidy는 빈 공백이나 선택적인 종료 태그를 제거해 줄 수 있지만, 마크업에 심각한 오류가 있는 페이지에서는 실행 자체가 거부될 겁니다.
레이아웃을 잡을 때 <table /> 태그를 사용하는 것은 이제 피해야 하는 구시대적인 방법입니다. 대신 floats(플로트), positioning(포지셔닝), flexbox(플렉스박스), 또는 grids(그리드)를 활용한 레이아웃 기법을 사용하셔야 합니다.
테이블(Tables)은 여전히 유효한 마크업이지만, 오직 '표 형태의 데이터(tabular data)'를 보여줄 때만 사용해야 해요. 브라우저가 페이지를 더 빨리 렌더링하도록 돕기 위해서는 테이블을 중첩해서 레이아웃을 짜는 대신, 플렉스박스(Flexible box) 레이아웃이나 그리드(Grid) 레이아웃 같은 최신 CSS 기법을 사용해야 합니다.
💡 강사님의 실무 팁!
디자인 툴인 Figma로 작업된 UI를 프론트엔드 코드로 옮길 때, 이 Flexbox와 Grid를 자유자재로 다루는 능력이 정말 중요해요! 테이블은 접근성(Accessibility) 측면에서도 데이터 표시에만 사용하는 것이 올바른 시맨틱 마크업(Semantic Markup)입니다.
대부분의 드로잉 애플리케이션(일러스트레이터 등)에서 만들어낸 SVG 파일에는 불필요한 메타데이터가 꽤 많이 포함되어 있는데, 이건 지워버려도 무방합니다. 서버를 설정하실 때 SVG 에셋에 대해 gzip 압축이 적용되도록 하세요.
용량이 큰 이미지는 페이지 로드 시간을 크게 지연시킵니다. 페이지에 이미지를 넣기 전에 포토샵 같은 이미지 편집 프로그램에 내장된 압축 기능을 사용하거나, Compress JPEG 또는 Tiny PNG 같은 전문화된 도구를 사용해서 이미지를 압축하는 것을 고려해 보세요.
💡 강사님의 실무 팁!
차세대 포맷인WebP나AVIF를 사용하는 것을 적극 권장합니다! 만약 Next.js를 공부하고 계시다면 내장된<Image />컴포넌트가 자동으로 이미지를 압축해주고 WebP 포맷으로 변환해 주는 아주 강력한 기능을 제공한답니다. 프론트엔드 포트폴리오를 만드실 때 이 부분을 강조하시면 좋은 점수를 받으실 수 있어요.
브라우저가 이미지와 테이블의 높이(height)나 너비(width)를 즉시 알아낼 수 있다면, 콘텐츠를 재배치(reflow)하지 않고도 웹 페이지를 화면에 바로 띄울 수 있습니다. 이렇게 하면 페이지 표시 속도가 빨라질 뿐만 아니라, 페이지 로딩이 끝날 무렵 레이아웃이 덜컥거리며 바뀌는 짜증 나는 현상도 방지할 수 있죠. 이런 이유로 이미지에는 가능하면 항상 height와 width 값을 지정해주어야 합니다.
테이블의 경우에는 다음과 같은 CSS 속성 조합을 사용해야 합니다:
table-layout: fixed;
그리고 컬럼(열)의 너비는 <col> 요소와 <colgroup> 요소를 사용해서 지정해 주어야 합니다.
💡 강사님의 실무 팁!
방금 언급된 '레이아웃이 덜컥거리며 바뀌는 현상'을 전문 용어로 CLS (Cumulative Layout Shift) 라고 부릅니다. 구글의 Core Web Vitals (웹 핵심 성능 지표) 중 하나로, SEO(검색엔진 최적화)와 프론트엔드 성능 최적화 면접에서 무조건 나오는 초핵심 개념이니 꼭 기억해 두세요!
기본적으로 이미지는 '즉각적(eagerly)'으로 로드됩니다. 즉, HTML에서 처리되자마자 이미지를 가져오고 렌더링해 버리죠. 이렇게 즉각적으로 로드되는 모든 이미지는 브라우저 창의 load 이벤트가 발생하기 전에 전부 그려집니다. 반면 이미지 로딩을 '지연 로딩(lazy loading)'으로 설정하면, 사용자가 스크롤을 내려서 그 이미지가 현재 보여야 할 시각적 뷰포트(visual viewport) 영역에 근접하기 전까지는 이미지 로딩을 보류하라고 브라우저에게 지시할 수 있습니다.
이미지를 지연 로딩되도록 지정하려면, 이미지 태그의 loading 속성 값을 lazy로 지정하세요. 이렇게 설정하면 이미지는 실제로 필요할 때만 다운로드됩니다.
<img src="./images/footerlogo.jpg" loading="lazy" alt="MDN logo" />
주의할 점은 지연 로딩되는 이미지는 load 이벤트가 발생했을 시점에 아직 준비되지 않았을 수도 있다는 겁니다. 특정 이미지가 로드되었는지 확인하려면, 해당 이미지 요소의 불리언(Boolean) 속성인 complete 값이 true인지 체크하면 됩니다.
페이지 디자인 개선의 효과를 극대화하려면, 진행하는 프로젝트에 대해 타당하고 합리적인 타겟 브라우저(사용자 에이전트) 요구사항을 명확히 설정해야 합니다. 모든 브라우저, 특히 구형 버전의 브라우저에서까지 픽셀 단위로 똑같이 완벽하게 보여야(Pixel-perfect) 한다고 고집하지 마세요.
이상적인 최소 요구사항은 관련 웹 표준을 제대로 지원하는 모던 브라우저들을 기준으로 삼는 것입니다. 여기에는 최신 버전의 파이어폭스(Firefox), 구글 크롬(Google Chrome), 오페라(Opera), 그리고 사파리(Safari) 등이 포함될 수 있습니다.
하지만 이 글에 나열된 수많은 팁들은 상식에 기반한 범용적인 기술들이기 때문에, 특정 브라우저 지원 요구사항과 상관없이 어떤 사용자 에이전트, 어떤 웹 페이지에도 폭넓게 적용할 수 있다는 점을 기억해 주세요.
여러분의 자바스크립트 파일들이 async(비동기)와 defer(지연) 속성 모두와 잘 호환되도록 작성하시고, 스크립트 요소가 여러 개일 경우에는 가능하면 항상 async를 사용하세요.
이렇게 하면 자바스크립트 파일을 백그라운드에서 로딩하는 동안에도 페이지 렌더링이 멈추지 않고 계속 진행될 수 있습니다. 만약 이 속성들을 넣지 않으면, 브라우저는 해당 스크립트 태그 뒤에 있는 요소들의 렌더링을 완전히 중단해버립니다.
참고: 이 속성들은 페이지가 처음 로딩될 때 엄청나게 큰 도움을 주지만, 브라우저에 따라 완벽하게 작동하지 않을 수도 있다는 점을 염두에 두고 사용하셔야 합니다. 만약 이미 자바스크립트 모범 사례(Best Practices)를 잘 따르고 있다면, 굳이 기존 코드를 바꿀 필요는 없습니다.
💡 강사님의 실무 팁!
"스크립트 태그의async와defer의 차이점을 설명해 보세요."
이건 프론트엔드 취업 면접 단골 질문 Top 10 안에 드는 질문입니다!
간략히 설명하자면, 둘 다 HTML 파싱과 스크립트 다운로드를 병렬로 진행하는 건 같습니다.
하지만async는 다운로드가 끝나는 즉시 HTML 파싱을 멈추고 스크립트를 실행해 버리고,
defer는 HTML 파싱이 끝날 때까지 얌전히 기다렸다가 순서대로 스크립트를 실행한다는 결정적인 차이가 있습니다. 꼭 기억해 두세요! TypeScript나 최신 문법으로 개발할 때도 이 빌드 결과물이 어떻게 삽입되는지 알아두면 큰 도움이 됩니다.
<html><head><body><header> / <main> / <table> 등)으로, 전체 페이지가 다 다운로드될 때까지 기다리지 않고도 화면에 바로 표시될 수 있는 요소들입니다.<script>