$ yh.log
[톺아보기] Vercel의 react-best-practices #9 - JavaScript Performance

[톺아보기] Vercel의 react-best-practices #9 - JavaScript Performance

javascriptperformanceoptimizationarrayscachingdom

작성자 : 오예환 | 작성일 : 2026-03-21 | 수정일 : 2026-03-21 | 조회수 :

titleimpactimpactDescriptiontags
Layout Thrashing 방지MEDIUMforced synchronous layouts 방지, 성능 병목 감소javascript, dom, css, performance, reflow, layout-thrashing

Layout Thrashing 방지

style writes와 layout reads를 interleaving하지 마세요. style 변경 사이에 layout properties를 읽으면, 브라우저가 synchronous reflow를 트리거해야 하므로 성능 병목이 발생합니다.

Incorrect (layout thrashing 발생):

function updateElements(elements: HTMLElement[]) {
  elements.forEach(el => {
    el.style.width = "100px"; // write
    const height = el.offsetHeight; // read - forces reflow!
    el.style.height = `${height * 2}px`; // write
  });
}

Correct (reads와 writes 분리):

function updateElements(elements: HTMLElement[]) {
  // 먼저 모든 값 읽기
  const heights = elements.map(el => el.offsetHeight);
 
  // 그 다음 모든 styles 쓰기
  elements.forEach((el, i) => {
    el.style.width = "100px";
    el.style.height = `${heights[i] * 2}px`;
  });
}

CSS Classes 사용 (더 나은 방법):

function updateElements(elements: HTMLElement[]) {
  elements.forEach(el => {
    el.classList.add("expanded");
  });
}

핵심 패턴:

  1. writes를 함께 batching - 브라우저가 reflow 계산을 최적화할 수 있음
  2. read와 write phases 분리 - 모든 layout measurements를 먼저 수행, 그 다음 style 변경 적용
  3. inline styles 대신 CSS classes 사용 - 브라우저 caching 활용, layout thrashing 완전 방지

React 애플리케이션에서는 useEffect hooks에서 inline styles를 조작하는 대신 className props를 통해 CSS classes를 토글하는 것을 선호하세요.



titleimpactimpactDescriptiontags
반복되는 함수 호출 캐싱MEDIUM중복 계산 방지javascript, caching, memoization, performance

반복되는 함수 호출 캐싱

렌더링 중 동일한 inputs가 반복적으로 발생할 때 중복 함수 실행을 방지하기 위해 module-level caching을 사용하세요.

Incorrect (캐싱 없이 동일 inputs로 100+ 번 호출):

function ProjectList({ projects }: Props) {
  return (
    <ul>
      {projects.map(p => (
        <li key={p.id}>
          {/* slugify가 동일한 이름으로 여러 번 호출될 수 있음 */}
          <a href={`/project/${slugify(p.name)}`}>{p.name}</a>
        </li>
      ))}
    </ul>
  );
}

Correct (Map 기반 caching):

const slugCache = new Map<string, string>();
 
function slugify(name: string): string {
  if (!slugCache.has(name)) {
    // 비용 큰 계산
    const slug = name.toLowerCase().replace(/\s+/g, "-");
    slugCache.set(name, slug);
  }
  return slugCache.get(name)!;
}

단일 값 caching (output이 하나일 때):

let currentUser: User | null = null;
 
function getCurrentUser(): User | null {
  if (currentUser === null) {
    currentUser = fetchUserFromSession();
  }
  return currentUser;
}
 
// 데이터가 변경되면 cache 초기화
function onLogout() {
  currentUser = null;
}

핵심 원칙: hook이 아닌 Map을 사용하여 어디서든 작동하게 하세요: utilities, event handlers, React components 외부에서도 사용 가능합니다.



titleimpactimpactDescriptiontags
Loops에서 Property Access 캐싱LOW-MEDIUMlookups 감소javascript, loops, optimization, caching

Loops에서 Property Access 캐싱

hot paths에서 object property lookups를 캐싱하세요.

Incorrect (3 lookups × N iterations):

for (let i = 0; i < arr.length; i++) {
  process(obj.config.settings.value);
}

Correct (총 1 lookup):

const value = obj.config.settings.value;
const len = arr.length;
for (let i = 0; i < len; i++) {
  process(value);
}


titleimpactimpactDescriptiontags
Storage API 호출 캐싱LOW-MEDIUM비용 큰 I/O 감소javascript, localStorage, storage, caching, performance

Storage API 호출 캐싱

localStorage, sessionStorage, document.cookie는 동기적이고 비용이 큽니다. memory에 reads를 캐싱하세요.

Incorrect (caching 없이 반복 접근):

function getTheme() {
  return localStorage.getItem("theme") ?? "light";
}
// 10번 호출 = 10번 storage reads

Correct (Map 기반 Cache):

const storageCache = new Map<string, string | null>();
 
function getLocalStorage(key: string) {
  if (!storageCache.has(key)) {
    storageCache.set(key, localStorage.getItem(key));
  }
  return storageCache.get(key);
}
 
function setLocalStorage(key: string, value: string) {
  localStorage.setItem(key, value);
  storageCache.set(key, value); // cache 동기화 유지
}

components와 handlers 전체에서 utility-wide 가용성을 위해 hooks가 아닌 Map을 사용하세요.

Cookie Caching:

let cookieCache: Record<string, string> | null = null;
 
function getCookie(name: string) {
  if (!cookieCache) {
    cookieCache = Object.fromEntries(document.cookie.split("; ").map(c => c.split("=")));
  }
  return cookieCache[name];
}

Cache Invalidation:

// storage가 외부에서 변경되면 cache 초기화
window.addEventListener("storage", e => {
  if (e.key) storageCache.delete(e.key);
});
 
document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "visible") {
    storageCache.clear();
  }
});


titleimpactimpactDescriptiontags
여러 배열 Iterations 결합LOW-MEDIUMiterations 감소javascript, arrays, loops, performance

여러 배열 Iterations 결합

동일한 배열에서 여러 .filter() 또는 .map() methods를 호출하면 불필요한 성능 overhead가 발생합니다. 각 작업에 대해 배열을 별도로 순회하는 대신, logic을 단일 loop로 통합하는 것이 더 효율적입니다.

Incorrect (3번 iterations):

const active = users.filter(u => u.isActive);
const admins = users.filter(u => u.role === "admin");
const verified = users.filter(u => u.isVerified);

Correct (1번 iteration):

const active: User[] = [];
const admins: User[] = [];
const verified: User[] = [];
 
for (const user of users) {
  if (user.isActive) active.push(user);
  if (user.role === "admin") admins.push(user);
  if (user.isVerified) verified.push(user);
}

이 최적화는 여러 passes의 누적 비용이 더 눈에 띄는 대규모 datasets 작업 시 특히 유용합니다.



titleimpactimpactDescriptiontags
함수에서 Early ReturnLOW-MEDIUM불필요한 계산 방지javascript, functions, optimization, early-return

함수에서 Early Return

결과가 결정되면 불필요한 처리를 건너뛰기 위해 early return하세요.

Incorrect (답을 찾은 후에도 모든 items 처리):

function validateUsers(users: User[]) {
  let hasError = false;
  let errorMessage = "";
 
  for (const user of users) {
    if (!user.email) {
      hasError = true;
      errorMessage = "Email required";
    }
    if (!user.name) {
      hasError = true;
      errorMessage = "Name required";
    }
    // error가 발견된 후에도 모든 users 계속 확인
  }
 
  return hasError ? { valid: false, error: errorMessage } : { valid: true };
}

Correct (첫 error에서 즉시 return):

function validateUsers(users: User[]) {
  for (const user of users) {
    if (!user.email) {
      return { valid: false, error: "Email required" };
    }
    if (!user.name) {
      return { valid: false, error: "Name required" };
    }
  }
 
  return { valid: true };
}


titleimpactimpactDescriptiontags
flatMap으로 Map과 Filter를 한 번에LOW-MEDIUM중간 배열 제거javascript, arrays, flatmap, performance

flatMap으로 Map과 Filter를 한 번에

.map().filter(Boolean)을 chaining하면 중간 배열이 생성되고 두 번 순회합니다. .flatMap()을 사용하여 한 번의 pass로 transform과 filter를 수행하세요.

Incorrect (2 iterations, 중간 배열):

const userNames = users.map(user => (user.isActive ? user.name : null)).filter(Boolean);

Correct (1 iteration, 중간 배열 없음):

const userNames = users.flatMap(user => (user.isActive ? [user.name] : []));

더 많은 예시:

// responses에서 valid emails 추출
// Before
const emails = responses.map(r => (r.success ? r.data.email : null)).filter(Boolean);
 
// After
const emails = responses.flatMap(r => (r.success ? [r.data.email] : []));
 
// valid numbers parse 및 filter
// Before
const numbers = strings.map(s => parseInt(s, 10)).filter(n => !isNaN(n));
 
// After
const numbers = strings.flatMap(s => {
  const n = parseInt(s, 10);
  return isNaN(n) ? [] : [n];
});

사용 시점:

  • items를 transform하면서 일부를 filter out
  • 일부 inputs가 output을 생성하지 않는 conditional mapping
  • invalid inputs를 건너뛰어야 하는 parsing/validating


titleimpactimpactDescriptiontags
RegExp 생성 끌어올리기LOW-MEDIUM재생성 방지javascript, regexp, optimization, memoization

RegExp 생성 끌어올리기

render 안에서 RegExp를 생성하지 마세요. module scope로 끌어올리거나 useMemo()로 memoize하세요.

Incorrect (매 render마다 new RegExp):

function Highlighter({ text, query }: Props) {
  const regex = new RegExp(`(${query})`, "gi");
  const parts = text.split(regex);
  return <>{parts.map((part, i) => ...)}</>;
}

Correct (memoize 또는 hoist):

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
 
function Highlighter({ text, query }: Props) {
  const regex = useMemo(
    () => new RegExp(`(${escapeRegex(query)})`, "gi"),
    [query]
  );
  const parts = text.split(regex);
  return <>{parts.map((part, i) => ...)}</>;
}

Warning (global regex는 mutable state를 가짐):

Global regex (/g)는 mutable lastIndex state를 가집니다:

const regex = /foo/g;
regex.test("foo"); // true, lastIndex = 3
regex.test("foo"); // false, lastIndex = 0


titleimpactimpactDescriptiontags
반복 Lookups를 위한 Index Maps 구축MEDIUM-HIGHO(n)에서 O(1)로javascript, map, lookup, performance

반복 Lookups를 위한 Index Maps 구축

이 최적화 기법은 흔한 성능 문제를 해결합니다: 동일한 key로 같은 배열에서 .find()를 반복 호출하는 것.

문제점:

배열에서 .find()를 여러 번 호출하면, 각 호출이 전체 배열을 스캔해야 합니다—O(n) 연산. 여러 items에 대해 이렇게 하면 복잡도가 곱해집니다.

해결책:

배열에서 한 번 Map을 생성하고, lookup key를 map key로 사용하세요. 한 번 Map 구축 (O(n)), 이후 모든 lookups는 O(1).

Incorrect (N × M 연산):

const orders = getOrders(); // 1000개 orders
const users = getUsers(); // 1000명 users
 
const enriched = orders.map(order => ({
  ...order,
  user: users.find(u => u.id === order.userId), // 매번 1000명 스캔
}));
// 1000 orders × 1000 users = 1,000,000 연산

Correct (N + M 연산):

const orders = getOrders();
const users = getUsers();
 
// 한 번의 iteration으로 Map 구축
const userMap = new Map(users.map(u => [u.id, u]));
 
const enriched = orders.map(order => ({
  ...order,
  user: userMap.get(order.userId), // O(1) lookup
}));
// 1000 + 1000 = 2,000 연산

효과: 1000 orders × 1000 users의 경우: 1M ops → 2K ops



titleimpactimpactDescriptiontags
배열 비교에서 Length 먼저 확인MEDIUM-HIGH불필요한 비용 큰 연산 방지javascript, arrays, performance, optimization, comparison

배열 비교에서 Length 먼저 확인

sorting이나 deep equality checks 같은 비용 큰 연산을 포함하는 배열 비교 시, 먼저 length 비교를 수행하면 상당한 성능 이점을 제공합니다. 다른 lengths의 배열은 같을 수 없으므로, 이것이 이상적인 early exit point입니다.

Incorrect (항상 sort 및 join, lengths가 달라도):

function arraysEqual(a: string[], b: string[]): boolean {
  // 항상 두 개의 O(n log n) sorts + join 연산
  return a.toSorted().join(",") === b.toSorted().join(",");
}

Correct (먼저 O(1) length 확인):

function arraysEqual(a: string[], b: string[]): boolean {
  if (a.length !== b.length) return false;
 
  const sortedA = a.toSorted();
  const sortedB = b.toSorted();
 
  for (let i = 0; i < sortedA.length; i++) {
    if (sortedA[i] !== sortedB[i]) return false;
  }
 
  return true;
}

주요 Benefits:

  • 배열 lengths가 일치하지 않으면 sorting과 joining 방지
  • 대규모 배열의 string concatenation으로 인한 메모리 소비 감소
  • mutation으로부터 원본 배열 보존
  • 차이점 발견 시 early return

이 기법은 배열 비교가 빈번하게 실행되는 event handlers나 rendering loops 같은 성능-critical code paths에서 특히 유용합니다.



titleimpactimpactDescriptiontags
Min/Max에 Sort 대신 Loop 사용LOWO(n log n) 대신 O(n)javascript, arrays, performance, sorting, algorithms

Min/Max에 Sort 대신 Loop 사용

최소 또는 최대 element를 찾는 것은 배열을 한 번만 통과하면 됩니다. 극단값 하나 또는 두 개를 추출하기 위해 전체 collection을 sorting하면 불필요한 O(n log n) 연산으로 계산 리소스가 낭비됩니다.

Incorrect (sort로 min/max 찾기):

function getMaxPrice(items: Item[]): number | null {
  if (items.length === 0) return null;
  const sorted = items.toSorted((a, b) => b.price - a.price);
  return sorted[0].price;
}

Correct (한 번의 loop):

function getMaxPrice(items: Item[]): number | null {
  if (items.length === 0) return null;
 
  let max = items[0].price;
  for (let i = 1; i < items.length; i++) {
    if (items[i].price > max) {
      max = items[i].price;
    }
  }
  return max;
}

min과 max 동시에 찾기:

function getMinMax(items: Item[]): { min: number; max: number } | null {
  if (items.length === 0) return null;
 
  let min = items[0].price;
  let max = items[0].price;
 
  for (let i = 1; i < items.length; i++) {
    const price = items[i].price;
    if (price < min) min = price;
    if (price > max) max = price;
  }
 
  return { min, max };
}

대안 고려사항: Math.min()Math.max()는 spread syntax로 작은 배열에 작동하지만 실질적인 한계가 있습니다—Chrome 143에서 약 124,000개 elements, Safari 18에서 638,000개—loop 접근법이 대규모 datasets에 더 안정적입니다.



titleimpactimpactDescriptiontags
O(1) Lookups에 Set/Map 사용LOW-MEDIUMO(n)에서 O(1)로javascript, set, map, data-structures, performance

O(1) Lookups에 Set/Map 사용

반복적인 membership checks를 위해 배열을 Set/Map으로 변환하세요.

Incorrect (확인당 O(n)):

const allowedIds = ["a", "b", "c" /* ... */];
items.filter(item => allowedIds.includes(item.id));

Correct (확인당 O(1)):

const allowedIds = new Set(["a", "b", "c" /* ... */]);
items.filter(item => allowedIds.has(item.id));


titleimpactimpactDescriptiontags
불변성을 위해 sort() 대신 toSorted() 사용MEDIUM-HIGHReact state에서 mutation 버그 방지javascript, arrays, immutability, react, state, mutation

불변성을 위해 sort() 대신 toSorted() 사용

.sort()는 배열을 in place로 mutate하여 React state와 props에서 버그를 유발할 수 있습니다. mutation 없이 새 sorted 배열을 생성하는 .toSorted()를 사용하세요.

Incorrect (원본 배열 mutate):

function UserList({ users }: { users: User[] }) {
  // users prop 배열을 mutate!
  const sorted = useMemo(() => users.sort((a, b) => a.name.localeCompare(b.name)), [users]);
  return <div>{sorted.map(renderUser)}</div>;
}

Correct (새 배열 생성):

function UserList({ users }: { users: User[] }) {
  // 새 sorted 배열 생성, 원본 unchanged
  const sorted = useMemo(() => users.toSorted((a, b) => a.name.localeCompare(b.name)), [users]);
  return <div>{sorted.map(renderUser)}</div>;
}

React에서 중요한 이유:

  1. Props/state mutations가 React의 immutability model을 깨뜨림 - React는 props와 state가 read-only로 취급되기를 기대합니다
  2. stale closure 버그 유발 - closures (callbacks, effects) 안에서 배열을 mutating하면 예상치 못한 동작 발생

Browser support (older browsers용 fallback):

.toSorted()는 모든 modern browsers에서 사용 가능합니다 (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). older 환경에서는 spread operator 사용:

// older browsers용 fallback
const sorted = [...items].sort((a, b) => a.value - b.value);

기타 immutable 배열 methods:

  • .toSorted() - immutable sort
  • .toReversed() - immutable reverse
  • .toSpliced() - immutable splice
  • .with() - immutable element replacement