자바스크립트가 발목 잡는 웹사이트 속도 개선 실전기

야심 차게 블로그에 추가한 사이트맵 페이지가 있습니다. 모든 게시물을 한눈에 보여주는, 블로그의 충실한 안내자 역할을 기대했던 페이지였죠. 하지만 현실은 처참했습니다. 페이지를 열면 하얀 화면만 둥둥 떠다니다 한참 뒤에야 굼벵이처럼 목록이 나타나는 상황. 사용자 경험은 물론, 운영하는 제 입장에서도 엄청난 답답함을 안겨주었습니다. 풀스택 개발자로서 '성능'은 절대 타협할 수 없는 가치인데, 제 블로그의 한 페이지가 이렇게 형편없는 성능을 보인다는 것은 자존심이 상하는 일이었습니다.

단순히 '느리다'는 감상적인 평가를 넘어, 개발자 도구를 열어 네트워크 탭과 퍼포먼스 탭을 확인하는 순간 직감했습니다. "이건 서버 문제가 아니다, 범인은 바로 프론트엔드, 구체적으로는 자바스크립트(JavaScript)다." 이 글은 바로 그 범인을 추적하고, 비효율적인 코드를 개선하여 웹사이트의 체감 속도를 극적으로 끌어올린 저의 실전 경험담입니다. 만약 여러분의 웹사이트가 이유 없이 느리다고 느껴진다면, 이 글에서 제시하는 웹 성능 최적화 기법들이 분명 좋은 실마리가 될 것입니다.

느린 속도의 근본 원인: 비효율적인 JavaScript는 어떻게 브라우저를 괴롭히나

느린 사이트맵의 원인을 파악하기 위해 코드를 열어보았을 때, 문제는 명확했습니다. 블로거 플랫폼에서 제공하는 피드(Feed) 데이터를 JSON 형태로 받아와 동적으로 HTML 목록을 생성하는 로직에 병목 현상이 있었습니다. 단순히 '코드가 많아서'가 아니었습니다. '나쁜 습관'으로 작성된 코드가 브라우저에 불필요한 부담을 가중시키고 있었던 것이죠. 구체적으로 두 가지 치명적인 문제점을 발견했습니다.

  1. 반복문(Loop) 안에서 DOM 컬렉션의 .length 속성을 매번 다시 호출하는 문제
  2. 객체의 깊은 속성(Deeply Nested Property)에 반복적으로 접근하는 문제

이 두 가지가 왜 프론트엔드 성능에 치명적인지 이해하려면, 먼저 브라우저가 JavaScript와 DOM을 어떻게 처리하는지에 대한 깊은 이해가 필요합니다. 이것은 단순한 코드 최적화 팁을 넘어, 웹 개발의 근본 원리에 대한 이야기입니다.

1. 살아있는 배신자: Live Collection과 .length의 함정

많은 개발자들이 for 루프를 작성할 때 다음과 같은 패턴을 무심코 사용합니다.

// 흔히 볼 수 있지만 잠재적 위험이 있는 코드
const elements = document.getElementsByClassName('item');

for (let i = 0; i < elements.length; i++) {
  // elements[i]를 이용한 작업...
}

이 코드는 대부분의 경우 문제없이 동작합니다. 하지만 document.getElementsByClassName이나 document.getElementsByTagName 등이 반환하는 HTMLCollection은 '살아있는(Live)' 컬렉션이라는 특성을 가집니다. '살아있다'는 것은, DOM에 변화가 생기면 별도의 재요청 없이도 이 컬렉션이 실시간으로 그 변화를 반영한다는 의미입니다.

이게 왜 문제가 될까요? 반복문이 한 번 돌 때마다 elements.length를 체크하는 조건문은, 이 '살아있는' 컬렉션에게 "지금 DOM 상태를 다시 확인해서 내 길이를 알려줘!"라고 매번 요청하는 것과 같습니다. 게시물이 수백, 수천 개인 사이트맵 페이지에서 이 작업이 수천 번 반복된다고 상상해 보십시오. 심지어 루프 안에서 DOM 구조를 변경하는 코드라도 있다면(예: 특정 요소를 삭제하거나 추가), .length 값이 계속 변하면서 루프가 무한히 돌거나 예상치 못한 결과를 낳을 수도 있습니다. 이는 마치 요리책의 레시피를 한 단계 진행할 때마다 책의 총 페이지 수를 처음부터 다시 세는 것과 같은 비효율의 극치입니다.

중요: querySelectorAll이 반환하는 NodeList는 대부분의 현대 브라우저에서 정적(Static) 컬렉션이므로 이 문제에서 비교적 자유롭습니다. 하지만 하위 호환성이나 특정 API가 HTMLCollection을 반환하는 경우를 대비해, .length를 캐싱하는 습관은 여전히 유효하고 안전한 자바스크립트 성능 최적화 전략입니다.

2. 점(.) 하나가 만드는 성능 저하: 객체 속성 접근의 비용

두 번째 문제는 객체의 깊은 속성에 반복적으로 접근하는 것이었습니다. 제 사이트맵 스크립트는 블로거 피드에서 받은 JSON 데이터를 처리하는데, 그 구조가 대략 아래와 같았습니다.

const feedData = {
  feed: {
    entry: [
      { title: { $t: '첫 번째 글' }, link: [ { rel: 'alternate', href: '...' } ] },
      { title: { $t: '두 번째 글' }, link: [ { rel: 'alternate', href: '...' } ] },
      // ... 수백 개의 글
    ]
  }
};

그리고 기존 코드는 각 게시물의 제목을 가져오기 위해 루프 안에서 매번 이렇게 접근했습니다.

// 비효율적인 접근 방식
for (let i = 0; i < feedData.feed.entry.length; i++) {
  let postTitle = feedData.feed.entry[i].title.$t;
  // ...
}

feedData.feed.entry[i].title.$t. 점이 무려 네 개나 찍힙니다. JavaScript 엔진이 이 속성을 찾기 위해 어떤 일을 할까요?

  1. 전역 스코프(또는 현재 스코프)에서 feedData 객체를 찾습니다.
  2. feedData 객체 안에서 feed 속성을 찾습니다.
  3. feed 객체 안에서 entry 속성을 찾습니다.
  4. entry 배열에서 i번째 인덱스를 찾습니다.
  5. 해당 객체 안에서 title 속성을 찾습니다.
  6. 마지막으로 title 객체 안에서 $t 속성을 찾아 그 값을 반환합니다.

이 과정은 스코프 체인 탐색과 프로토타입 체인 탐색을 포함하는, 생각보다 비용이 드는 작업입니다. 한두 번이라면 무시할 수 있지만, 수백, 수천 번의 반복문 안에서 이 과정이 되풀이된다면 티끌 모아 태산이 되어 전체적인 스크립트 실행 시간을 늦추는 주범이 됩니다. 특히 오래된 디바이스나 저사양 환경에서는 이 차이가 더욱 극명하게 드러나며, 이는 곧바로 나쁜 사용자 경험으로 이어집니다.

실전! 웹사이트 속도를 되살리는 JavaScript 최적화 처방전

원인을 분석했으니 이제 해결할 차례입니다. 앞서 진단한 두 가지 문제점에 대한 해결책은 놀랍도록 간단하지만, 그 효과는 매우 강력합니다. 핵심은 '반복을 줄이고, 미리 계산하고, 저장해두는 것', 즉 캐싱(Caching)입니다.

처방 1: 반복문의 군살 빼기 - `length` 값 캐싱

첫 번째 문제는 루프가 시작되기 전에 length 값을 변수에 미리 할당해두는 것만으로 간단히 해결할 수 있습니다. 이는 루프 조건문이 실행될 때마다 DOM을 재계산하는 대신, 메모리에 저장된 숫자 값을 바로 가져오게 하여 불필요한 연산을 원천 차단합니다.

// 최적화 전
for (let i = 0; i < feedData.feed.entry.length; i++) {
  // ...
}

// 최적화 후: length를 변수에 캐싱
const entries = feedData.feed.entry;
const entryCount = entries.length; // ★ 핵심: 루프 시작 전 단 한 번만 계산!

for (let i = 0; i < entryCount; i++) {
  // ...
}

이 작은 변화만으로도 브라우저는 반복문이 돌 때마다 DOM에 접근할 필요가 없어집니다. 대신 메모리에 있는 `entryCount`라는 변수 값만 확인하면 되므로 비교 연산이 훨씬 가볍고 빨라집니다. 이것이 바로 for 루프 성능 최적화 자바스크립트 팁의 가장 기본이자 핵심입니다.

처방 2: 자주 쓰는 주소는 단축번호로 - 객체 참조 캐싱

두 번째 문제 역시 비슷한 원리로 해결합니다. 반복적으로 접근해야 하는 깊은 객체의 경로를 루프 바깥에서 변수에 미리 할당해두는 것입니다. 이렇게 하면 JavaScript 엔진이 매번 전체 경로를 탐색하는 수고를 덜어줄 수 있습니다.

앞선 예제를 종합하여 최종적으로 개선된 코드는 다음과 같습니다.

// === 최종 최적화 코드 예시 ===

// 1. 자주 사용하는 객체 경로를 변수에 캐싱
const entries = feedData.feed.entry; 
// 2. 배열의 길이를 변수에 캐싱
const entryCount = entries.length;

let postListHtml = ""; // 문자열을 계속 더해나갈 변수

for (let i = 0; i < entryCount; i++) {
  // 3. 루프 내에서도 자주 쓰는 객체는 변수로!
  const currentEntry = entries[i];
  
  const postTitle = currentEntry.title.$t;
  const postUrl = currentEntry.link.find(link => link.rel === 'alternate').href;

  // 나중에 한 번에 DOM에 삽입하기 위해 HTML 문자열을 만듭니다. (성능 팁!)
  postListHtml += `<li><a href="${postUrl}">${postTitle}</a></li>`;
}

// 최종적으로 생성된 HTML 문자열을 DOM에 한 번만 삽입합니다.
document.getElementById('sitemap-container').innerHTML = postListHtml;

위 코드에서는 세 가지 최적화가 적용되었습니다.

  1. feedData.feed.entry라는 긴 경로를 entries라는 짧은 변수에 저장했습니다.
  2. entries.lengthentryCount에 저장하여 루프 조건 검사 비용을 줄였습니다.
  3. 루프 안에서도 entries[i]currentEntry라는 변수에 할당하여, titlelink에 접근할 때 탐색 경로를 한 단계 줄였습니다.

이러한 작은 습관들이 모여 애플리케이션 전체의 반응성을 높이고, 특히 데이터 양이 많은 사이트맵 속도 같은 페이지에서는 눈에 띄는 성능 향상을 가져옵니다.

한 걸음 더: 풀스택 개발자가 제안하는 심화 프론트엔드 성능 최적화

앞서 소개한 두 가지 방법만으로도 제 블로거 최적화 문제는 상당 부분 해결되었습니다. 하지만 여기서 멈춘다면 '경험 많은 개발자'라고 할 수 없겠죠. 근본적인 웹 성능 최적화 관점에서 몇 가지 추가적인 기법을 더 고민하고 적용해볼 수 있습니다.

1. DOM 조작 최소화: 리플로우(Reflow)와 리페인트(Repaint) 줄이기

웹 성능 저하의 가장 큰 주범 중 하나는 바로 잦은 DOM 조작입니다. 브라우저는 DOM 트리에 변화가 생길 때마다 화면을 어떻게 다시 그릴지 계산하고(리플로우), 실제로 픽셀을 찍어내는 과정(리페인트)을 거칩니다. 이 과정은 CPU 자원을 많이 소모합니다.

만약 루프 안에서 매번 document.getElementById('sitemap-container').innerHTML += ... 와 같이 DOM을 직접 건드렸다면 어땠을까요? 수백 개의 게시물이라면 수백 번의 리플로우와 리페인트가 발생하여 페이지가 거의 마비 상태에 빠졌을 겁니다.

"DOM을 직접 건드리는 것은 비용이 매우 비싸다. 가능한 한 메모리 안에서 모든 작업을 마친 뒤, 단 한 번만 실제 DOM에 반영하라." - 웹 성능 최적화의 제1원칙

앞선 최적화 코드 예시에서 제가 HTML 문자열을 변수(postListHtml)에 계속 축적한 뒤 루프가 끝난 후 단 한 번만 .innerHTML을 사용한 이유가 바로 이것입니다. 하지만 더 세련된 방법도 있습니다. 바로 DocumentFragment를 사용하는 것입니다.

DocumentFragment는 '메모리상의 가상 DOM'이라고 생각할 수 있습니다. 실제 DOM 트리에 속해있지 않기 때문에, 여기에 아무리 많은 자식 노드를 추가해도 리플로우나 리페인트가 발생하지 않습니다. 모든 준비가 끝나면 이 프래그먼트를 실제 DOM에 한 번만 추가하면 됩니다.

// DocumentFragment를 사용한 더욱 향상된 DOM 조작
const fragment = document.createDocumentFragment();
const entries = feedData.feed.entry;
const entryCount = entries.length;

for (let i = 0; i < entryCount; i++) {
  const currentEntry = entries[i];
  const postTitle = currentEntry.title.$t;
  const postUrl = currentEntry.link.find(link => link.rel === 'alternate').href;

  const listItem = document.createElement('li');
  const link = document.createElement('a');
  link.href = postUrl;
  link.textContent = postTitle;
  
  listItem.appendChild(link);
  fragment.appendChild(listItem); // ★ 가상 DOM에 추가 (리플로우 없음!)
}

// 모든 작업이 끝난 후, 실제 DOM에 단 한 번만 추가!
document.getElementById('sitemap-container').appendChild(fragment);

이 방식은 .innerHTML을 사용하는 것보다 더 안전하고(XSS 공격 방지 측면에서) 프로그래밍적으로도 명확하여 복잡한 DOM 구조를 생성할 때 특히 유용합니다. 이것이야말로 진정한 느린 웹사이트 속도 자바스크립트 해결법의 정수 중 하나입니다.

2. 목적에 맞는 반복문 선택하기

JavaScript에는 다양한 종류의 반복문이 있습니다. for, forEach, for...of, while 등. 각각의 특성과 성능상 미묘한 차이를 이해하고 상황에 맞게 사용하는 것도 중요합니다.

반복문 종류 장점 단점 주 사용처 성능
전통 for 루프 가장 빠름. break, continue 사용 가능. 코드가 다소 길고, 인덱스 변수 관리가 필요함. 성능이 극도로 중요한 대규모 배열 순회. 최상
forEach 코드가 간결하고 가독성이 좋음. 함수형 프로그래밍 스타일. break, continue 사용 불가. 전통 for 루프보다 약간 느림. 배열의 모든 요소를 순회하며 특정 작업을 수행할 때. 양호
for...of Array, Map, Set 등 반복 가능한(iterable) 객체에 모두 사용 가능. 코드가 직관적. 인덱스가 필요하면 별도 변수 필요. 객체 순회에는 부적합. 배열의 '값'에만 관심이 있을 때. 좋음
for...in 객체의 열거 가능한(enumerable) 속성을 순회. 매우 느림. 프로토타입 체인까지 순회하며, 순서를 보장하지 않음. 배열 순회에 절대 사용 금지! 객체의 키(key)를 순회할 때 (디버깅용으로 주로 사용). 나쁨

제 경우에는 수백, 수천 개의 데이터를 처리해야 하고 성능이 가장 중요했기 때문에 전통적인 for 루프를 선택한 것이 합리적이었습니다. 만약 코드의 가독성이 더 중요하고 데이터 양이 적다면 forEachfor...of도 훌륭한 대안이 될 수 있습니다. 중요한 것은 각 도구의 특성을 이해하고 전략적으로 선택하는 것입니다.

결과: 놀랍도록 빨라진 사이트맵과 값진 교훈

이러한 코드 최적화 작업들을 적용한 결과는 기대 이상이었습니다. 이전에는 5초 이상 걸려 답답함을 유발했던 사이트맵 페이지가, 이제는 1초 이내에 모든 목록을 그려내며 방문자를 맞이하게 되었습니다. 체감상으로는 거의 즉시 로딩되는 것처럼 느껴졌습니다. 이 작은 성공은 단순한 속도 개선을 넘어, 성능 최적화의 중요성과 매력을 다시 한번 일깨워주는 계기가 되었습니다.

자바스크립트 최적화 후 블로거 사이트맵의 현저히 빨라진 로딩 속도를 보여주는 GIF
최적화 이후, 눈에 띄게 빨라진 사이트맵 로딩 속도

물론 여전히 개선의 여지는 남아있습니다. 근본적으로 블로거 피드를 가져오는 네트워크 요청 시간 자체를 줄일 수는 없으니까요. 하지만 적어도 가져온 데이터를 처리하고 화면에 그리는 과정에서 발생하는 병목은 성공적으로 해결했습니다. 사용자가 느끼는 '체감 성능'을 극적으로 끌어올린 것이죠.

이번 최적화 프로젝트의 핵심 교훈

  • 측정 없이는 개선도 없다: 감에 의존하지 말고, 브라우저 개발자 도구의 Performance 탭을 적극 활용하여 병목 구간을 정확히 찾아내라.
  • 기본으로 돌아가라: 화려한 라이브러리나 프레임워크가 아닌, JavaScript의 기본적인 동작 원리(스코프, DOM, 이벤트 루프)에 대한 이해가 성능의 기반이 된다.
  • 코드는 기계가 아닌 사람을 위한 것, 그러나...: 가독성도 중요하지만, 성능이 중요한 구간에서는 기계(브라우저)가 어떻게 더 효율적으로 일할 수 있을지 고민해야 한다.
  • 작은 습관이 큰 차이를 만든다: .length 캐싱과 같은 사소해 보이는 습관이 모여 견고하고 빠른 애플리케이션을 만든다.

이 글에서 다룬 웹사이트 로딩 시간 단축 코드 예제들은 비단 블로거 사이트맵뿐만 아니라, 대량의 데이터를 동적으로 처리하는 모든 웹 애플리케이션에 적용될 수 있는 보편적인 원칙입니다. 혹시 여러분도 원인 모를 성능 저하로 고민하고 있다면, 오늘 당장 여러분의 코드 속 반복문을 한번 점검해보는 것은 어떨까요? 아마 생각지도 못한 곳에서 성능을 갉아먹고 있는 범인을 발견하게 될지도 모릅니다. 더 좋은 최적화 아이디어가 있다면 댓글로 공유해주시면 저에게도 큰 도움이 될 것 같습니다!

```

Post a Comment