Vercel Labs에서 skill로 공유한 React Best Practices를 개인 공부용으로 사람이 읽기 좋게 정리하고 설명을 다시 작성한 글입니다. 나중에 슥슥 읽으려고 AI와 함께 작성한 글이니까 이상한 부분 있으면 알려주세요.
1.1 Defer Await Until Needed
- 핵심: await는 실제로 필요한 분기 안으로 옮긴다.
- 왜 중요한가: 불필요한 대기를 제거해 응답 시간을 즉시 줄일 수 있다.
- 코드리뷰 신호: `early return`이 가능한데도 그 전에 실행되는 오래 걸리는 비동기 호출이 있다.
`early return`이 가능한데도 그 전에 실행되는 오래 걸리는 비동기 호출이 있다 징후는 기능을 빠르게 추가할 때 쉽게 누적된다. 먼저 의존성이 없는 작업은 먼저 시작하고, 실제 소비 지점에서만 `await` 하도록 분리한다. 이렇게 구조를 잡아두면 `await`는 실제로 필요한 분기 안으로 옮긴다 원칙을 예외 없이 유지하기가 훨씬 쉬워진다.
적용 후에는 네트워크 탭의 요청 시작/완료 시각과 서버 로그의 요청 체인 길이를 확인해 불필요한 대기를 제거해 응답 시간을 즉시 줄일 수 있다 효과가 실제로 나타나는지 검증한다.
예시 (Bad)
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) {
// 즉시 반환하지만 `userData`를 이미 기다린 상태
return { skipped: true }
}
// 이 분기에서만 `userData`를 사용
return processUserData(userData)
}
예시 (Good)
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// 기다리지 않고 즉시 반환
return { skipped: true }
}
// 필요한 경우에만 조회
const userData = await fetchUserData(userId)
return processUserData(userData)
}
1.2 Dependency-Based Parallelization
- 핵심: 부분 의존 관계는 의존 없는 작업부터 먼저 시작한다.
- 왜 중요한가: 전체 완료 시간을 가장 느린 경로 기준으로 압축할 수 있다.
- 코드리뷰 신호: 의존 없는 작업이 의존 작업 뒤에서 시작된다.
실무에서 의존 없는 작업이 의존 작업 뒤에서 시작된다 부분만 단발성으로 고치면 같은 문제가 다시 나오기 쉽다. 작은 단위(한 페이지, 한 훅, 한 API)부터 부분 의존 관계는 의존 없는 작업부터 먼저 시작한다 패턴을 적용하고, 경계 케이스를 함께 정리해야 회귀를 줄일 수 있다.
이후 네트워크 탭의 요청 시작/완료 시각과 서버 로그의 요청 체인 길이를 배포 전후로 비교해 전체 완료 시간을 가장 느린 경로 기준으로 압축할 수 있다 개선이 지속되는지 확인한다.
예시 (Bad)
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
])
const profile = await fetchProfile(user.id)
예시 (Good)
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)
}
})
1.3 Prevent Waterfall Chains in API Routes
- 핵심: API Route/Server Action에서도 독립 Promise를 먼저 시작한다.
- 왜 중요한가: 서버 라우트의 순차 대기 누적을 줄인다.
- 코드리뷰 신호: `auth -> config -> data`처럼 불필요한 직렬 체인이 있다.
`auth -> config -> data`처럼 불필요한 직렬 체인이 있다 코드는 보통 구현 편의 때문에 생기지만, 트래픽이 늘면 병목으로 확대된다. 이 항목의 핵심은 문법 변경이 아니라 실행/렌더 흐름을 정리해 API Route/Server Action에서도 독립 `Promise`를 먼저 시작한다를 기본값으로 만드는 데 있다.
변경 뒤에는 네트워크 탭의 요청 시작/완료 시각과 서버 로그의 요청 체인 길이와 회귀 테스트를 같이 보면서 서버 라우트의 순차 대기 누적을 줄인다 결과가 재현되는지 점검한다.
예시 (Bad)
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 })
}
예시 (Good)
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 })
}
1.4 Promise.all() for Independent Operations
- 핵심: 독립 비동기는 기본적으로 `Promise.all()`로 묶는다.
- 왜 중요한가: 왕복 지연(RTT) 누적을 한 번의 대기로 합칠 수 있다.
- 코드리뷰 신호: 독립적인 `await`가 여러 줄 연속으로 나온다.
코드리뷰에서 독립적인 `await`가 여러 줄 연속으로 나온다 신호가 보이면 먼저 책임 경계를 다시 나누는 것이 우선이다. 책임이 정리되면 의존성이 없는 작업은 먼저 시작하고, 실제 소비 지점에서만 await 하도록 분리한다 이 원칙을 자연스럽게 적용할 수 있고, 독립 비동기는 기본적으로 `Promise.all()`로 묶는다 규칙도 팀 기준으로 고정하기 쉬워진다.
마지막으로 네트워크 탭의 요청 시작/완료 시각과 서버 로그의 요청 체인 길이를 기준으로 왕복 지연(RTT) 누적을 한 번의 대기로 합칠 수 있다 개선이 체감과 일치하는지 확인한다.
예시 (Bad)
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
예시 (Good)
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
1.5 Strategic Suspense Boundaries
- 핵심: 페이지 shell은 먼저 렌더하고 느린 영역만 Suspense로 지연한다.
- 왜 중요한가: 첫 화면을 빨리 보여 체감 성능을 크게 개선한다.
- 코드리뷰 신호: 페이지 전체가 단일 `async` 경계에 묶여 있다.
페이지 전체가 단일 `async` 경계에 묶여 있다 징후는 기능을 빠르게 추가할 때 쉽게 누적된다. 먼저 의존성이 없는 작업은 먼저 시작하고, 실제 소비 지점에서만 `await` 하도록 분리한다. 이렇게 구조를 잡아두면 페이지 shell은 먼저 렌더하고 느린 영역만 `Suspense`로 지연한다 원칙을 예외 없이 유지하기가 훨씬 쉬워진다.
적용 후에는 네트워크 탭의 요청 시작/완료 시각과 서버 로그의 요청 체인 길이를 확인해 첫 화면을 빨리 보여 체감 성능을 크게 개선한다 효과가 실제로 나타나는지 검증한다.
예시 (Bad)
async function Page() {
const data = await fetchData() // 페이지 전체 렌더를 블로킹
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<DataDisplay data={data} />
</div>
<div>Footer</div>
</div>
)
}
예시 (Good)
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() // 이 컴포넌트만 블로킹
return <div>{data.content}</div>
}
'Develop > Web' 카테고리의 다른 글
| [Web][React] 디자인 시스템 JSDoc에 스크린샷 자동 주입하기 (1) | 2026.01.13 |
|---|---|
| [Web] JSDoc과 Storybook으로 컴포넌트 문서화 (0) | 2025.12.25 |
| [Web] 시맨틱 태그 적용 작업 (3) | 2025.12.12 |
| [Web][Issue] MUI의 INP 성능 저하 문제 (0) | 2025.12.12 |
| [JS] Named export와 Default export, 둘 중에 뭘 써야할까? (0) | 2025.12.09 |