포스트 검색

포스트 제목으로 검색합니다

Vercel의 React Best Practices 톺아보기 [1]

ReactNext.jsReact Best PracticesOptimization

Vercel이 지난 1월 React Best Practices라는 에이전트 Skills를 공개했습니다. 비록 Skills라는 포맷으로 나왔지만 React, Next.js 개발자를 위한 성능 최적화 바이블에 가깝다고 느껴졌고 하나씩 살펴보며 학습해보려고 합니다.

React Best Practices

먼저 Vercel의 React Best Practices를 알아보기 전에 소개글의 일부를 가져왔습니다.


저희는 10년 이상 축적해 온 React 및 Next.js 최적화 노하우를 AI 에이전트와 LLM에 최적화된 구조화된 저장소인 react-best-practices 에 담았습니다.

...

우리는 10년 넘게 운영 중인 코드베이스 전반에서 동일한 근본 원인을 목격해 왔습니다.

  • 비동기 작업이 우연히 순차 작업으로 바뀌는 경우
  • 시간이 지남에 따라 규모가 커지는 대형 고객 패키지
  • 필요 이상으로 자주 다시 렌더링되는 구성 요소

이유는 간단합니다: 이것들은 미세한 최적화가 아닙니다. 대기 시간, 버벅거림, 반복 비용 등으로 나타나 모든 사용자 세션에 영향을 미칩니다.

그래서 저희는 이러한 문제점을 더 쉽게 발견하고 더 빠르게 해결할 수 있도록 React 모범 사례 프레임워크를 만들었습니다.

Vercel React Best Practices 공식 소개 문서


Vercel이 10년동안 축적해온 노하우를 공개했다니...!
대 AI 시대에서 성능/최적화 쪽을 사람이 먼저 잘 이해하지 못하면 에이전트에게도 제대로 요구하기 어렵다고 느껴졌습니다.
AI 도구 전용 학습 지침서지만 안 읽고 넘어가기가 어려웠습니다.

본격적으로 저장소안에는 8가지 섹션으로 나누어 성능 범주를 등급으로 분류해놓았습니다.

  1. 워터폴 처리 방식 제거 (비동기식)
    영향: 심각
    설명: 워터폴은 성능 저하의 가장 큰 원인입니다. 각 순차적인 대기 시간은 네트워크 지연 시간을 증가시킵니다. 워터폴을 제거하면 성능 향상 효과가 가장 큽니다.

  2. 번들 크기 최적화 (bundle)
    영향: 매우 중요
    설명: 초기 번들 크기를 줄이면 상호 작용 가능 시간 및 최대 콘텐츠 표시 시간이 향상됩니다.

  3. 서버 측 성능
    영향도: 높음
    설명: 서버 측 렌더링 및 데이터 가져오기를 최적화하여 서버 측 워터폴 현상을 제거하고 응답 시간을 단축합니다.

  4. 클라이언트 측 데이터 가져오기
    영향도: 중간-높음
    설명: 자동 데이터 중복 제거 및 효율적인 데이터 가져오기 패턴을 통해 불필요한 네트워크 요청을 줄입니다.

  5. 리렌더링 최적화(re-render)
    영향도: 중간
    설명: 불필요한 재렌더링을 줄이면 낭비되는 연산이 최소화되고 UI 응답성이 향상됩니다.

  6. 렌더링 성능 (렌더링)
    영향도: 중간
    설명: 렌더링 프로세스를 최적화하면 브라우저가 수행해야 하는 작업량이 줄어듭니다.

  7. 자바스크립트 성능 (js)
    영향도: 낮음-중간
    설명: 자주 실행되는 경로에 대한 미세 최적화는 의미 있는 성능 향상으로 이어질 수 있습니다.

  8. 고급 패턴 (고급)
    영향도: 낮음
    설명: 특정 사례에 적용되는 고급 패턴으로, 신중한 구현이 필요합니다.

React Best Practices 레포지토리

앞으로는 영향도가 높은 주제부터 차례로 짚어 가며 학습해 보려 합니다. 원문 그대로를 번역하지만 이해하기 쉽게 의역이 포함되어 있을 수 있습니다.

Warterfalls 제거 - 매우 중요

1.1 Defer Await Until Needed (await은 꼭 그 결과가 필요해지는 순간까지 미루세요)

  • 영향도: 높음 (HIGH)

await실제로 그 결과가 쓰이는 분기 안으로 옮기세요. 그래야 그 데이터가 필요 없는 경로까지 블로킹하지 않습니다.

잘못된 예 (두 분기 모두 막힘):

async function handleRequest(userId: string, skipProcessing: boolean) { const userData = await fetchUserData(userId); if (skipProcessing) { // 바로 return 하지만 이미 userData를 기다린 뒤임 return { skipped: true }; } // userData를 쓰는 건 이 분기뿐 return processUserData(userData); }

올바른 예 (필요할 때만 대기):

async function handleRequest(userId: string, skipProcessing: boolean) { if (skipProcessing) { // 기다리지 않고 즉시 반환 return { skipped: true }; } // 필요할 때만 fetch const userData = await fetchUserData(userId); return processUserData(userData); }

추가 예시 (조기 반환 최적화):

// 잘못됨: 항상 권한을 가져옴 async function updateResource(resourceId: string, userId: string) { const permissions = await fetchPermissions(userId); const resource = await getResource(resourceId); if (!resource) { return { error: "Not found" }; } if (!permissions.canEdit) { return { error: "Forbidden" }; } return await updateResourceData(resource, permissions); } // 올바름: 필요할 때만 가져옴 async function updateResource(resourceId: string, userId: string) { const resource = await getResource(resourceId); if (!resource) { return { error: "Not found" }; } const permissions = await fetchPermissions(userId); if (!permissions.canEdit) { return { error: "Forbidden" }; } return await updateResourceData(resource, permissions); }

이 최적화는 건너뛰기(스킵) 분기를 자주 타는 경우이거나, await을 뒤로 미룬 연산이 비용이 큰 경우에 특히 효과가 큽니다.

1.2 Dependency-Based Parallelization (의존 관계 병렬화)

  • 영향도: 매우 중요 (CRITICAL / 2~10배 개선)

일부만 서로 의존하는 작업이면 better-all 라이브러리로 병렬을 최대화하세요. 각 작업은 가능한 한 이른 타이밍에 자동으로 시작됩니다.

better-all(shuding/better-all)은 의존 관계만 적어 두면, 각 비동기 작업을 가능한 한 빨리 이어서 시작하도록 스케줄링해 주는 유틸리티입니다.

잘못된 예 (profile이 config까지 불필요하게 기다림):

// user,config는 병렬이지만 다음 줄의 profile은 첫 Promise.all이 끝난 뒤에만 시작됨 const [user, config] = await Promise.all([fetchUser(), fetchConfig()]); const profile = await fetchProfile(user.id); // profile은 user만 필요 (config 완료를 기다릴 필요 없음)

올바른 예 (config와 profile이 병렬로 실행):

import { all } from "better-all"; // user가 준비되면 profile 시작 → config fetch와 시간이 겹칠 수 있음 const { user, config, profile } = await all({ async user() { return fetchUser(); }, async config() { return fetchConfig(); }, async profile() { return fetchProfile((await this.$.user).id); // user에만 의존 }, });

대안 (추가 라이브러리 없이):

프로미스를 먼저 만들고 마지막에 Promise.all()을 해도 됩니다.

const userPromise = fetchUser(); const profilePromise = userPromise.then((user) => fetchProfile(user.id)); // user 이후에만 profile 체인 const [user, config, profile] = await Promise.all([ userPromise, fetchConfig(), // user,profile 흐름과 동시에 실행 profilePromise, ]);

참고: better-all

1.3 Prevent Waterfall Chains in API Routes (API Routes에서 Warterfall 체인 막기)

  • 영향도: 매우 중요 (CRITICAL / 2~10배 개선)