Vercel의 React Best Practices 톺아보기 [5]
5. 리렌더링 최적화 - 중간 (MEDIUM)
불필요한 재렌더링을 줄이면 낭비되는 연산이 최소화되고 UI 응답성이 향상됩니다.
5.1 Calculate Derived State During Rendering (파생된 상태는 렌더링 중에 계산하세요.)
- 영향도: 중간 (MEDIUM / 불필요한 리렌더, 상태 불일치 방지)
현재 props/state 상태만으로 계산할 수 있는 값이면 state에 넣지 말고, effect로 업데이트 하지 마세요. 불필요한 렌더링과 state 변화를 방지하기 위해 렌더링 중에 값을 도출하세요. props가 바뀔 때마다 effect로만 state를 맞추는 패턴은 피하고, 파생 값이나 key로 컴포넌트를 리셋하는 쪽을 우선하세요.
잘못된 예 (불필요한 state + effect):
function Form() {
const [firstName, setFirstName] = useState("First");
const [lastName, setLastName] = useState("Last");
const [fullName, setFullName] = useState("");
// firstName/lastName이 바뀔 때마다 한 턴 늦게 fullName이 갱신될 수 있음
useEffect(() => {
setFullName(firstName + " " + lastName);
}, [firstName, lastName]);
return <p>{fullName}</p>;
}올바른 예 (렌더 중에 파생):
function Form() {
const [firstName, setFirstName] = useState("First");
const [lastName, setLastName] = useState("Last");
// 매 렌더마다 최신 first/last로 계산 — 별도 state 불필요
const fullName = firstName + " " + lastName;
return <p>{fullName}</p>;
}참고: You Might Not Need an Effect
5.2 Defer State Reads to Usage Point (상태 읽기는 실제 쓰는 시점으로 미루세요.)
- 영향도: 중간 (MEDIUM / 불필요한 구독 방지)
콜백 함수 내에서만 동적 상태(searchParams, localStorage)를 읽는 경우에는 해당 동적 상태를 구독하지 마세요.
잘못된 예 (searchParams 변경마다 컴포넌트가 구독):
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams();
const handleShare = () => {
const ref = searchParams.get("ref");
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}올바른 예 (필요할 때만 읽기, 구독 없음):
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
// 클릭 시점의 URL만 읽음 — useSearchParams 리렌더와 무관
const params = new URLSearchParams(window.location.search);
const ref = params.get("ref");
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}5.3 Do not wrap a simple expression with a primitive result type in useMemo (단순한 원시값 표현식은 useMemo로 감싸지 마세요.)
- 영향도: 중하 (LOW-MEDIUM / 렌더링할 때마다 불필요한 연산 발생)
표현식이 단순(논리·산술 연산 몇 개 수준)하고 결과가 원시 타입(boolean, number, string)이면 useMemo로 감싸지 마세요. useMemo 호출과 의존성 비교만으로도, 그 표현식 자체보다 비용이 더 나갈 수 있습니다.
잘못된 예:
function Header({ user, notifications }: Props) {
const isLoading = useMemo(() => {
return user.isLoading || notifications.isLoading;
}, [user.isLoading, notifications.isLoading]);
if (isLoading) return <Skeleton />;
// 마크업 반환
}올바른 예:
function Header({ user, notifications }: Props) {
// boolean 하나면 그냥 매 렌더에 계산하는 편이 가볍다
const isLoading = user.isLoading || notifications.isLoading;
if (isLoading) return <Skeleton />;
// 마크업 반환
}5.4 Don't Define Components Inside Components (컴포넌트 안에서 컴포넌트를 정의하지 마세요.)
- 영향도: 높음 (HIGH / 렌더링 시마다 재마운트를 방지)
한 컴포넌트 함수 본문 안에서 또 다른 컴포넌트를 정의하면, 렌더할 때마다 새로운 컴포넌트 타입이 생깁니다. React는 매번 다른 컴포넌트로 보고 완전히 언마운트 후 다시 마운트하므로, 내부 state와 DOM이 초기화됩니다.
이렇게 쓰는 흔한 이유는 부모 변수에 접근하려고 props를 안 넘기려는 경우입니다. 대신 props로 넘기세요.
잘못된 예 (부모가 리렌더될 때마다 자식 리마운트):
function UserProfile({ user, theme }) {
// theme에 접근하기 위해 내부 정의 — 비권장
const Avatar = () => (
<img
src={user.avatarUrl}
className={theme === "dark" ? "avatar-dark" : "avatar-light"}
/>
);
// user에 접근하기 위해 내부 정의 — 비권장
const Stats = () => (
<div>
<span>{user.followers} followers</span>
<span>{user.posts} posts</span>
</div>
);
return (
<div>
<Avatar />
<Stats />
</div>
);
}UserProfile이 리렌더될 때마다 Avatar와 Stats는 새 타입입니다. React는 이전 인스턴스를 언마운트하고 새로 마운트하므로, 내부 state가 사라지고 effect가 다시 돌고 DOM 노드도 다시 만들어지게 됩니다.
올바른 예 (props로 전달):
function Avatar({ src, theme }: { src: string; theme: string }) {
return (
<img
src={src}
className={theme === "dark" ? "avatar-dark" : "avatar-light"}
/>
);
}
function Stats({ followers, posts }: { followers: number; posts: number }) {
return (
<div>
<span>{followers} followers</span>
<span>{posts} posts</span>
</div>
);
}
function UserProfile({ user, theme }) {
return (
<div>
<Avatar src={user.avatarUrl} theme={theme} />
<Stats followers={user.followers} posts={user.posts} />
</div>
);
}이 버그의 흔한 증상:
- 입력창이 타이핑 할 때마다 포커스가 풀림
- 애니메이션이 갑자기 처음부터 다시 시작됨
- 부모가 리렌더될 때마다
useEffectcleanup/setup이 반복 - 컴포넌트 내부의 스크롤 위치가 리셋됨