컴포넌트가 실제로 화면에 필요하지 않을 때 렌더링을 막으면 성능과 사용자 경험이 동시에 좋아집니다. 본 글은 조건부 렌더링을 실무에서 어떻게 최적화할지, 재렌더링을 줄이는 패턴과 주의할 점을 코드와 함께 정리합니다.
1. 조건 분기 위치: 부모에서 막을 것인가, 자식에서 막을 것인가
가능하면 부모 컴포넌트에서 조건을 걸어 자식 자체를 생성하지 않도록 막는 것이 가장 비용이 적습니다. 자식 내부에서 null을 반환하는 것도 유효하지만, 이미 함수 호출과 props 계산은 진행된 뒤입니다.
// 비권장: 자식 내부에서 early return (호출 비용은 발생)
function Modal({ open }) {
if (!open) return null;
return <div role="dialog">내용</div>;
}
// 권장: 부모에서 조건으로 아예 생성하지 않음
function Page({ open }) {
return (
<main>
{open ? <Modal /> : null}
</main>
);
}
주의: Hooks는 조건부로 호출할 수 없습니다. 부모에서 조건으로 자식 자체를 렌더링하지 않는 방식은 Hooks 규칙을 위반하지 않으면서도 낭비를 줄입니다.
2. 안정적인 조건과 참조 유지: props·state로 불필요한 재렌더 줄이기
조건 값은 원시 타입으로 유지하고, 객체·배열을 매 렌더마다 새로 만들지 않도록 합니다. 자식이 React.memo로 감싸져 있어도 얕은 비교에서 참조가 바뀌면 재렌더링됩니다.
const Child = React.memo(function Child({ options, onSelect }) {
// ...
return <button onClick={() => onSelect(options.key)}>선택</button>;
});
function Parent() {
const [active, setActive] = React.useState(false);
// 매 렌더마다 새로운 객체를 만들지 않도록 메모이즈
const options = React.useMemo(() => ({ key: active ? 'a' : 'b' }), [active]);
const handleSelect = React.useCallback((key) => {
setActive(key === 'a');
}, []);
return active ? <Child options={options} onSelect={handleSelect} /> : null;
}
포인트: 객체, 배열, 함수는 useMemo/useCallback으로 참조를 안정화합니다. 조건식 자체도 불필요한 계산을 줄이기 위해 상수화하거나, 상위에서 한 번만 평가되도록 구성합니다.
3. 리스트 조건 최적화: 필터·키·윈도잉
리스트는 조건 분기 비용이 크게 누적됩니다. 필요 없는 항목을 먼저 필터링하고, key는 안정적으로 유지하며, 항목 수가 많으면 윈도잉을 적용합니다.
// 필터를 먼저, 그 다음 map; key는 안정적인 고유 ID 사용
function UserList({ users, showActiveOnly }) {
const list = React.useMemo(() => {
const base = showActiveOnly ? users.filter(u => u.active) : users;
return base;
}, [users, showActiveOnly]);
return (
<ul>
{list.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// 대용량일 때: react-window 예시
import { FixedSizeList as List } from 'react-window';
function HugeList({ items, height = 400, itemSize = 36 }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].label}</div>
);
return (
<List height={height} itemCount={items.length} itemSize={itemSize} width={"100%"}>
{Row}
</List>
);
}
윈도잉은 화면에 보이는 셀만 렌더링하므로 조건부로 숨기는 것보다 훨씬 효과적입니다.
4. 조건부 로딩과 코드 스플리팅: 필요할 때만 가져오기
무거운 컴포넌트는 React.lazy와 Suspense로 조건 충족 시에만 로드합니다. 초기 페인트를 빠르게 하고, 사용 시점에 로딩 UI를 보여줍니다.
const Chart = React.lazy(() => import('./Chart'));
function Panel({ showChart }) {
return (
<section>
{showChart ? (
<React.Suspense fallback={<div role="status">차트 로딩...</div>}>
<Chart />
</React.Suspense>
) : null}
</section>
);
}
SSR/하이드레이션 주의: 서버와 클라이언트의 조건이 다르면 불일치가 발생합니다. 브라우저 전용 조건은 useEffect 이후에 렌더되게 하거나, 프레임워크의 동적 임포트 옵션을 활용해 클라이언트 전용으로 분리합니다.
5. 가시성 기반 조건: 보일 때만 렌더
뷰포트에 들어올 때만 무거운 컴포넌트를 렌더링하면 낭비가 크게 줄어듭니다. IntersectionObserver를 래핑한 커스텀 훅을 활용합니다.
function useInView(options) {
const ref = React.useRef(null);
const [inView, setInView] = React.useState(false);
React.useEffect(() => {
if (!ref.current) return;
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) setInView(true);
}, options);
io.observe(ref.current);
return () => io.disconnect();
}, [options]);
return { ref, inView };
}
function LazyChartSection() {
const { ref, inView } = useInView({ rootMargin: '200px' });
return (
<div ref={ref}>
{inView ? <HeavyChart /> : <div style={{ height: 300 }} />}
</div>
);
}
rootMargin을 넉넉히 주면 사용자에게 거의 즉시 로드된 것처럼 보입니다.
6. 입력 지연과 전환: useDeferredValue, useTransition
사용자 입력은 즉시 반응하되, 무거운 렌더는 늦춰서 체감 속도를 개선합니다. React 18 전환 API를 조건부 렌더와 함께 적용합니다.
// useDeferredValue: 무거운 필터링을 지연
function Search({ items }) {
const [query, setQuery] = React.useState('');
const deferredQuery = React.useDeferredValue(query);
const filtered = React.useMemo(() => {
const q = deferredQuery.toLowerCase();
return items.filter(i => i.label.toLowerCase().includes(q));
}, [items, deferredQuery]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="검색" />
{query !== deferredQuery ? <div>검색 중...</div> : null}
<Results list={filtered} />
</div>
);
}
// useTransition: 조건 토글 시 무거운 업데이트를 낮은 우선순위로
function FilterToggle() {
const [on, setOn] = React.useState(false);
const [isPending, startTransition] = React.useTransition();
const toggle = () => {
startTransition(() => setOn(v => !v));
};
return (
<section>
<button onClick={toggle}>필터 {on ? '끄기' : '켜기'}</button>
{isPending ? <span>적용 중...</span> : null}
{on ? <HeavyFilteredPanel /> : null}
</section>
);
}
7. Context와 조건: 범위를 좁히고 셀렉터 사용
Context 값이 바뀌면 구독하는 모든 하위가 재렌더링됩니다. 조건부 UI가 많다면 컨텍스트 범위를 좁히거나 셀렉터 기반 구독을 사용합니다.
import { createContext } from 'use-context-selector';
import { useContextSelector } from 'use-context-selector';
const StoreCtx = createContext(null);
function StoreProvider({ children }) {
const [state, setState] = React.useState({ theme: 'light', count: 0 });
const value = React.useMemo(() => ({ state, setState }), [state]);
return <StoreCtx.Provider value={value}>{children}</StoreCtx.Provider>;
}
// 필요한 조각만 구독
function CountBadge() {
const count = useContextSelector(StoreCtx, v => v.state.count);
return <span>{count}</span>;
}
대안으로 컨텍스트를 여러 개로 분리하거나, Zustand/Jotai 등 선택적 구독을 지원하는 스토어를 사용해 조건부 영역만 영향을 받도록 합니다.
8. 흔한 함정과 회피 전략
display:none은 DOM과 이펙트가 살아 있어 비용이 남습니다. 렌더링을 막고 싶다면 아예 언마운트(null 반환 또는 조건부 생성)합니다.
SSR에서는 브라우저 전용 조건(window, matchMedia 등)으로 분기하면 하이드레이션 불일치가 생길 수 있습니다. 클라이언트 마운트 이후로 분기하거나, server-safe 가드를 둡니다.
Hooks를 조건부로 호출하면 안 됩니다. 조건이 있는 로직은 렌더 트리 레벨에서 분기하거나, 훅 내부에서 조건 처리만 하되 호출 순서는 고정합니다.
인라인 핸들러, 인라인 객체는 참조를 매번 바꿉니다. React.memo와 함께 사용할 때는 useCallback/useMemo로 안정화합니다.
9. 실무 체크리스트
1) 부모에서 조건으로 자식 자체를 생성하지 말지 결정합니다. 2) 무거운 컴포넌트는 lazy + Suspense로 필요 시만 로드합니다. 3) 리스트는 필터를 먼저, 키는 안정적으로, 대용량은 윈도잉합니다. 4) 객체/배열/함수 props는 useMemo/useCallback으로 고정합니다. 5) 뷰포트 가시성 기반으로 렌더를 연기합니다. 6) 입력과 무거운 렌더 사이에 useDeferredValue/useTransition을 둡니다. 7) Context는 범위를 쪼개거나 셀렉터 구독으로 영향 범위를 최소화합니다. 8) SSR 조건 분기와 Hooks 규칙을 위반하지 않도록 아키텍처를 설계합니다.
결론: 조건 최적화의 핵심은 “애초에 만들지 않기”와 “만들더라도 늦게 만들기”입니다. 위의 패턴을 조합하면 복잡한 화면에서도 체감 성능을 안정적으로 끌어올릴 수 있습니다.
'React' 카테고리의 다른 글
| React에서 이메일 템플릿 미리보기 UI 개발하기 (0) | 2026.07.13 |
|---|---|
| React 앱에서 파일 시스템 접근 API 활용하기 (0) | 2026.07.13 |
| React 앱에서 스마트폰 센서 데이터 통합 처리 (0) | 2026.07.12 |
| React에서 애플리케이션 Configuration 파일 동적 로드하기 (0) | 2026.07.10 |
| React 앱에서 사용자 활동 로그 시각화하기 (1) | 2026.07.10 |