JavaScript 코드 성능을 향상시키는 방법

발행: (2025년 12월 19일 오후 04:56 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

1. Understand the JavaScript Execution Model

현대 JavaScript 엔진(V8, SpiderMonkey, JavaScriptCore)은 Just‑In‑Time(JIT) 컴파일, 인라인 캐싱, 숨겨진 클래스/쉐이프, 그리고 추측 최적화에 의존합니다.

Avoid De‑Optimization Triggers

엔진은 관찰된 타입을 기반으로 함수들을 최적화합니다. 실행 중에 타입을 변경하면 디옵티마이즈가 발생합니다.

// Avoid this
function sum(a, b) {
  return a + b;
}

sum(1, 2);       // optimized for numbers
sum("1", "2");   // de‑optimizes

// Do this
function sum(a, b) {
  a = Number(a);
  b = Number(b);
  return a + b;
}

2. Shape Stability and Object Access

JavaScript 객체는 숨겨진 클래스를 사용합니다. 객체 구조를 동적으로 변경하면 속성 접근 최적화가 방해됩니다. 대규모 데이터셋을 순회할 때, 형태가 안정적이면 20~50 % 정도 성능 향상을 얻을 수 있습니다.

// Avoid this
const user = {};
user.name = "Alice";
user.age = 30;
user.isAdmin = true;

// Do this
const user = {
  name: "Alice",
  age: 30,
  isAdmin: true
};

3. Minimize Garbage Collection Pressure

가비지 컬렉션(GC)은 실행을 일시 중지합니다. 높은 할당률은 빈번한 GC 사이클을 초래합니다. 핫 경로에서 불필요한 할당을 피하세요.

// Excessive allocation
function process(items) {
  return items.map(item => ({
    id: item.id,
    value: item.value * 2
  }));
}

// Object reuse
function process(items) {
  const result = new Array(items.length);
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    result[i] = {
      id: item.id,
      value: item.value * 2
    };
  }
  return result;
}

Loop vs. Functional Methods

// Using map / filter / reduce
const total = items
  .filter(p => p > 10)
  .map(p => p * 1.2)
  .reduce((a, b) => a + b, 0);

// Optimized loop
let total = 0;
for (let i = 0; i < items.length; i++) {
  const p = items[i];
  if (p > 10) {
    total += p * 1.2;
  }
}

5. Async Performance: Avoid Accidental Serialization

async/await는 잘못 사용하면 숨겨진 병목을 초래할 수 있습니다. 적절한 병렬 처리를 사용하면 실행 시간을 O(n × latency)에서 O(max latency)로 줄일 수 있습니다.

// Serialized execution
async function fetchAll(urls) {
  const results = [];
  for (const url of urls) {
    results.push(await fetch(url));
  }
  return results;
}

// Parallel execution
async function fetchAll(urls) {
  return Promise.all(urls.map(fetch));
}

6. Memoization and Cache Invalidation

결정적이고 비용이 많이 드는 함수에 대해 재계산을 피하세요. 메모이제이션은 캐시 크기 제한과 명확한 무효화 규칙과 함께 사용해야 하며, 그렇지 않으면 메모리 누수가 이득을 상쇄합니다.

const cache = new Map();

function expensiveCalc(n) {
  if (cache.has(n)) return cache.get(n);

  let result = 0;
  for (let i = 0; i < n; i++) {
    result += Math.sqrt(i);
  }

  cache.set(n, result);
  return result;
}

DOM Batching (Read/Write Separation)

// Bad: interleaved reads and writes
elements.forEach(el => {
  el.style.width = el.offsetWidth + 10 + "px";
});

// Good: batch reads then writes
const widths = elements.map(el => el.offsetWidth);

elements.forEach((el, i) => {
  el.style.width = widths[i] + 10 + "px";
});

8. Measure, Don’t Guess

사용 가능한 도구와 API를 활용해 성능을 측정하세요. 측정 없이 최적화하는 것은 카고 컬트 엔지니어링과 같습니다.

  • performance.now()
  • Chrome DevTools Performance tab
  • Node.js --prof
  • Flamegraphs
const start = performance.now();
// critical code
const end = performance.now();
console.log(`Execution: ${end - start}ms`);

당신의 코드는 이 시스템들과 협력하거나 적극적으로 대립합니다. 안정된 타입, 안정된 형태, 제어된 메모리 할당, 그리고 의도적인 async 동작은 엔진이 적극적으로 최적화하도록 합니다.

Back to Blog

관련 글

더 보기 »

베어 메탈 프론트엔드

Bare-metal frontend 소개 현대 프론트엔드 애플리케이션은 매우 풍부하고 복잡하며 정교해졌습니다. 단순히 데이터를 폴링하는 UI만은 아닙니다. 그들은…