useEffect 의존성 배열 관리 방법

kangdari·2020년 8월 22일
24

useEffect 의존성 배열 관리 방법

의존성 배열은 useEffect 훅에 입력하는 두 번째 매개변수이다. 의존성 배열의 내용이 변경되었을 경우 부수 효과 함수가 실행된다. 의존성 배열은 잘못 관리하면 쉽게 버그로 이어지기 때문에 입력하지 않는게 좋지만 필요에 있어 입력할 일이 생긴다.

부수 효과 함수에서 API 호출하는 경우

아래 코드의 fetchUse는 렌더링될 때마다 호출되기 때문에 비효율적이다.

useEffect(() => {
  fetchUser(userId).then((data) => setUser(data));
});

이렇게 의존성 배열에 userId 상태 값을 넣으면 userId가 변경될 때만 fetchUser 함수가 호출된다. 추후에 새로 추가되는 변수가 있다면 그 변수도 의존성 배열에 넣어주어야 한다.

useEffect(() => {
  fetchUser(userId).then((data) => setUser(data));
}, [userId]);

useEffect 훅에서 async / await 함수 사용

useEffect 훅에서 부수 효과함수를 async / await 함수로 만들면 오류가 발생한다. 왜냐하면 부수 효과 함수는 함수만 반환할 수 있는데 async / await 함수는 Promise 객체를 반환하기 때문이다.

  useEffect(async () => {
    const data = await fetchUser(userId);
    setUser(data);
  }, [userId]);

async / await 함수 사용하기 위해서는 부수효과 함수 내부에 async / await 함수를 만들어 호출하면 된다.

  useEffect(() => {
    async function fetchAndSetUser() {
      const data = await fetchUser(userId);
      setUser(data);
    }
    fetchAndSetUser();
  }, [userId]);

useEffect 훅 밖에서 fetchAndSetUser 함수가 필요한 경우

개발을 하다보면 useEffect 안의 함수를 훅 밖으로 빼야 하는 상황이 있다.

  async function fetchAndSetUser(value) {
    const data = await fetchUser(userId);
    setUser(data);
  }

  useEffect(() => {
    fetchAndSetUser(true);
  }, [fetchAndSetUser]);

  return <button onClick={() => fetchAndSetUser(false)}>더보기</button>;

fetchAndSetUser 함수를 useEffect 훅 밖으로 빼내 작성했다. 이제 훅 내부에서 fetchAndSetUser 함수를 사용하므로 의존성 배열에 추가해준다.

그런데 fetchAndSetUser 함수는 렌더링할 때마다 갱신되므로 결과적으로 fetchAndSetUser 함수는 렌더링을 할 때마다 호출된다.

이 문제를 해결하려면 fetchAndSetUser 함수가 필요할 때만 갱싱되도록 해야한다. 이럴 때 useCallback을 사용한다.

  const fetchAndSetUser = useCallback(async () => {
    const data = fetchUser(userId);
    setUser(data);
  }, [userId]);

이제 fetchAndSetUser 함수는 userId 값이 변경될 때만 갱신된다.

의존성 배열을 없애는 방법

의존성 배열은 사용하지 않는것이 좋다. 에러의 원인이 되고 관리하는데 생각보다 많은 시간과 노력이 필요하다.

부수 효과 함수 내에서 분기 처리하기
의존성 배열을 작성하지 않고 부수 효과 함수 내에서 실행 시점을 조절 할 수 있다.

function Profile({ userId }) {
  const [user, setUser] = useState();
  async function fetchAndSetUser(needDetail) {
    const data = await fetchUser(userId, needDetail);
    setUser(data);
  }

  useEffect(() => {
    if (!user || user.id !== userId) {
      fetchAndSetUser(false);
    }
  });
}

부수 효과 함수 내에서 if문으로 fetchAndSetUser 호출 시점을 관리한다. 이렇게 의존성 배열을 입력하지 않으면 부수 효과 함수에서 사용된 모든 변수는 가장 최신화된 값을 참조한다. 이제 useCallback 훅을 사용하지 않아도 된다.( 이해 안됨 ㅋㅋ)

useState의 상태 값 변경 함수에 입력하기
이전 상태 값을 기반으로 다음 상태 값을 계산하기 위해 의존성 배열에 상태 값을 배열에 추가하는 경우가 있다. 이 경우 상태 값 변경 함수에 함수를 입력하면 상태 값을 의존성 배열에서 제거할 수 있다.

화면 클릭 시 count가 1씩 증가하는 컴포넌트다. useEffect를 보면 의존성 배열에 count 값이 들어있고 onClick 함수에서는 이전 상태를 기반으로 상태를 변경하고 있다.

function Component() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    function onClick() {
      setCount(count + 1);
    }
    window.addEventListener('click', onClick);
    return () => window.removeEventListener('click', onClick);
  }, [count]);
}

setCount 함수에 함수를 입력하면 이전 상태 값을 매개변수로 받을 수 있다. 이렇게 작성하면 의존성 배열에서 상태 값을 제거할 수 있다.

  useEffect(() => {
    function onClick() {
      setCount((prev) => prev + 1);
    }
    window.addEventListener('click', onClick);
    return () => window.removeEventListener('click', onClick);
  }, [count]);

useReducer 활용하기
여러 상태 값을 참조하면서 값을 변경할 때는 useReducer 훅을 사용하는게 좋다.
예를 들어 타이머에서 시간이 흐를 때 시, 분, 초 세 가지 상태 값을 참조하면서 값을 변경한다.

function Timer({ initialTotalSeconds }) {
  const [hour, setHour] = useState(Math.floor(initialTotalSeconds / 3600));
  const [minute, setMinute] = useState(Math.floor((initialTotalSeconds & 3600) / 60));
  const [second, setSecond] = useState(initialTotalSeconds % 60);

  useEffect(() => {
    const id = setInterval(() => {
      if (second) {
        setSecond(second - 1);
      } else if (minute) {
        // minute가 바뀌면
        setMinute(minute - 1);
        setSecond(59);
      } else if (hour) {
        setHour(hour - 1);
        setMinute(59);
        setSecond(59);
      }
    }, 1000);
    return () => clearInterval(id);
  }, [hour, minute, second]);

  return <div>{`${hour} : ${minute} : ${second}`}</div>;
}

setInterval를 사용했음에도 위 코드는 1초마다 상태 값이 변하기 때문에 setInterval, clearInterval을 반복해서 호출한다.

useReducer를 사용하여 setInterval을 한 번만 호출하도록 개선할 수 있다.

function Timer({ initialTotalSeconds }) {
  const [state, dispatch] = useReducer(reducer, {
    hour: Math.floor(initialTotalSeconds / 3600),
    minute: Math.floor((initialTotalSeconds & 3600) / 60),
    second: initialTotalSeconds % 60,
  });
  const { hour, minute, second } = state;

  useEffect(() => {
    const id = setInterval(() => dispatch(state), 1000);
    return () => clearInterval(id);
  });

  return <div>{`${hour} : ${minute} : ${second}`}</div>;
}

function reducer(state) {
  const { hour, minute, second } = state;

  if (second) {
    return { ...state, second: second - 1 };
  } else if (minute) {
    return { ...state, minute: minute - 1, second: 59 };
  } else if (hour) {
    return { ...state, hour: hour - 1, minute: 59, second: 59 };
  } else {
    return state;
  }
}

세 개의 상태 값은 useReducer 훅으로 관리한다.
리액트에서 dispatch는 변하지 않는 값(항상 고정된 값)이므로 의존성 배열에서 제거할 수 있다.
useEffect 내부에서는 무슨 일이 일어났는지 명시만 할 뿐, 상태 값을 직접 가져다 사용하지 않는다. 이렇게 하면 useEffect는 컴포넌트 내부의 어떤 상태 값도 사용하지 않는다.

useRef 활용하기
속성 값으로 전달되는 함수같이 해당 속성 값이 렌더링 결과에 영향을 주는 값이 아니라면 useRef 훅을 이용해서 의존성 배영르 제거할 수 있다.

function Component({ onClick }) {
  useEffect(() => {
    window.addEventListener('click', () => {
      onClick();
      // ...
    });
    // 연산량이 많은 코드
  }, [onClick]);
}

속성으로 전달된 onClick 함수는 내용은 그대로인데 렌더링할 때마다 변경되는 경우가 많다. 이로 인해 부수 효과 함수가 불필요하게 자주 호출된다.

아래 코드에서는 속성으로 전달된 onClick 함수를 useRef에 저장한다. useRef에 저장된 값은 변경된다 해도 컴포넌트가 리렌더링 되지 않는 특성을 이용한 것이다. 때문에 부수 효과 함수에서 사용된 useRef 값은 의존성 배열에 추가할 필요가 없다.

function Component({ onClick }) {
  const onClickRef = useRef();
  useEffect(() => {
    onClickRef.current = onClick;
  });

  useEffect(() => {
    window.addEventListener('click', () => {
      onClickRef.current();
    });
    // 연산량이 많은 코드
  });
}

0개의 댓글