React는 재사용성을 위해 컴포넌트라는 개념을 사용하고 컴포넌트의 렌더링을 제어하기 위해서 useEffect를 사용한다.
⏰ useEffect는 언제 실행되는가?
- 기본적으로 컴포넌트가 화면에 그려진 이후에 실행된다.
🎛️ 실행 조건의 제어는?
- 전달된 의존성 배열에 따라 useEffect 내부의 Effect의 실행 여부를 결정한다.
- 이전 렌더링의 의존성 배열값과 현재의 값을 비교하여 (JS의 Object.is를 비교 사용) 변경 사항이 있다면 effect의 콜백을 수행한다.
- 의존성 배열이 제공되지 않았다면 매 렌더링 마다 항상 effect를 호출한다.
참고 : React 의 Strict Mode에서는 useEffect를 두 번 호출하여 순수성을 체크하기 때문에 두 번 실행된다.
📪 의존성 배열에 따른 useEffect의 실행 차이
- 기본적으로 useEffect 에 따라 Effect의 호출 타이밍과 빈도가 달라진다.
1. 의존성 배열의 생략 : useEffect(fn) 형태
- 해당 Effect는 컴포넌트가 렌더링 될 때마다 실행이 된다.
- 즉 초기 마운트 후 모든 업데이트 렌더링마다 빠짐없이 effect 콜백이 호출된다.
- 한눈에 봐도 꽤나 비효율적인게 느껴진다. -> 거의 사용하지 않는다.
코드 예시
function LoggerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`렌더링 완료, 현재 count: ${count}`);
}); // 의존성 배열 없음 – 매번 실행
return <button onClick={() => setCount(count + 1)}>증가</button>;
}
2. 빈 의존성 배열 전달한 경우
- 해당 Effect 는 컴포넌트 마운트 시 한번만 실행되고 이후의 업데이트에서는 재실행되지 않는다.
- 언마운트 될 때는 clean-up 함수가 한 번 호출된다. clean up 함수는 보통 언마운트 시 이벤트 리스너를 활용한 코드 작성 시에 해당 이벤트 리스너를 제거하기 위해 사용한다. ( 대표적으로 resize )
- react life cycle에서는 componentDidMount 와 componentWillUnmount에 해당한다.
코드 예시
function ProfilePage() {
useEffect(() => {
// 마운트 시 한 번 실행: 예를 들어 데이터 패치
fetchUserProfile();
return () => {
// 언마운트 시 실행: 이벤트 리스너 제거 등 정리 작업
window.removeEventListener('resize', handleResize);
};
}, []); // 빈 배열 – 마운트시 1회 실행, 언마운트시 정리
…
}
3. 의존성 배열에 static한 객체, 함수를 넣은 경우
- 의존성 배열에는 보통 state나 props 값을 넣게 되는데, 사용법을 잘 몰라 객체를 넣은 적이 있었다. 왜 그러면 안 되는지 정리해 본다.
- 컴포넌트 랜더링 시마다 새로 생성되는 객체 리터럴이나 함수는 참조가 매번 달라지기 때문에 의존성에 넣으면 매 렌더링마다 변경된 것으로 간주된다. 그러므로 effect 가 매번 실행된다.
코드 예시
function Example() {
const [count, setCount] = useState(0);
const obj = { value: 123 }; // 렌더링 때마다 새로운 객체 생성
useEffect(() => {
console.log('Effect 실행 - obj 변경 감지');
}, [obj]); // 객체를 의존성으로 추가
return <button onClick={() => setCount(c => c + 1)}>Update count</button>;
}
이 코드에서 obj는 컴포넌트 함수가 호출될 때마다 새로운 객체가 되어, button 클릭 시마다 count가 변경된 재랜더링 된다.
> 이 경우에는 의존성 배열을 안 쓴 것과 같은 결과가 된다. 위 코드는 “컴포넌트 내부에서 정의된 객체나 함수는 Effect를 불필요하게 자주 재실행시킬 위험이 있다”고 React 공식 문서에 맞는 잘못된 코드 예시이다.
해결법 : 정말 필요한 객체라면 컴포넌트 외부나 useMemo를 통해 참조 동일성(reference equality)을 보장해야 한다.
4. React Query의 useQuery와 함께 사용 시 의존성 배열
- React Query에서 useQuery 가 일반적으로 반환하는 { data, error, isLoading,... } 등의 경우, 반환 객체는 매 렌더마다 참조가 바뀔 수 있지만 data 속성은 React Query 내부에서 동일한 참조를 유지하도록 최적화를 해주고 있다.
코드 예시
const { data: todos } = useQuery(['todos'], fetchTodos);
useEffect(() => {
console.log('할 일 목록 업데이트:', todos);
// 데이터가 갱신될 때에만 실행됨
}, [todos]);
위와 같이 사용하고 useQuery의 반환값 전체를 의존성으로 넣으면 안 된다. 예를 들어 const query = useQuery(...); useEffect(..., [query]);처럼 쓰는 것은 좋지 않다.
또한 지양해야 할 패턴이 있는데, React Query의 데이터를 가져온 뒤 추가 가공을 위해 useEffect를 사용하는 패턴이다.
코드 예시
// 안티 패턴: React Query 데이터로 로컬 상태 설정
const { data } = useQuery(['items'], fetchItems);
const [items, setItems] = useState([]);
useEffect(() => {
if (data) setItems(data); // 쓸데없이 data를 로컬 state로 복사
}, [data]);
이렇게 하면 data 가 바뀔 때마다 items 상태 업데이트로 추가 렌더링이 발생해 비효율적이다. 데이터가 로드된 후의 처리가 필요하다면 onSuccess를 활용해 보는 것이 좋다.
🚫 useEffect 사용 시 주의점 정리 ( 하지 말아야 할 것! 🙅🏻♀️)
1. 의존성 배열 누락 또는 오용
2. 무한 루프 렌더링
- 아래와 같은 코드는 절대 작성하지 않도록 하자. (상태 업데이트 -> 재렌더 -> effect -> 다시 상태 업데이트... 와 같은 무한 루프)
useEffect(() => {
setCount(count + 1);
}, [count]);
3. 불필요한 이중 상태 관리
4. 이벤트 리스너 or 타이머에서 Effect 정리 함수 누락
5. 렌더링 관련 없는 작업에 사용
- React 공식 문서 조언: "Effect가 단지 props나 state를 가지고 뭔가 state를 업데이트하는 것이라면, 애초에 effect가 필요하지 않을 수 있다"는 점을 기억하세요.
'프론트엔드 > React' 카테고리의 다른 글
Event 버블링 & 캡쳐링 in React (0) | 2024.12.01 |
---|