웹 개발자라면 한 번쯤 이런 고민에 빠져본 적 있을 겁니다. 느린 웹 페이지는 높은 이탈률(Bounce Rate)로 직결되고, 이는 비즈니스에 치명적입니다. 놀랍게도 이 문제의 주범은 우리가 편리하다고 믿어왔던 최신 개발 도구와 전통적인 개발 방식일 때가 많습니다.

하지만 걱정 마세요. 몇 가지 핵심 지식과 영리한 전략만 있다면 이 흔한 문제를 쉽게 해결하고 경쟁에서 앞서나갈 수 있습니다. 핵심 목표는 **’가볍고 빠른 페이지’**입니다. 초기 페이지 로딩 시 발생하는 네트워크 요청을 최소화하고, 사용자 화면에 무엇이든 최대한 빨리 보여주는 데 집중해야 합니다.
오늘 소개해 드릴 4가지 기술로 무장하고, 당신의 웹사이트를 무겁게 만드는 프레임워크와 라이브러리의 유혹에서 벗어나 보세요. 당신의 차별화된 접근 방식이 사용자에게 최고의 경험을 선사할 것입니다.
1. ‘무거운’ 프레임워크, 정말 필수일까요?
React, Angular, Vue와 같은 프레임워크는 현대 웹 개발의 표준처럼 여겨집니다. 하지만 이들은 배우기 어려울 뿐만 아니라, 실행하는 데에도 상당한 리소스를 소모합니다. 대부분의 프레임워크는 브라우저가 이미 잘하고 있는 일을 자바스크립트에 과도하게 위임하며 ‘바퀴를 재발명’하는 경우가 많습니다. 이는 성능 저하의 주된 원인이 됩니다.
우리의 전략이 ‘가볍고 빠른 웹페이지’라면, 무거운 프레임워크는 잘못된 선택일 수 있습니다. 프레임워크가 내세우는 주요 가치는 SPA(Single-Page Application), 더 나은 코드 구조화, 효율적인 DOM 업데이트 등입니다. 하지만 첫 두 가지는 논쟁의 여지가 있고, 세 번째는 특정 시나리오에서만 사실입니다.
개인적으로 저는 더 작고 집중된 프로젝트에는 **웹 컴포넌트(Web Components)**를 사용합니다. 훨씬 가벼울 뿐만 아니라, React, Angular, Vue가 역사 속으로 사라진 먼 미래에도 웹 컴포넌트는 W3C 표준의 일부이기 때문에 계속 작동할 것입니다.
만약 프레임워크를 꼭 사용해야 한다면, 구글의 Lit처럼 웹 컴포넌트를 가볍게 감싼 래퍼(wrapper)를 고려해 보세요. 반복적인 코드(Boilerplate)를 줄여주면서도 성능을 유지하는 훌륭한 대안입니다.
가능하다면, 무거운 프레임워크 사용을 피하세요. 이어지는 조언들은 대부분 프레임워크를 사용하지 않는다는 가정하에 더욱 강력한 힘을 발휘합니다.
2. 의존성(Dependency) 다이어트: 더 적게, 더 가볍게
페이지가 처음 로딩될 때, 외부 라이브러리(의존성)를 가져오기 위한 네트워크 요청은 성능 저하의 가장 큰 원인입니다. 저는 Lodash나 Ramda 같은 헬퍼 라이브러리를 포함한 모든 의존성을 최대한 피하려고 노력합니다. 때로는 불편하더라도 직접 필요한 기능을 구현하는 것이 장기적으로는 이득입니다.
예를 들어, 코드 전반에서 통신 채널 역할을 하는 간단한 이벤트 버스(Event Bus)가 필요했던 적이 있습니다. 인기 있는 해결책은 RxJS라는 라이브러리지만, 압축 후 17.7KB에 달하는 이 라이브러리를 포함하는 대신, 저는 200바이트짜리 클래스를 직접 작성했습니다. 이렇게 절약한 초기 로딩 시간은 고작 100ms일 수 있지만, 이런 작은 노력들이 모여 엄청난 차이를 만들어냅니다.
ESM의 함정: 의존성 트리(Dependency Tree)를 경계하라
최신 자바스크립트 모듈 시스템인 **ESM(ECMAScript Modules)**은 라이브러리를 가져오는 새로운 표준 방식입니다. 하지만 ESM이 만병통치약은 아닙니다. 만약 가져오려는 모듈이 다른 많은 모듈에 의존하고 있다면, 브라우저는 이 모든 모듈, 즉 ‘의존성 트리’ 전체를 다운로드해야 합니다.
예를 들어, Lodash 라이브러리에서 단어의 첫 글자를 대문자로 만드는 capitalize
함수 하나를 가져오는 경우를 보겠습니다.
HTML
<script type="module">
import capitalize from 'https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/capitalize.js';
alert(capitalize("hello world!"));
</script>
단순히 capitalize
함수 하나를 사용하기 위해 브라우저는 23개의 파일을 추가로 요청합니다. 느린 네트워크 환경(GPRS)에서는 4.4초, 100Mb의 빠른 환경에서도 2.2초나 걸립니다. 연결 속도가 5000배 빨라져도 다운로드 시간이 고작 2배 개선된다는 것은, 대역폭이 문제가 아니라 수많은 요청 자체가 병목이라는 뜻입니다.
반면, 이 기능을 직접 구현하는 데는 몇 줄의 코드면 충분합니다.
JavaScript
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
핵심 교훈: 의존성은 아껴서 사용하세요. 프레임워크는 거대한 의존성이며, 라이브러리는 잠재적 부채입니다. 직접 구현하는 것이 30분 미만으로 가능하다면, 주저하지 마세요. 네트워크 지연 시간(Latency)이 당신의 웹 성능을 갉아먹는 주범입니다.
3. <script>
태그의 마법: defer
와 async
제대로 사용하기
defer
와 async
는 <script>
태그에 추가하여 동작 방식을 바꾸는 속성입니다. 둘 다 성능에 영향을 주지만, 미묘하게 다르게 작동하며 명확한 사용 사례가 있습니다.
먼저, 아무 속성도 없는 일반적인 <script>
태그는 HTML 파싱을 멈추고 스크립트를 다운로드하고 실행합니다. 여러 스크립트가 있더라도, 모든 스크립트 실행이 끝나야 비로소 브라우저는 나머지 HTML을 화면에 그리기 시작합니다. 이것이 바로 페이지가 하얗게 멈춰 보이는 ‘렌더링 차단(Render-blocking)’ 현상입니다.
defer
defer
속성은 브라우저에게 “스크립트 실행을 HTML 렌더링이 끝날 때까지 미뤄줘”라고 알려줍니다. 무거운 자바스크립트가 다운로드되고 실행되는 동안 사용자가 빈 화면을 보지 않도록 하는 매우 중요한 속성입니다. defer
스크립트들은 HTML에 명시된 순서대로 실행되는 것을 보장합니다.
HTML
<head>
<script src="/js/heavy-script.js" defer></script>
</head>
<p>나는 스크립트 실행 전에 먼저 렌더링됩니다.</p>
defer
스크립트는 모든 HTML과 스타일시트(CSS)가 로드된 후에 실행되므로, 스크립트 내에서 안전하게 DOM에 접근할 수 있습니다.
async
async
속성은 브라우저에게 “HTML 파싱과 동시에 스크립트를 다운로드하고, 다운로드가 끝나면 즉시 실행해줘”라고 말합니다. 스크립트와 HTML 파서는 서로를 기다리지 않지만, 스크립트 실행 시점에는 HTML 파싱이 잠시 중단될 수 있습니다. 실행 순서가 보장되지 않으므로, 다른 스크립트나 DOM에 의존하지 않는 독립적인 스크립트에 적합합니다.
HTML
<head>
<script src="/js/analytics.js" async></script>
</head>
요약
스크립트 타입 | 다운로드 시점 | 실행 시점 | 특징 |
일반(Normal) | 즉시 (파싱 중단) | 다운로드 후 즉시 | HTML 렌더링을 차단함 |
async | 즉시 (파싱과 병렬) | 다운로드 완료 시 즉시 | 실행 순서 보장 안 됨, 파싱을 중단시킬 수 있음 |
defer | 즉시 (파싱과 병렬) | HTML 파싱 완료 후 | 실행 순서가 보장됨, 렌더링을 차단하지 않음 |
Sheets로 내보내기
결론: 광고나 분석 스크립트처럼 페이지의 다른 부분과 상호작용하지 않는 독립적인 스크립트에는 async
를 사용하세요. 그 외의 모든 경우에는 최대한 defer
를 사용하는 것이 좋습니다.
4. 핵심(Critical) CSS 인라이닝: 첫인상을 결정하는 1초
사용자가 페이지에 접속했을 때 스크롤 없이 바로 보이는 화면 영역을 **’Above the Fold’**라고 합니다. **핵심 CSS(Critical CSS)**는 이 ‘Above the Fold’ 영역의 스타일을 최대한 빨리 렌더링하여 사용자에게 페이지가 빠르게 로딩된다는 인상을 주는 기술입니다. 사용자는 기다리는 것을 싫어하며, 로딩이 길어질수록 페이지를 떠날 확률이 높아지기 때문에 이 기법은 매우 중요합니다.
전략은 간단합니다. 전체 스타일시트를 ‘Above the Fold’용과 ‘Below the Fold(스크롤해야 보이는 영역)’용 두 개로 나누는 것입니다.
- 핵심 CSS (Above the Fold):
<style>
태그를 이용해 HTML <head>
안에 직접 삽입합니다. (인라이닝) - 비핵심 CSS (Below the Fold): 나머지 스타일은
<link>
태그를 이용해 <body>
태그가 닫히기 직전에 로드합니다.
이렇게 하면 브라우저는 거대한 CSS 파일을 모두 다운로드하기를 기다리지 않고, 일단 보이는 부분이라도 빠르게 그려낼 수 있습니다.
HTML
<!DOCTYPE html>
<html>
<head>
<style>
/* Above the fold 영역에 필요한 최소한의 스타일 */
aside { background-color: lightblue; padding: 20px; }
/* 2. FOUC 방지를 위해 Below the fold 영역을 잠시 숨김 */
main { opacity: 0; transition: opacity 0.5s; }
</style>
<script src="below-the-fold.js" defer></script>
</head>
<body>
<aside>Above the fold 영역입니다. 즉시 렌더링됩니다.</aside>
<main>
<p>Below the fold 영역입니다. 스타일이 로드되면 나타납니다.</p>
</main>
<link rel="stylesheet" href="below-the-fold.css">
</body>
</html>
위 예제의 below-the-fold.js
파일은 아래와 같이 간단한 코드를 가집니다.
JavaScript
// below-the-fold.js
document.addEventListener("DOMContentLoaded", () => {
// 숨겨뒀던 Below the fold 영역을 부드럽게 표시
document.querySelector('main').style.opacity = 1;
});
이 과정은 다음과 같이 진행됩니다.
- 브라우저는 HTML을 파싱하며
<head>
의 인라인 스타일(핵심 CSS)을 즉시 적용합니다. - ‘Above the Fold’ 영역(
aside
)이 먼저 화면에 그려집니다. - ‘Below the Fold’ 영역(
main
)은 opacity: 0
때문에 보이지 않습니다. (FOUC 방지) - 브라우저는 렌더링을 계속하며
<body>
끝에 있는 비핵심 CSS 파일을 다운로드합니다. - 모든 HTML과 CSS가 준비되면
defer
로 지정된 스크립트가 실행되어 main
영역의 opacity
를 1로 변경, 부드럽게 표시합니다.
결론적으로, 보이는 부분을 먼저 빠르게 그리고, 나머지는 준비되는 대로 보여주는 것입니다.
당신의 웹사이트를 위한 최종 점검
오늘 다룬 내용을 소화하고 당신의 프로젝트에 적용해 보세요. React, Angular, 수천 개의 의존성을 추천하는 일반적인 조언들에서 한 걸음 물러나, 다음과 같은 원칙을 세워보세요.
- 의존성을 철저히 검토하고 최소한으로 유지하세요. 요청이 적을수록 로딩은 빨라집니다.
<script>
태그가 언제, 어떻게 로드되는지 이해하고 렌더링 차단을 피하세요.- 스타일시트가 로드되는 시점과 렌더링 차단 여부를 파악하세요.
- 사용자에게 보이는 HTML을 가장 먼저, 가장 빠르게 렌더링하세요.
이 원칙들을 지킨다면, 당신의 웹사이트는 사용자에게는 쾌적한 경험을, 당신에게는 개발자로서의 큰 자부심을 안겨줄 것입니다.