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 없이 더 안전하게 데이터를 관리할 수 있다.