Somedding

useEffect는 어떤 점에서 비효율적인가? 본문

React

useEffect는 어떤 점에서 비효율적인가?

somedding 2024. 12. 16. 12:00

그동안 프로젝트를 진행하면서 무지성 useEffect를 많이 사용해 왔다.

하지만, 도저히 비효율을 견딜 수 없어서 필요 여부에 따라 효율적으로 코드를 작성하고자 한다.

 

 

useEffect란?

아마 리액트에서 useState 다음으로 많이 볼 수 있는 훅이 아닐까 싶다.

useEffect는 리액트 컴포넌트의 렌더링 이후 특정 작업을 실행할 수 있도록 하는 훅이다.

 

즉, 컴포넌트의 생명주기 중 Side Effect(부수효과)를 처리하는 용도로 사용된다.

 

useEffect는 다음과 같은 특징이 있다.

- 렌더링 이후 실행되는 코드를 정의할 수 있음.

- 의존성 배열을 기반으로 실행 시점을 조절할 수 있음.

- 컴포넌트 언마운트 시 클린업 수행 가능.

 

하지만 이런 useEffect는 꽤나 비효율적인 면을 가지고 있다.

 

 

useEffect의 비효율

useEffect는 잘못 사용하면 성능 문제나 예기치 않은 버그를 발생할 수 있다.

특히, 의존성 배열이나 클린업 함수를 잘못 다루면 비효율적인 동작이 발생한다.

 

1. 당연하게도 의존성 배열이 없는 경우나, 불필요한 값이 포함된 경우에는 성능을 저하시킨다.

// ⚠️ 의존성 배열 없음
useEffect(() => {
  console.log("매 렌더링마다 실행");
});

// ⚠️ 의존성 배열에 불필요한 값이 있음
useEffect(() => {
  console.log("dep이 변경될 때마다 실행되어야 함");
}, [dep, no]);


// 🥰 해결 방법
useEffect(() => {
  console.log("dep이 변경될 때마다 실행");
  }, [dep]);

 

2. 또한 당연하게도 무한루프의 가능성이 있다.

const [count, setCount] = useState(0);

// ⚠️ count가 계속 변경되어 무한 루프 발생
useEffect(() => {
  setCount(count + 1);
}, [count]);

 

3. 클린업 함수가 없으면 메모리 누수가 발생할 수 있다.

// ⚠️ 클린업 함수 없음
useEffect(() => {
  const timer = setInterval(() => {
    console.log('타이머 실행 중');
  }, 1000);
}, []);

// 🥰 해결 방법
useEffect(() => {
  const timer = setInterval(() => {
    console.log('타이머 실행 중');
  }, 1000);

  return () => {
    clearInterval(timer); // 타이머 정리
  };
}, []);

 

이제 이런 간단한, 누구나 알 법한 것들 말고

더 본격적으로 들어가 보면 효율적으로 만들면서 더 뿌듯할 수 있다.

 

4. API 요청 시 불필요한 함수 재생성

사용자의 입력값에 따라 매번 API를 호출해야 하는 코드를 생각해 보자.

(예를 들면 자동완성 같은 기능)

그러면 다음 코드를 일반적으로 생각할 수 있다.

const fetchData = async () => {}

useEffect(() => {
  fetchData();
}, [input]);

매 입력마다 fetchData()가 재생성되고, API 호출이 발생한다.

하지만 문제가 생길 부분이 존재한다.

fetchData함수는 컴포넌트의 리렌더링마다 재생성된다는 것이다.

(물론 매 입력마다 API 호출을 발생시키는 것 자체도 비효율적이기 때문에 이것에 대해 다루는 게시글을 작성할 예정)

 

그래서 우리는 함수가 불필요하게 재생성되는 비효율을 useCallback으로 해결할 수 있다.

const fetchData = useCallback(async () => {}, [input]); // fetchData()는 input이 변경될 때에만 생성

useEffect(() => {
  fetchData();
}, [fetchData]);

useEffect의 의존성 배열에 fetchData 자체를 넣어주고, fetchData를 useCallback으로 생성한다.

fetchData는 의존성 배열이 input인 useCallback이므로 input이 변경될 때만 재생성된다.

 

5. 단순 계산

단순 계산을 useEffect로 하면 불필요한 렌더링이 발생될 수 있다.

다음 코드는 두 개의 입력값으로부터 합계를 계산하는 코드이다.

const A = () => {
  const [number1, setNumber1] = useState(0);
  const [number2, setNumber2] = useState(0);
  const [sum, setSum] = useState(0);

  useEffect(() => {
    setSum(number1 + number2);
  }, [number1, number2]);

  return (
    <div>
      <input
        type="number"
        value={number1}
        onChange={(e) => setNumber1(parseInt(e.target.value))}
      />
      <input
        type="number"
        value={number2}
        onChange={(e) => setNumber2(parseInt(e.target.value))}
      />
      <p>합계: {sum}</p>
    </div>
  );
};

number1과 number2가 변경될 때마다 sum의 상태를 변경한 후, UI에 출력한다.

이 코드가 비효율적인 이유는 불필요한 렌더링이 존재하고, 상태 업데이트가 과도하게 이루어진다.

 

여기서 useEffect를 제거하고, useState로 생성한 sum을 없애면 훨씬 효율적인 코드가 된다.

const [number1, setNumber1] = useState(0);
const [number2, setNumber2] = useState(0);

const sum = number1 + number2; // 합계를 직접 계산

단순히 계산만 하면 되기 때문에 sum을 상태로 관리하지 않고 단순 변수로 설정해도 된다.

이렇게 해도 sum은 여전히 매 렌더링 시에 number1 + number2의 값을 반영하기 때문에

오히려 useEffect의 사용은 비효율적이다.

 

 

 

작은 프로젝트면 그러려니 하겠지만(사실 안됨)

큰 프로젝트가 될수록 이런 문제점들은 계속해서 쌓이고, 결국 눈에 띄는 성능 문제로 이어진다.

 

그렇기 때문에 이런 기본적인 부분들에서부터 효율성을 챙기려고 노력해야 한다.

useEffect는 피한다기보다는 오히려 필수적으로 사용해야 하는 경우가 많기 때문에

조심할 필요가 있다.

 

 

 

 

 

 

글 내용 중, 잘못됐거나 더 알아야 하는 지식이 있다면 댓글로 남겨주시면 감사하겠습니다!

모두 좋은 하루 보내세요:)

Comments