useEffect 완벽 가이드: 동작 원리부터 올바른 사용법까지
🪄 useEffect
컴포넌트가 렌더링된 이후에 사이드 이펙트를 수행할 수 있게 해주는 hook
1. 핵심 동작 원리
렌더링이 완료되어 화면이 업데이트된 직후 실행
- Render Phase: 함수형 컴포넌트가 호출되고 JSX를 반환, React는 가상 DOM을 생성
- Commit Phase: React가 실제 브라우저 DOM에 변경 사항을 반영
- Browser Paint: 브라우저가 변경된 DOM을 화면에 그림
- Passive Effects: 화면이 다 그려진 후, useEffect 내부에 정의된 콜백 함수가 실행
2. 의존성 배열 비교 방식
이전 렌더링 때의 의존성 배열과 현재 렌더링 때의 의존성 배열을 비교
- 비교 알고리즘: Object.is()를 사용한 얕은 비교 수행
- 동작: 배열 안의 요소 중 하나라도 값이 바뀌었다면(false가 하나라도 있다면) React는 이펙트 함수를 실행
객체나 배열을 의존성 배열에 넣으면 내용은 같더라도 참조값이 바뀌면 React는 "변경되었다"라고 판단하여 이펙트 다시 실행
3. 클로저와 스냅샷
useEffect는 자바스크립트의 클로저 성질을 이용. 이펙트 함수는 실행될 당시의 props와 state를 가두어 둠
- 매 렌더링마다 컴포넌트는 새로 호출
- 따라서 각 렌더링은 자신만의 props와 state 스냅샷을 가짐
- useEffect 또한 해당 특정 렌더링에 종속된 함수이므로 그 시점의 데이터를 참고하게 됨
4. Clean-up 함수의 실행 타이밍
이펙트 내부에서 함수를 return 하면 이를 Clean-up 함수라고 부름
- 다음 이펙트 실행 직전: 의존성 배열의 값이 바뀌어 새로운 이펙트를 실행해야 할 때, 이전 렌더링에서 생성된 Clean-up 함수를 먼저 실행하여 이전 상태를 정리함
- Unmount 시점: 컴포넌트가 화면에서 사라질 때 마지막으로 실행
💡 사실은 useEffect가 필요하지 않을 수 있다?
1. 데이터 흐름의 추적이 어려움
- useEffect는 선언적인 리액트의 성격 안에서 명령형으로 동작하는 탈출구
- useEffect가 많아지면 특정 상태가 변했을 때 어떤 효과가 발생할 지 한 눈에 파악하기 어려움
2. 불필요한 렌더링과 성능 저하
useEffect 내부에서 상태를 업데이트하면 리액트는 추가적인 렌더링을 수행
- 렌더링 발생 (화면 그림)
- useEffect 실행
- useEffect 내부의 setState로 인해 다시 렌더링
➡️ 이 과정이 반복되면 사용자는 미세한 화면 깜빡임을 느낄 수 있고, 저사양 기기에서는 성능 병목 현상이 발생
➡️ 사실 많은 경우 useEffect 없이 렌더링 도중에 계산을 하거나 이벤트 핸들러에서 처리 가능
3. 동기화 버그
외부 API를 호출하는 useEffect가 많으면 네트워크 응답 순서에 따라 의도치 않은 결과가 화면에 나타날 수 있음
- 예시) 검색어를 바꿀 때마다 useEffect가 실행되어 데이터를 가져온다고 가정
- 첫 번째 검색어 결과보다 두 번째 검색어 결과가 더 빨리 도착하면 화면에는 결국 이전 검색 결과가 남게 되는 버그
(이를 위해 Clean-up 로직이 추가 필요)
🤔 언제 줄일 수 있을까?
- 계산 가능한 값: 다른 state나 props로 계산할 수 있는 값이라면 useEffect 대신 useMemo나 일반 변수 사용
- 이벤트 대응: 사용자 클릭 등 특정 액션에 의한 변화라면 useEffect 대신 이벤트 핸들러 함수 내부 처리
- 데이터 패칭: React Query나 SWR 같은 라이브러리를 사용하여 useEffect 없이 더 안전하게 데이터 관리