# React 모범 사례 **버전 0.1.0** Vercel 엔지니어링 2026년 1월 > **참고:** > 이 문서는 주로 에이전트와 LLM이 Vercel에서 React 및 Next.js 코드베이스를 > 유지보수, 생성 또는 리팩토링할 때 따라야 할 지침입니다. 사람도 유용하게 > 사용할 수 있지만, 여기의 지침은 AI 지원 워크플로우에 의한 자동화와 > 일관성을 위해 최적화되어 있습니다. --- ## 요약 React 및 Next.js 애플리케이션을 위한 포괄적인 성능 최적화 가이드로, AI 에이전트와 LLM을 위해 설계되었습니다. 8개 카테고리에 걸쳐 40개 이상의 규칙을 포함하며, 중요도(워터폴 제거, 번들 크기 감소 등 치명적)부터 점진적(고급 패턴)까지 우선순위가 지정되어 있습니다. 각 규칙에는 상세한 설명, 잘못된 구현과 올바른 구현을 비교하는 실제 예제, 자동화된 리팩토링 및 코드 생성을 안내하는 구체적인 영향 지표가 포함되어 있습니다. --- ## 목차 1. [워터폴 제거](#1-워터폴-제거) — **치명적** - 1.1 [필요할 때까지 Await 지연](#11-필요할-때까지-await-지연) - 1.2 [의존성 기반 병렬화](#12-의존성-기반-병렬화) - 1.3 [API 라우트에서 워터폴 체인 방지](#13-api-라우트에서-워터폴-체인-방지) - 1.4 [독립적인 작업을 위한 Promise.all()](#14-독립적인-작업을-위한-promiseall) - 1.5 [전략적 Suspense 경계](#15-전략적-suspense-경계) 2. [번들 크기 최적화](#2-번들-크기-최적화) — **치명적** - 2.1 [배럴 파일 임포트 피하기](#21-배럴-파일-임포트-피하기) - 2.2 [조건부 모듈 로딩](#22-조건부-모듈-로딩) - 2.3 [중요하지 않은 서드파티 라이브러리 지연 로드](#23-중요하지-않은-서드파티-라이브러리-지연-로드) - 2.4 [무거운 컴포넌트를 위한 동적 임포트](#24-무거운-컴포넌트를-위한-동적-임포트) - 2.5 [사용자 의도에 기반한 프리로드](#25-사용자-의도에-기반한-프리로드) 3. [서버 사이드 성능](#3-서버-사이드-성능) — **높음** - 3.1 [요청 간 LRU 캐싱](#31-요청-간-lru-캐싱) - 3.2 [RSC 경계에서 직렬화 최소화](#32-rsc-경계에서-직렬화-최소화) - 3.3 [컴포넌트 합성을 통한 병렬 데이터 페칭](#33-컴포넌트-합성을-통한-병렬-데이터-페칭) - 3.4 [React.cache()를 사용한 요청별 중복 제거](#34-reactcache를-사용한-요청별-중복-제거) - 3.5 [비차단 작업을 위한 after() 사용](#35-비차단-작업을-위한-after-사용) 4. [클라이언트 사이드 데이터 페칭](#4-클라이언트-사이드-데이터-페칭) — **중간-높음** - 4.1 [글로벌 이벤트 리스너 중복 제거](#41-글로벌-이벤트-리스너-중복-제거) - 4.2 [자동 중복 제거를 위한 SWR 사용](#42-자동-중복-제거를-위한-swr-사용) 5. [재렌더링 최적화](#5-재렌더링-최적화) — **중간** - 5.1 [사용 시점까지 상태 읽기 지연](#51-사용-시점까지-상태-읽기-지연) - 5.2 [메모이제이션된 컴포넌트로 추출](#52-메모이제이션된-컴포넌트로-추출) - 5.3 [이펙트 의존성 좁히기](#53-이펙트-의존성-좁히기) - 5.4 [파생 상태 구독](#54-파생-상태-구독) - 5.5 [함수형 setState 업데이트 사용](#55-함수형-setstate-업데이트-사용) - 5.6 [지연 상태 초기화 사용](#56-지연-상태-초기화-사용) - 5.7 [긴급하지 않은 업데이트에 트랜지션 사용](#57-긴급하지-않은-업데이트에-트랜지션-사용) 6. [렌더링 성능](#6-렌더링-성능) — **중간** - 6.1 [SVG 요소 대신 래퍼 애니메이션](#61-svg-요소-대신-래퍼-애니메이션) - 6.2 [긴 리스트를 위한 CSS content-visibility](#62-긴-리스트를-위한-css-content-visibility) - 6.3 [정적 JSX 요소 호이스팅](#63-정적-jsx-요소-호이스팅) - 6.4 [SVG 정밀도 최적화](#64-svg-정밀도-최적화) - 6.5 [깜빡임 없이 하이드레이션 불일치 방지](#65-깜빡임-없이-하이드레이션-불일치-방지) - 6.6 [표시/숨김을 위한 Activity 컴포넌트 사용](#66-표시숨김을-위한-activity-컴포넌트-사용) - 6.7 [명시적 조건부 렌더링 사용](#67-명시적-조건부-렌더링-사용) 7. [JavaScript 성능](#7-javascript-성능) — **낮음-중간** - 7.1 [DOM CSS 변경 일괄 처리](#71-dom-css-변경-일괄-처리) - 7.2 [반복 조회를 위한 인덱스 맵 구축](#72-반복-조회를-위한-인덱스-맵-구축) - 7.3 [루프에서 속성 접근 캐시](#73-루프에서-속성-접근-캐시) - 7.4 [반복 함수 호출 캐시](#74-반복-함수-호출-캐시) - 7.5 [스토리지 API 호출 캐시](#75-스토리지-api-호출-캐시) - 7.6 [여러 배열 순회 결합](#76-여러-배열-순회-결합) - 7.7 [배열 비교 시 길이 먼저 확인](#77-배열-비교-시-길이-먼저-확인) - 7.8 [함수에서 조기 반환](#78-함수에서-조기-반환) - 7.9 [RegExp 생성 호이스팅](#79-regexp-생성-호이스팅) - 7.10 [정렬 대신 루프로 최소/최대 찾기](#710-정렬-대신-루프로-최소최대-찾기) - 7.11 [O(1) 조회를 위한 Set/Map 사용](#711-o1-조회를-위한-setmap-사용) - 7.12 [불변성을 위해 sort() 대신 toSorted() 사용](#712-불변성을-위해-sort-대신-tosorted-사용) 8. [고급 패턴](#8-고급-패턴) — **낮음** - 8.1 [이벤트 핸들러를 Ref에 저장](#81-이벤트-핸들러를-ref에-저장) - 8.2 [안정적인 콜백 Ref를 위한 useLatest](#82-안정적인-콜백-ref를-위한-uselatest) --- ## 1. 워터폴 제거 **영향: 치명적** 워터폴은 성능 저하의 가장 큰 원인입니다. 각각의 순차적 await는 전체 네트워크 지연 시간을 추가합니다. 이를 제거하면 가장 큰 성능 향상을 얻을 수 있습니다. ### 1.1 필요할 때까지 Await 지연 **영향: 높음 (사용되지 않는 코드 경로 차단 방지)** `await` 작업을 실제로 필요한 분기 내부로 이동하여 필요하지 않은 코드 경로를 차단하지 않도록 합니다. **잘못된 예: 두 분기 모두 차단** ```typescript async function handleRequest(userId: string, skipProcessing: boolean) { const userData = await fetchUserData(userId); if (skipProcessing) { // 즉시 반환하지만 여전히 userData를 기다림 return { skipped: true }; } // 이 분기만 userData를 사용 return processUserData(userData); } ``` **올바른 예: 필요할 때만 차단** ```typescript async function handleRequest(userId: string, skipProcessing: boolean) { if (skipProcessing) { // 기다림 없이 즉시 반환 return { skipped: true }; } // 필요할 때만 페치 const userData = await fetchUserData(userId); return processUserData(userData); } ``` **또 다른 예: 조기 반환 최적화** ```typescript // 잘못된 예: 항상 권한을 페치 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); } ``` 이 최적화는 건너뛰는 분기가 자주 사용되거나 지연된 작업이 비용이 많이 들 때 특히 유용합니다. ### 1.2 의존성 기반 병렬화 **영향: 치명적 (2-10배 개선)** 부분적인 의존성이 있는 작업의 경우, `better-all`을 사용하여 병렬성을 최대화합니다. 각 작업을 가능한 가장 빠른 시점에 자동으로 시작합니다. **잘못된 예: profile이 config를 불필요하게 기다림** ```typescript const [user, config] = await Promise.all([fetchUser(), fetchConfig()]); const profile = await fetchProfile(user.id); ``` **올바른 예: config와 profile이 병렬로 실행** ```typescript 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); }, }); ``` 참고: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) ### 1.3 API 라우트에서 워터폴 체인 방지 **영향: 치명적 (2-10배 개선)** API 라우트와 Server Actions에서 독립적인 작업을 즉시 시작합니다. 아직 await하지 않더라도요. **잘못된 예: config가 auth를 기다리고, data가 둘 다 기다림** ```typescript 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 }); } ``` **올바른 예: auth와 config가 즉시 시작** ```typescript 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 }); } ``` 더 복잡한 의존성 체인이 있는 작업의 경우, `better-all`을 사용하여 자동으로 병렬성을 최대화합니다 (의존성 기반 병렬화 참조). ### 1.4 독립적인 작업을 위한 Promise.all() **영향: 치명적 (2-10배 개선)** 비동기 작업에 상호 의존성이 없을 때, `Promise.all()`을 사용하여 동시에 실행합니다. **잘못된 예: 순차 실행, 3번의 라운드 트립** ```typescript const user = await fetchUser(); const posts = await fetchPosts(); const comments = await fetchComments(); ``` **올바른 예: 병렬 실행, 1번의 라운드 트립** ```typescript const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]); ``` ### 1.5 전략적 Suspense 경계 **영향: 높음 (더 빠른 초기 페인트)** 비동기 컴포넌트에서 JSX를 반환하기 전에 데이터를 await하는 대신, Suspense 경계를 사용하여 데이터가 로드되는 동안 래퍼 UI를 더 빨리 표시합니다. **잘못된 예: 래퍼가 데이터 페칭에 의해 차단됨** ```tsx async function Page() { const data = await fetchData(); // 전체 페이지 차단 return (
Sidebar
Header
Footer
); } ``` 전체 레이아웃이 데이터를 기다리지만 중간 섹션만 데이터가 필요합니다. **올바른 예: 래퍼가 즉시 표시되고, 데이터가 스트리밍됨** ```tsx function Page() { return (
Sidebar
Header
}>
Footer
); } async function DataDisplay() { const data = await fetchData(); // 이 컴포넌트만 차단 return
{data.content}
; } ``` Sidebar, Header, Footer가 즉시 렌더링됩니다. DataDisplay만 데이터를 기다립니다. **대안: 컴포넌트 간 프로미스 공유** ```tsx function Page() { // 페치를 즉시 시작하지만 await하지 않음 const dataPromise = fetchData(); return (
Sidebar
Header
}>
Footer
); } function DataDisplay({ dataPromise }: { dataPromise: Promise }) { const data = use(dataPromise); // 프로미스 언랩 return
{data.content}
; } function DataSummary({ dataPromise }: { dataPromise: Promise }) { const data = use(dataPromise); // 같은 프로미스 재사용 return
{data.summary}
; } ``` 두 컴포넌트가 같은 프로미스를 공유하므로 페치는 한 번만 발생합니다. 레이아웃이 즉시 렌더링되고 두 컴포넌트가 함께 기다립니다. **이 패턴을 사용하지 말아야 할 때:** - 레이아웃 결정에 필요한 중요 데이터 (위치 지정에 영향) - 폴드 위의 SEO 중요 콘텐츠 - Suspense 오버헤드가 가치 없는 작고 빠른 쿼리 - 레이아웃 시프트를 피하고 싶을 때 (로딩 → 콘텐츠 점프) **트레이드오프:** 더 빠른 초기 페인트 vs 잠재적 레이아웃 시프트. UX 우선순위에 따라 선택하세요. --- ## 2. 번들 크기 최적화 **영향: 치명적** 초기 번들 크기를 줄이면 상호작용 시간(TTI)과 최대 콘텐츠풀 페인트(LCP)가 개선됩니다. ### 2.1 배럴 파일 임포트 피하기 **영향: 치명적 (200-800ms 임포트 비용, 느린 빌드)** 사용하지 않는 수천 개의 모듈을 로드하지 않도록 배럴 파일 대신 소스 파일에서 직접 임포트합니다. **배럴 파일**은 여러 모듈을 재내보내는 진입점입니다 (예: `export * from './module'`을 수행하는 `index.js`). 인기 있는 아이콘 및 컴포넌트 라이브러리는 진입 파일에 **최대 10,000개의 재내보내기**가 있을 수 있습니다. 많은 React 패키지의 경우, **임포트하는 데만 200-800ms가 걸리며**, 이는 개발 속도와 프로덕션 콜드 스타트 모두에 영향을 미칩니다. **트리 쉐이킹이 도움이 되지 않는 이유:** 라이브러리가 외부(번들되지 않음)로 표시되면 번들러가 최적화할 수 없습니다. 트리 쉐이킹을 활성화하기 위해 번들하면 전체 모듈 그래프를 분석하느라 빌드가 상당히 느려집니다. **잘못된 예: 전체 라이브러리 임포트** ```tsx // 1,583개 모듈 로드, 개발 시 ~2.8초 추가 // 런타임 비용: 매 콜드 스타트마다 200-800ms import { Button, TextField } from "@mui/material"; import { Check, Menu, X } from "lucide-react"; // 2,225개 모듈 로드, 개발 시 ~4.2초 추가 ``` **올바른 예: 필요한 것만 임포트** ```tsx // 3개 모듈만 로드 (~2KB vs ~1MB) import Button from "@mui/material/Button"; import TextField from "@mui/material/TextField"; import Check from "lucide-react/dist/esm/icons/check"; import Menu from "lucide-react/dist/esm/icons/menu"; import X from "lucide-react/dist/esm/icons/x"; // 사용하는 것만 로드 ``` **대안: Next.js 13.5+** ```js // 그러면 편리한 배럴 임포트를 유지할 수 있습니다: import { Check, Menu, X } from "lucide-react"; // next.config.js - optimizePackageImports 사용 module.exports = { experimental: { optimizePackageImports: ["lucide-react", "@mui/material"], }, }; // 빌드 시 자동으로 직접 임포트로 변환됨 ``` 직접 임포트는 15-70% 더 빠른 개발 부트, 28% 더 빠른 빌드, 40% 더 빠른 콜드 스타트, 상당히 빠른 HMR을 제공합니다. 일반적으로 영향받는 라이브러리: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. 참고: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) ### 2.2 조건부 모듈 로딩 **영향: 높음 (필요할 때만 큰 데이터 로드)** 기능이 활성화될 때만 큰 데이터나 모듈을 로드합니다. **예: 애니메이션 프레임 지연 로드** ```tsx function AnimationPlayer({ enabled }: { enabled: boolean }) { const [frames, setFrames] = useState(null); useEffect(() => { if (enabled && !frames && typeof window !== "undefined") { import("./animation-frames.js") .then((mod) => setFrames(mod.frames)) .catch(() => setEnabled(false)); } }, [enabled, frames]); if (!frames) return ; return ; } ``` `typeof window !== 'undefined'` 체크는 이 모듈이 SSR용으로 번들되는 것을 방지하여 서버 번들 크기와 빌드 속도를 최적화합니다. ### 2.3 중요하지 않은 서드파티 라이브러리 지연 로드 **영향: 중간 (하이드레이션 후 로드)** 분석, 로깅, 오류 추적은 사용자 상호작용을 차단하지 않습니다. 하이드레이션 후에 로드합니다. **잘못된 예: 초기 번들 차단** ```tsx import { Analytics } from "@vercel/analytics/react"; export default function RootLayout({ children }) { return ( {children} ); } ``` **올바른 예: 하이드레이션 후 로드** ```tsx import dynamic from "next/dynamic"; const Analytics = dynamic(() => import("@vercel/analytics/react").then((m) => m.Analytics), { ssr: false, }); export default function RootLayout({ children }) { return ( {children} ); } ``` ### 2.4 무거운 컴포넌트를 위한 동적 임포트 **영향: 치명적 (TTI와 LCP에 직접 영향)** 초기 렌더링에 필요하지 않은 큰 컴포넌트를 지연 로드하려면 `next/dynamic`을 사용합니다. **잘못된 예: Monaco가 메인 청크와 함께 번들됨 ~300KB** ```tsx import { MonacoEditor } from "./monaco-editor"; function CodePanel({ code }: { code: string }) { return ; } ``` **올바른 예: Monaco가 필요 시 로드** ```tsx import dynamic from "next/dynamic"; const MonacoEditor = dynamic(() => import("./monaco-editor").then((m) => m.MonacoEditor), { ssr: false, }); function CodePanel({ code }: { code: string }) { return ; } ``` ### 2.5 사용자 의도에 기반한 프리로드 **영향: 중간 (체감 지연 감소)** 체감 지연을 줄이기 위해 무거운 번들을 필요하기 전에 프리로드합니다. **예: 호버/포커스 시 프리로드** ```tsx function EditorButton({ onClick }: { onClick: () => void }) { const preload = () => { if (typeof window !== "undefined") { void import("./monaco-editor"); } }; return ( ); } ``` **예: 기능 플래그가 활성화되면 프리로드** ```tsx function FlagsProvider({ children, flags }: Props) { useEffect(() => { if (flags.editorEnabled && typeof window !== "undefined") { void import("./monaco-editor").then((mod) => mod.init()); } }, [flags.editorEnabled]); return {children}; } ``` `typeof window !== 'undefined'` 체크는 프리로드된 모듈이 SSR용으로 번들되는 것을 방지하여 서버 번들 크기와 빌드 속도를 최적화합니다. --- ## 3. 서버 사이드 성능 **영향: 높음** 서버 사이드 렌더링과 데이터 페칭을 최적화하면 서버 사이드 워터폴을 제거하고 응답 시간을 줄입니다. ### 3.1 요청 간 LRU 캐싱 **영향: 높음 (요청 간 캐시)** `React.cache()`는 하나의 요청 내에서만 작동합니다. 순차적 요청 간에 공유되는 데이터의 경우 (사용자가 버튼 A를 클릭한 다음 버튼 B를 클릭), LRU 캐시를 사용합니다. **구현:** ```typescript import { LRUCache } from "lru-cache"; const cache = new LRUCache({ max: 1000, ttl: 5 * 60 * 1000, // 5분 }); export async function getUser(id: string) { const cached = cache.get(id); if (cached) return cached; const user = await db.user.findUnique({ where: { id } }); cache.set(id, user); return user; } // 요청 1: DB 쿼리, 결과 캐시됨 // 요청 2: 캐시 히트, DB 쿼리 없음 ``` 순차적 사용자 행동이 몇 초 내에 같은 데이터가 필요한 여러 엔드포인트를 호출할 때 사용합니다. **Vercel의 [Fluid Compute](https://vercel.com/docs/fluid-compute) 사용 시:** 여러 동시 요청이 같은 함수 인스턴스와 캐시를 공유할 수 있으므로 LRU 캐싱이 특히 효과적입니다. 이는 Redis와 같은 외부 스토리지 없이도 캐시가 요청 간에 지속된다는 것을 의미합니다. **전통적인 서버리스에서:** 각 호출이 격리되어 실행되므로 프로세스 간 캐싱을 위해 Redis를 고려하세요. 참고: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) ### 3.2 RSC 경계에서 직렬화 최소화 **영향: 높음 (데이터 전송 크기 감소)** React Server/Client 경계는 모든 객체 속성을 문자열로 직렬화하여 HTML 응답과 후속 RSC 요청에 포함합니다. 이 직렬화된 데이터는 페이지 가중치와 로드 시간에 직접 영향을 미치므로 **크기가 매우 중요합니다**. 클라이언트가 실제로 사용하는 필드만 전달합니다. **잘못된 예: 50개 필드 모두 직렬화** ```tsx async function Page() { const user = await fetchUser(); // 50개 필드 return ; } ("use client"); function Profile({ user }: { user: User }) { return
{user.name}
; // 1개 필드 사용 } ``` **올바른 예: 1개 필드만 직렬화** ```tsx async function Page() { const user = await fetchUser(); return ; } ("use client"); function Profile({ name }: { name: string }) { return
{name}
; } ``` ### 3.3 컴포넌트 합성을 통한 병렬 데이터 페칭 **영향: 치명적 (서버 사이드 워터폴 제거)** React Server Components는 트리 내에서 순차적으로 실행됩니다. 데이터 페칭을 병렬화하려면 합성으로 재구성합니다. **잘못된 예: Sidebar가 Page의 페치 완료를 기다림** ```tsx export default async function Page() { const header = await fetchHeader(); return (
{header}
); } async function Sidebar() { const items = await fetchSidebarItems(); return ; } ``` **올바른 예: 둘 다 동시에 페치** ```tsx async function Header() { const data = await fetchHeader(); return
{data}
; } async function Sidebar() { const items = await fetchSidebarItems(); return ; } export default function Page() { return (
); } ``` **children prop을 사용한 대안:** ```tsx async function Layout({ children }: { children: ReactNode }) { const header = await fetchHeader(); return (
{header}
{children}
); } async function Sidebar() { const items = await fetchSidebarItems(); return ; } export default function Page() { return ( ); } ``` ### 3.4 React.cache()를 사용한 요청별 중복 제거 **영향: 중간 (요청 내 중복 제거)** 서버 사이드 요청 중복 제거를 위해 `React.cache()`를 사용합니다. 인증과 데이터베이스 쿼리가 가장 큰 이점을 얻습니다. **사용법:** ```typescript import { cache } from "react"; export const getCurrentUser = cache(async () => { const session = await auth(); if (!session?.user?.id) return null; return await db.user.findUnique({ where: { id: session.user.id }, }); }); ``` 단일 요청 내에서 `getCurrentUser()`에 대한 여러 호출은 쿼리를 한 번만 실행합니다. ### 3.5 비차단 작업을 위한 after() 사용 **영향: 중간 (더 빠른 응답 시간)** 응답이 전송된 후 실행해야 하는 작업을 예약하려면 Next.js의 `after()`를 사용합니다. 이는 로깅, 분석 및 기타 부수 효과가 응답을 차단하는 것을 방지합니다. **잘못된 예: 응답 차단** ```tsx import { logUserAction } from "@/app/utils"; export async function POST(request: Request) { // 뮤테이션 수행 await updateDatabase(request); // 로깅이 응답을 차단 const userAgent = request.headers.get("user-agent") || "unknown"; await logUserAction({ userAgent }); return new Response(JSON.stringify({ status: "success" }), { status: 200, headers: { "Content-Type": "application/json" }, }); } ``` **올바른 예: 비차단** ```tsx import { cookies, headers } from "next/headers"; import { after } from "next/server"; import { logUserAction } from "@/app/utils"; export async function POST(request: Request) { // 뮤테이션 수행 await updateDatabase(request); // 응답 전송 후 로그 after(async () => { const userAgent = (await headers()).get("user-agent") || "unknown"; const sessionCookie = (await cookies()).get("session-id")?.value || "anonymous"; logUserAction({ sessionCookie, userAgent }); }); return new Response(JSON.stringify({ status: "success" }), { status: 200, headers: { "Content-Type": "application/json" }, }); } ``` 응답이 즉시 전송되고 로깅은 백그라운드에서 발생합니다. **일반적인 사용 사례:** - 분석 추적 - 감사 로깅 - 알림 전송 - 캐시 무효화 - 정리 작업 **중요 참고:** - `after()`는 응답이 실패하거나 리다이렉트되더라도 실행됩니다 - Server Actions, Route Handlers, Server Components에서 작동합니다 참고: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after) --- ## 4. 클라이언트 사이드 데이터 페칭 **영향: 중간-높음** 자동 중복 제거와 효율적인 데이터 페칭 패턴은 중복 네트워크 요청을 줄입니다. ### 4.1 글로벌 이벤트 리스너 중복 제거 **영향: 낮음 (N개 컴포넌트에 단일 리스너)** 컴포넌트 인스턴스 간에 글로벌 이벤트 리스너를 공유하려면 `useSWRSubscription()`을 사용합니다. **잘못된 예: N개 인스턴스 = N개 리스너** ```tsx function useKeyboardShortcut(key: string, callback: () => void) { useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.metaKey && e.key === key) { callback(); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [key, callback]); } ``` `useKeyboardShortcut` 훅을 여러 번 사용하면 각 인스턴스가 새 리스너를 등록합니다. **올바른 예: N개 인스턴스 = 1개 리스너** ```tsx import useSWRSubscription from "swr/subscription"; // 모듈 레벨 Map으로 키별 콜백 추적 const keyCallbacks = new Map void>>(); function useKeyboardShortcut(key: string, callback: () => void) { // Map에 이 콜백 등록 useEffect(() => { if (!keyCallbacks.has(key)) { keyCallbacks.set(key, new Set()); } keyCallbacks.get(key)!.add(callback); return () => { const set = keyCallbacks.get(key); if (set) { set.delete(callback); if (set.size === 0) { keyCallbacks.delete(key); } } }; }, [key, callback]); useSWRSubscription("global-keydown", () => { const handler = (e: KeyboardEvent) => { if (e.metaKey && keyCallbacks.has(e.key)) { keyCallbacks.get(e.key)!.forEach((cb) => cb()); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }); } function Profile() { // 여러 단축키가 같은 리스너를 공유 useKeyboardShortcut("p", () => { /* ... */ }); useKeyboardShortcut("k", () => { /* ... */ }); // ... } ``` ### 4.2 자동 중복 제거를 위한 SWR 사용 **영향: 중간-높음 (자동 중복 제거)** SWR은 컴포넌트 인스턴스 간 요청 중복 제거, 캐싱 및 재검증을 가능하게 합니다. **잘못된 예: 중복 제거 없음, 각 인스턴스가 페치** ```tsx function UserList() { const [users, setUsers] = useState([]); useEffect(() => { fetch("/api/users") .then((r) => r.json()) .then(setUsers); }, []); } ``` **올바른 예: 여러 인스턴스가 하나의 요청 공유** ```tsx import useSWR from "swr"; function UserList() { const { data: users } = useSWR("/api/users", fetcher); } ``` **불변 데이터의 경우:** ```tsx import { useImmutableSWR } from "@/lib/swr"; function StaticContent() { const { data } = useImmutableSWR("/api/config", fetcher); } ``` **뮤테이션의 경우:** ```tsx import { useSWRMutation } from "swr/mutation"; function UpdateButton() { const { trigger } = useSWRMutation("/api/user", updateUser); return ; } ``` 참고: [https://swr.vercel.app](https://swr.vercel.app) --- ## 5. 재렌더링 최적화 **영향: 중간** 불필요한 재렌더링을 줄이면 낭비되는 계산을 최소화하고 UI 응답성이 향상됩니다. ### 5.1 사용 시점까지 상태 읽기 지연 **영향: 중간 (불필요한 구독 방지)** 콜백 내에서만 읽는다면 동적 상태 (searchParams, localStorage)를 구독하지 마세요. **잘못된 예: 모든 searchParams 변경을 구독** ```tsx function ShareButton({ chatId }: { chatId: string }) { const searchParams = useSearchParams(); const handleShare = () => { const ref = searchParams.get("ref"); shareChat(chatId, { ref }); }; return ; } ``` **올바른 예: 필요 시 읽기, 구독 없음** ```tsx function ShareButton({ chatId }: { chatId: string }) { const handleShare = () => { const params = new URLSearchParams(window.location.search); const ref = params.get("ref"); shareChat(chatId, { ref }); }; return ; } ``` ### 5.2 메모이제이션된 컴포넌트로 추출 **영향: 중간 (조기 반환 가능)** 비용이 많이 드는 작업을 메모이제이션된 컴포넌트로 추출하여 계산 전에 조기 반환을 가능하게 합니다. **잘못된 예: 로딩 중에도 아바타 계산** ```tsx function Profile({ user, loading }: Props) { const avatar = useMemo(() => { const id = computeAvatarId(user); return ; }, [user]); if (loading) return ; return
{avatar}
; } ``` **올바른 예: 로딩 시 계산 건너뜀** ```tsx const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { const id = useMemo(() => computeAvatarId(user), [user]); return ; }); function Profile({ user, loading }: Props) { if (loading) return ; return (
); } ``` **참고:** 프로젝트에 [React Compiler](https://react.dev/learn/react-compiler)가 활성화되어 있다면, `memo()` 및 `useMemo()`를 사용한 수동 메모이제이션은 필요하지 않습니다. 컴파일러가 자동으로 재렌더링을 최적화합니다. ### 5.3 이펙트 의존성 좁히기 **영향: 낮음 (이펙트 재실행 최소화)** 이펙트 재실행을 최소화하려면 객체 대신 원시 의존성을 지정합니다. **잘못된 예: user 필드 변경 시 재실행** ```tsx useEffect(() => { console.log(user.id); }, [user]); ``` **올바른 예: id 변경 시에만 재실행** ```tsx useEffect(() => { console.log(user.id); }, [user.id]); ``` **파생 상태의 경우, 이펙트 외부에서 계산:** ```tsx // 잘못된 예: width=767, 766, 765...에서 실행 useEffect(() => { if (width < 768) { enableMobileMode(); } }, [width]); // 올바른 예: boolean 전환 시에만 실행 const isMobile = width < 768; useEffect(() => { if (isMobile) { enableMobileMode(); } }, [isMobile]); ``` ### 5.4 파생 상태 구독 **영향: 중간 (재렌더링 빈도 감소)** 재렌더링 빈도를 줄이려면 연속 값 대신 파생된 boolean 상태를 구독합니다. **잘못된 예: 매 픽셀 변경마다 재렌더링** ```tsx function Sidebar() { const width = useWindowWidth() // 지속적으로 업데이트 const isMobile = width < 768 return