React 애플리케이션의 성능과 안정성은 화면 뒤에서 일어나는 렌더링 메커니즘에 대한 깊은 이해에서 시작됩니다. 많은 개발자들이 React를 어느 정도 안다고 생각하지만, 막상 "컴포넌트는 정확히 언제, 어떻게 다시 그려지는가?" 라는 질문 앞에서는 자신 있게 답하기 어려워합니다.
useState의 상태 업데이트 동작 useMemo, useCallback 같은 메모이제이션 훅의 필요성 useEffect의 적절한 사용 시점 이 지식들은 예측 불가능한 버그를 막고, 사용자 경험을 극대화하는 핵심 열쇠입니다.
이 글에서는 React 렌더링의 기본 원리부터, 상태 관리의 핵심(useState), 자동 배칭(Automatic Batching), 그리고 최적화 훅(useMemo, useCallback, useEffect) 의 올바른 사용법까지 깊이 있게 다룹니다.
React 컴포넌트의 렌더링은 크게 두 가지입니다.
최초 렌더링 (Initial Render)
컴포넌트가 처음 화면에 나타날 때, React는 코드를 순차적으로 실행하여 초기 UI를 구성합니다.
리렌더링 (Re-rendering)
props나 state 값이 변경되면 컴포넌트를 다시 렌더링합니다. 이때 함수 전체가 다시 호출되며 내부 모든 코드가 다시 실행됩니다.
문제는 리렌더링이 불필요하게 자주 발생할 때입니다. 작은 상태 변화 하나가 많은 자식 컴포넌트를 연쇄적으로 리렌더링시켜 성능 저하를 일으킵니다.
useState는 React에서 가장 기본적인 훅이지만, 그 동작 원리를 잘못 이해하면 버그를 만들기 쉽습니다.
핵심은 스코프(Scope) 와 스냅샷(Snapshot) 입니다.
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
👉 결과: **카운트 값: 1, 렌더링 횟수: 1**
- `handleClick` 실행 시 `count`는 0으로 고정된 **스냅샷 값**
- 따라서 세 번의 `setCount`는 모두 `1`을 설정하려는 시도
- React는 이를 **중복 업데이트**로 묶어 한 번만 적용
### 해결: 함수형 업데이트(Functional Update)
```tsx
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
👉 최종 count = 3
자동 배칭은 React가 여러 개의 상태 업데이트를 한 번의 렌더링으로 묶는 최적화 기법입니다.
setState 호출 → 자동으로 하나로 묶임앞선 예제에서 세 번 setCount 했지만 렌더링이 한 번만 일어난 이유가 바로 자동 배칭입니다.
const result = useMemo(() => heavyCalculation(data), [data]);
const handleClick = useCallback(() => doSomething(), [deps]);
| 구분 | useMemo | useCallback |
|---|---|---|
| 목적 | 값의 메모이제이션 | 함수 메모이제이션 |
| 반환 값 | 값(Value) | 함수(Function) |
| 사용 예시 | 복잡한 연산 결과값 캐싱 | 자식에 함수 전달 최적화 |
useEffect는 렌더링 이후 부수효과(Side Effect) 를 처리하는 훅입니다.
예: API 호출, DOM 조작, 구독, 타이머 등
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id); // cleanup
}, []);
정리하면:
올바른 훅 사용법을 이해하면 React 애플리케이션의 성능과 안정성을 모두 잡을 수 있습니다.
