$ yh.log
[톺아보기] Vercel의 react-best-practices #3 - Eliminating Waterfalls (CRITICAL) 관련 Rule

[톺아보기] Vercel의 react-best-practices #3 - Eliminating Waterfalls (CRITICAL) 관련 Rule

ReactNext.jsPerformanceWaterfallSuspensePromise.all

작성자 : 오예환 | 작성일 : 2026-01-19 | 수정일 : 2026-01-19 | 조회수 :

async-api-routes.md

titleimpactimpactDescriptiontags
API Routes에서 Waterfall Chains 방지CRITICAL2-10배 개선api-routes, server-actions, waterfalls, parallelization

API Routes에서 Waterfall Chains 방지

API routes와 Server Actions에서 독립적인 작업은 아직 await하지 않더라도 즉시 시작하세요.

Incorrect (config가 auth를 기다리고, data가 둘 다 기다림):

export async function GET(request: Request) {
  const session = await auth();
  const config = await fetchConfig();
  const data = await fetchData(session.user.id);
  return Response.json({ data, config });
}

Correct (auth와 config가 즉시 시작됨):

export async function GET(request: Request) {
  const sessionPromise = auth();
  const configPromise = fetchConfig();
  const session = await sessionPromise;
  const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]);
  return Response.json({ data, config });
}

더 복잡한 dependency chains이 있는 작업의 경우, better-all을 사용하여 자동으로 병렬성을 최대화하세요 (Dependency-Based Parallelization 참조).


async-defer-await.md

titleimpactimpactDescriptiontags
필요할 때까지 Await 지연HIGH사용되지 않는 코드 경로 blocking 방지async, await, conditional, optimization

필요할 때까지 Await 지연

await 작업을 실제로 사용되는 분기로 이동하여 필요하지 않은 코드 경로를 blocking하는 것을 피하세요.

Incorrect (두 분기 모두 block됨):

async function handleRequest(userId: string, skipProcessing: boolean) {
  const userData = await fetchUserData(userId);
 
  if (skipProcessing) {
    // 즉시 반환하지만 여전히 userData를 기다림
    return { skipped: true };
  }
 
  // 이 분기만 userData를 사용함
  return processUserData(userData);
}

Correct (필요할 때만 block됨):

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

또 다른 예시 (early return 최적화):

// Incorrect: 항상 permissions를 fetch함
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);
}
 
// Correct: 필요할 때만 fetch함
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);
}

이 최적화는 skip되는 분기가 자주 실행되거나, 지연된 작업이 비용이 클 때 특히 가치가 있습니다.


async-dependencies.md

titleimpactimpactDescriptiontags
Dependency 기반 병렬화CRITICAL2-10배 개선async, parallelization, dependencies, better-all

Dependency 기반 병렬화

부분적인 dependencies가 있는 작업의 경우, better-all을 사용하여 병렬성을 최대화하세요. 각 task를 가능한 가장 빠른 시점에 자동으로 시작합니다.

Incorrect (profile이 config를 불필요하게 기다림):

const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
const profile = await fetchProfile(user.id);

Correct (config와 profile이 병렬로 실행됨):

import { all } from "better-all";
 
const { user, config, profile } = await all({
  async user() {
    return fetchUser();
  },
  async config() {
    return fetchConfig();
  },
  async profile() {
    return fetchProfile((await this.$.user).id);
  },
});

Reference: https://github.com/shuding/better-all


async-parallel.md

titleimpactimpactDescriptiontags
독립적인 작업을 위한 Promise.all()CRITICAL2-10배 개선async, parallelization, promises, waterfalls

독립적인 작업을 위한 Promise.all()

async 작업들이 서로 의존성이 없을 때, Promise.all()을 사용하여 동시에 실행하세요.

Incorrect (순차 실행, 3번의 round trips):

const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();

Correct (병렬 실행, 1번의 round trip):

const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);

async-suspense-boundaries.md

titleimpactimpactDescriptiontags
전략적 Suspense BoundariesHIGH더 빠른 initial paintasync, suspense, streaming, layout-shift

전략적 Suspense Boundaries

async components에서 JSX를 반환하기 전에 데이터를 await하는 대신, Suspense boundaries를 사용하여 데이터가 로드되는 동안 wrapper UI를 더 빠르게 보여주세요.

Incorrect (wrapper가 data fetching에 의해 block됨):

async function Page() {
  const data = await fetchData(); // 전체 페이지를 block함
 
  return (
    <div>
      <div>Sidebar</div>
      <div>Header</div>
      <div>
        <DataDisplay data={data} />
      </div>
      <div>Footer</div>
    </div>
  );
}

중간 섹션만 데이터가 필요한데도 전체 레이아웃이 데이터를 기다립니다.

Correct (wrapper가 즉시 표시되고, data는 stream됨):

function Page() {
  return (
    <div>
      <div>Sidebar</div>
      <div>Header</div>
      <div>
        <Suspense fallback={<Skeleton />}>
          <DataDisplay />
        </Suspense>
      </div>
      <div>Footer</div>
    </div>
  );
}
 
async function DataDisplay() {
  const data = await fetchData(); // 이 component만 block함
  return <div>{data.content}</div>;
}

Sidebar, Header, Footer는 즉시 렌더링됩니다. DataDisplay만 데이터를 기다립니다.

Alternative (components 간 promise 공유):

function Page() {
  // fetch를 즉시 시작하지만 await하지 않음
  const dataPromise = fetchData();
 
  return (
    <div>
      <div>Sidebar</div>
      <div>Header</div>
      <Suspense fallback={<Skeleton />}>
        <DataDisplay dataPromise={dataPromise} />
        <DataSummary dataPromise={dataPromise} />
      </Suspense>
      <div>Footer</div>
    </div>
  );
}
 
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
  const data = use(dataPromise); // promise를 unwrap함
  return <div>{data.content}</div>;
}
 
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
  const data = use(dataPromise); // 동일한 promise를 재사용
  return <div>{data.summary}</div>;
}

두 components가 같은 promise를 공유하므로 fetch는 한 번만 발생합니다. 레이아웃은 즉시 렌더링되고 두 components는 함께 기다립니다.

이 패턴을 사용하지 말아야 할 때:

  • 레이아웃 결정에 필요한 critical data (위치에 영향을 줌)
  • above the fold의 SEO-critical 콘텐츠
  • suspense 오버헤드가 가치 없는 작고 빠른 쿼리
  • layout shift를 피하고 싶을 때 (loading → content jump)

Trade-off: 더 빠른 initial paint vs 잠재적 layout shift. UX 우선순위에 따라 선택하세요.