본문 바로가기
프론트엔드/React

[React] useEffect 사용시 하기 쉬운 실수 & 의존성 배열

by 새파란레몬 2025. 4. 27.

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