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

[톺아보기] Vercel의 react-best-practices #8 - Rendering Performance

reactrenderingperformancehydrationcssoptimization

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

titleimpactimpactDescriptiontags
Show/Hide에 Activity Component 사용MEDIUMstate/DOM 보존rendering, activity, visibility, state-preservation

Show/Hide에 Activity Component 사용

리소스 집약적인 components의 visibility 토글을 관리하는 솔루션으로 React의 <Activity> component를 활용하세요. 이 접근법은 visibility 전환 중 component state와 DOM 요소 모두를 유지합니다.

사용 예시:

import { Activity } from "react";
 
function Dropdown({ isOpen }: Props) {
  return (
    <Activity mode={isOpen ? "visible" : "hidden"}>
      <ExpensiveMenu />
    </Activity>
  );
}

이 기법은 빈번한 show/hide 사이클을 거치는 components를 감싸서 비용 큰 re-renders와 state 손실을 방지합니다. component tree를 unmount하지 않고 불필요한 재계산을 방지하며 internal state 관리를 보존합니다.



titleimpactimpactDescriptiontags
SVG Element 대신 SVG Wrapper 애니메이션LOW하드웨어 가속 활성화rendering, svg, css, animation, performance

SVG Element 대신 SVG Wrapper 애니메이션

많은 브라우저가 SVG elements에서 CSS3 animations에 대한 하드웨어 가속을 지원하지 않습니다. 이 성능 문제를 해결하려면 SVG graphics를 <div> container 안에 넣고 SVG 자체가 아닌 wrapper에 animations를 적용하세요.

Incorrect (SVG elements 직접 애니메이션):

function AnimatedIcon() {
  return (
    <svg className="animate-spin">
      <circle cx="50" cy="50" r="40" />
    </svg>
  );
}

Correct (wrapper div 애니메이션):

function AnimatedIcon() {
  return (
    <div className="animate-spin">
      <svg>
        <circle cx="50" cy="50" r="40" />
      </svg>
    </div>
  );
}

지원되는 Properties:

  • transform
  • opacity
  • translate
  • scale
  • rotate

wrapper div를 사용하면 브라우저가 animation 렌더링을 GPU로 오프로드하여 SVG elements를 직접 애니메이션하는 CPU 기반 렌더링에 비해 더 나은 성능과 부드러운 시각적 경험을 제공합니다.



titleimpactimpactDescriptiontags
명시적 조건부 렌더링 사용LOW0 또는 NaN 렌더링 방지rendering, conditional, jsx, falsy-values

명시적 조건부 렌더링 사용

JSX에서 components를 조건부로 렌더링할 때, && operator는 예상치 못한 output을 생성할 수 있습니다. 조건이 0, NaN, 또는 렌더되는 다른 falsy 값일 수 있을 때는 && 대신 명시적 ternary operators (? :)를 사용하세요.

Incorrect (0이 화면에 렌더됨):

function NotificationBadge({ count }: { count: number }) {
  return <div>{count && <span className="badge">{count}</span>}</div>;
}
// count가 0일 때, "0"이 DOM에 렌더됨

Correct (null이 제대로 처리됨):

function NotificationBadge({ count }: { count: number }) {
  return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
}
// count가 0일 때, 아무것도 렌더되지 않음

문제는 &&가 만나는 첫 번째 falsy 값을 반환하기 때문에 발생합니다. count가 0일 때, 표현식 count &&는 0으로 평가되고, 이것이 렌더링을 방지하는 falsy 값으로 처리되지 않고 DOM에 text로 렌더됩니다.



titleimpactimpactDescriptiontags
긴 Lists에 CSS content-visibility 적용HIGH더 빠른 초기 렌더rendering, css, content-visibility, long-lists

긴 Lists에 CSS content-visibility 적용

content-visibility: auto를 적용하여 off-screen 렌더링을 지연시키세요.

CSS:

.message-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 80px;
}

Example:

function MessageList({ messages }: { messages: Message[] }) {
  return (
    <div className="h-screen overflow-y-auto">
      {messages.map(msg => (
        <div key={msg.id} className="message-item">
          <Avatar user={msg.author} />
          <div>{msg.content}</div>
        </div>
      ))}
    </div>
  );
}

1000개 메시지의 경우, 브라우저가 ~990개 off-screen 항목의 layout/paint를 건너뜁니다 (10배 빠른 초기 렌더).



titleimpactimpactDescriptiontags
Static JSX Elements 끌어올리기LOW불필요한 재생성 방지rendering, jsx, static, optimization

Static JSX Elements 끌어올리기

렌더 중 불필요한 재생성을 방지하기 위해 static JSX를 components 외부로 추출하세요.

JSX elements가 component 함수 내부에 정의되면, 내용이 절대 변경되지 않더라도 component가 렌더될 때마다 재생성됩니다.

Incorrect (매 렌더마다 재생성):

function LoadingSkeleton() {
  return <div className="h-20 animate-pulse bg-gray-200" />;
}
 
function Container() {
  return <div>{loading && <LoadingSkeleton />}</div>;
}

Correct (한 번 생성, 참조로 사용):

const loadingSkeleton = <div className="h-20 animate-pulse bg-gray-200" />;
 
function Container() {
  return <div>{loading && loadingSkeleton}</div>;
}

이 최적화는 재생성 중 상당한 계산 overhead를 수반하는 복잡하고 변경되지 않는 SVG 구조에 특히 유용합니다.

Modern Considerations: React Compiler가 활성화되면 자동으로 static JSX elements를 끌어올리고 component re-renders를 최적화하여, 이 기능을 사용하는 프로젝트에서는 수동 구현이 불필요해질 수 있습니다.



titleimpactimpactDescriptiontags
Flickering 없이 Hydration Mismatch 방지MEDIUMvisual flicker와 hydration errors 방지rendering, ssr, hydration, localStorage, flicker

Flickering 없이 Hydration Mismatch 방지

client-side storage 작업 시, 개발자들은 딜레마에 직면합니다: server-side에서 localStorage에 접근하면 errors가 발생하고, hydration 후 업데이트하면 visible flickering이 생깁니다. 해결책은 React hydration이 발생하기 전에 DOM을 수정하는 동기 script를 주입하는 것입니다.

Server-side 접근 실패: server rendering 중 localStorage를 읽으려고 하면 Node.js 환경에 API가 존재하지 않아 error가 발생합니다.

Post-hydration 업데이트는 flicker 발생: 저장된 값을 가져오기 위해 useStateuseEffect를 사용하면 component가 처음에 기본 내용으로 렌더되고, hydration 완료 후 업데이트되어 jarring한 visual flash가 발생합니다.

Correct (inline script로 동기 적용):

function ThemeProvider({ children }: { children: React.ReactNode }) {
  return (
    <div id="theme-container">
      <script
        dangerouslySetInnerHTML={{
          __html: `
            (function() {
              try {
                var theme = localStorage.getItem('theme') || 'light';
                document.getElementById('theme-container').className = theme;
              } catch (e) {}
            })();
          `,
        }}
      />
      {children}
    </div>
  );
}

inline script는 element를 보여주기 전에 동기적으로 실행되어 DOM이 이미 올바른 값을 가지도록 보장합니다. Flickering도 없고, hydration mismatch도 없습니다.

사용 사례:

  • Theme toggles
  • User preferences
  • Authentication states
  • 즉각적인 렌더링이 필요한 모든 client-only 데이터


titleimpactimpactDescriptiontags
예상되는 Hydration Mismatches 억제LOW-MEDIUM알려진 차이에 대한 noisy hydration warnings 방지rendering, hydration, ssr, nextjs

예상되는 Hydration Mismatches 억제

Next.js 같은 server-side rendering frameworks에서, random identifiers, timestamps, locale-specific formatting 같은 특정 값들은 server와 client renders 간에 의도적으로 다릅니다. 불필요한 console warnings를 제거하기 위해 이런 예상되는 mismatches를 suppressHydrationWarning으로 감싸는 것을 권장합니다. 단, 이 도구는 알려진 불일치에만 적용하고 진짜 버그를 감추는 수단으로 남용해서는 안 됩니다.

Incorrect (warnings 생성):

function Timestamp() {
  return <span>{new Date().toLocaleString()}</span>;
}

Correct (예상되는 warning 억제):

function Timestamp() {
  return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>;
}

핵심 차이점은 suppressHydrationWarning이 dynamic content를 자체 element 내에 격리하여 framework가 해당 특정 mismatch에 대한 validation을 건너뛰면서 component tree의 다른 곳에서 의도하지 않은 차이에 대한 warnings는 보존하도록 한다는 것입니다.



titleimpactimpactDescriptiontags
React DOM Resource Hints 사용HIGHcritical resources의 load time 감소rendering, preload, preconnect, prefetch, performance

React DOM Resource Hints 사용

React DOM은 브라우저에 필요한 resources를 hint하는 APIs를 제공합니다. 이는 client가 HTML을 받기도 전에 resources를 loading 시작하는 server components에서 특히 유용합니다.

  • prefetchDNS(href): 연결 예정인 domain의 DNS 해석
  • preconnect(href): server에 connection 수립 (DNS + TCP + TLS)
  • preload(href, options): 곧 사용할 resource (stylesheet, font, script, image) fetch
  • preloadModule(href): 곧 사용할 ES module fetch
  • preinit(href, options): stylesheet나 script를 fetch하고 evaluate
  • preinitModule(href): ES module을 fetch하고 evaluate

Example (third-party APIs에 preconnect):

import { preconnect, prefetchDNS } from "react-dom";
 
export default function App() {
  prefetchDNS("https://analytics.example.com");
  preconnect("https://api.example.com");
 
  return <main>{/* content */}</main>;
}

Example (critical fonts와 styles preload):

import { preload, preinit } from "react-dom";
 
export default function RootLayout({ children }) {
  // font file preload
  preload("/fonts/inter.woff2", {
    as: "font",
    type: "font/woff2",
    crossOrigin: "anonymous",
  });
 
  // critical stylesheet를 즉시 fetch하고 적용
  preinit("/styles/critical.css", { as: "style" });
 
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

Example (code-split routes에 modules preload):

import { preloadModule, preinitModule } from "react-dom";
 
function Navigation() {
  const preloadDashboard = () => {
    preloadModule("/dashboard.js", { as: "script" });
  };
 
  return (
    <nav>
      <a href="/dashboard" onMouseEnter={preloadDashboard}>
        Dashboard
      </a>
    </nav>
  );
}

각각 사용 시점:

API사용 사례
prefetchDNS나중에 연결할 third-party domains
preconnect즉시 fetch할 APIs나 CDNs
preload현재 페이지에 필요한 critical resources
preloadModule다음 navigation 가능성이 높은 JS modules
preinit일찍 실행해야 하는 stylesheets/scripts
preinitModule일찍 실행해야 하는 ES modules

Reference: React DOM Resource Preloading APIs



titleimpactimpactDescriptiontags
Script Tags에 defer 또는 async 사용HIGHrender-blocking 제거rendering, script, defer, async, performance

Script Tags에 defer 또는 async 사용

deferasync 없는 script tags는 script가 download되고 실행되는 동안 HTML parsing을 블로킹합니다. 이는 First Contentful Paint와 Time to Interactive를 지연시킵니다.

  • defer: 병렬로 download, HTML parsing 완료 후 실행, 실행 순서 유지
  • async: 병렬로 download, 준비되면 즉시 실행, 순서 보장 없음

DOM이나 다른 scripts에 의존하는 scripts에는 defer를 사용하세요. analytics 같은 독립적인 scripts에는 async를 사용하세요.

Incorrect (렌더링 블로킹):

export default function Document() {
  return (
    <html>
      <head>
        <script src="https://example.com/analytics.js" />
        <script src="/scripts/utils.js" />
      </head>
      <body>{/* content */}</body>
    </html>
  );
}

Correct (non-blocking):

export default function Document() {
  return (
    <html>
      <head>
        {/* 독립적인 script - async 사용 */}
        <script src="https://example.com/analytics.js" async />
        {/* DOM 의존 script - defer 사용 */}
        <script src="/scripts/utils.js" defer />
      </head>
      <body>{/* content */}</body>
    </html>
  );
}

Note: Next.js에서는 raw script tags 대신 strategy prop과 함께 next/script component를 선호하세요:

import Script from "next/script";
 
export default function Page() {
  return (
    <>
      <Script src="https://example.com/analytics.js" strategy="afterInteractive" />
      <Script src="/scripts/utils.js" strategy="beforeInteractive" />
    </>
  );
}

Reference: MDN - Script element



titleimpactimpactDescriptiontags
SVG Precision 최적화LOW파일 크기 감소rendering, svg, optimization, svgo

SVG Precision 최적화

파일 크기를 최소화하기 위해 SVG 좌표 precision을 줄이세요. 적절한 precision level은 viewBox dimensions에 따라 다르지만, precision을 낮추는 것이 일반적으로 권장됩니다.

Incorrect (과도한 decimal places):

<path d="M 10.293847 20.847362 L 30.938472 40.192837" />

Correct (single decimal precision):

<path d="M 10.3 20.8 L 30.9 40.2" />

SVGO로 자동화:

npx svgo --precision=1 --multipass icon.svg

이 command는 더 나은 compression 결과를 위해 multipass process로 precision 감소를 적용합니다.



titleimpactimpactDescriptiontags
수동 Loading States 대신 useTransition 사용LOWre-renders 감소와 코드 명확성 향상rendering, transitions, useTransition, loading, state

수동 Loading States 대신 useTransition 사용

수동 useState loading state 관리를 자동으로 transition logic을 처리하는 built-in isPending state를 제공하는 React의 useTransition hook으로 교체하세요.

Incorrect (수동 loading state 관리):

function Search() {
  const [isLoading, setIsLoading] = useState(false);
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
 
  const handleSearch = async (value: string) => {
    setIsLoading(true);
    setQuery(value);
    try {
      const data = await fetchResults(value);
      setResults(data);
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    <>
      <input onChange={e => handleSearch(e.target.value)} />
      {isLoading ? <Spinner /> : <Results data={results} />}
    </>
  );
}

Correct (useTransition 사용):

import { useTransition, useState } from "react";
 
function Search() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
 
  const handleSearch = async (value: string) => {
    startTransition(async () => {
      setQuery(value);
      const data = await fetchResults(value);
      setResults(data);
    });
  };
 
  return (
    <>
      <input onChange={e => handleSearch(e.target.value)} />
      {isPending ? <Spinner /> : <Results data={results} />}
    </>
  );
}

주요 Benefits:

  • 자동 pending state: 수동으로 setIsLoading(true/false)를 관리할 필요 없음
  • Transitions에서 errors가 발생해도 resilience
  • 업데이트 중 향상된 UI responsiveness
  • 새 transitions가 자동으로 pending transitions를 취소

Reference: React useTransition