[React] useEffect

Jin·2022년 2월 4일
1

React

목록 보기
2/3
post-thumbnail

React 컴포넌트는 기본적으로 내부 상태(state)가 변할 때 마다 다시 렌더링을 한다.

useEffect

명령형 또는 어떤 effect를 발생하는 함수를 인자로 받는다.
변형, 구독, 타이멍, 로깅 또는 다른 side effect는 함수 컴포넌트의 본문안에서 허용되지 않기 때문에 useEffect를 사용할 수 있다.
useEffect에 전달된 함수는 기본적으로 화면에 모든 렌더링이 완료 된 이후에 실행이 되지만, 어떤 값이 변경되었을 때만 실행되게 할 수도 있다.

Side Effect 란?

React 컴포넌트가 화면에 렌더링 된 이후에 비동기로 처리되어야 하는 부수적인 효과들을 흔히 Side EFfect라고 한다. 예를들어, 어떤 데이터를 가져오기 위해 외부 API를 호출하는 경우, 일단 화면에 렌더링 할 수 있는 것은 먼저 렌더링하고 실제 데이터는 비동기로 가져오는것이 권장된다. 요청 즉시 1차 렌더링을 함으로 연동하는 API가 응답이 늦어지거나 응답이 없을 경우에도 영향을 최소화 시킬수 있어 사용자 경험 측면에서 유리하기 때문이다.

effect 정리

effect는 종종 컴포넌트가 화면에서 제거 될때 정리해야하는 리소스를 만든다.

useEffect(() => {
  const subscription = props.source.subscribe()
  return () => {
	// Clean up the subscription
    subscription.unsubscribe()
  }
})

정리 함수는 메모리 누수 방지를 위해 UI에서 컴포넌트를 제거하기 전에 수행이 된다. 만약, 컴포넌트가 여러번 렌더링 된다면 다음 effect가 수행되기 전에 이전 effect는 정리가 된다. 위의 코드에서는, 매 갱신마다 새로운 구독이 생선된다고 볼수 있다.

조건부 effect 발생

effect의 기본 동작은 모든 렌더링을 완료한후에 effect를 발생하는 것이다. 하지만 이 방법은 일부 경우에 과도한 작업이 될수가있다. 예시로 위의 코드에서는, 항상 새로운 subscription을 생성할 필요는 없고, 오직 sorce prop이 변경이 될때만 업데이트를 하면 된다.

이것을 수행하기 위해 useEffect에 두번째 인자를 전달 할 수가 있다.

useEffect(
  () => {
    const subscription = props.source.subscribe()
    return () => {
      subscription.unsubscribe()
    }
  },
  [props.source],
)

이제는 props.source가 변경될 때에만 구독이 재생성된다.

effect를 한번만 실행하고 싶다면 두 번째 인자로 빈 배열[]을 전달할 수 있다. 이를 통해 effect는 React에게 props나 state에서 가져온 어떤값에도 의존하지 않으므로, 다시 실행할 필요가 전혀 없다는것을 알려준다.
빈 배열을 전달하면 effect 안에 있는 props와 state는 항상 초깃값을 가지게 된다.

dependency 배열의 생략 여부

function Example({someProp}) {
  function doSomething() {
    consol.elog(someProp)
  }
  
  useEffect(() => {
    doSomething()
  }, [] ) // someProp을 사용하는 doSomething을 호출하고 있다.
}

Effect 외부의 함수에서 어떤 props, state가 사용되는지 기억하기 어렵다. 그렇기에 보통 함수를 effect 안에서 선언하여 사용한다. 그러면 effect가 미치는 컴포넌트 범위의 값을 쉽게 확인할 수 있다.

function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);
    }

    doSomething();
  }, [someProp]); // efefct는 someProp만 사용하기에 안전하다.
}

컴포넌트에 있는 value를 사용하지 않으면 빈 배열로 놔둬도 괜찮다.

useEffect(() => {
  function doSomething() {
    console.log('hello');
  }

  doSomething();
}, []); // 컴포넌트에 위치한 value를 사용하지 않기에 안전

만약 useEffect, useLayoutEffect, useMemo, useCallback 그리고 useImperativeHandle에 특정 list of dependencies를 지정하면, 콜백 내에서 사용되는 모든 값을 포함하고 React 데이터 흐름에 참여해야 한다. props, state 및 파생된 모든것이 포함된다.

함수 컴포넌트가 props, state 또는 파생된 값을 참조하지 않는 경우에만 dependency 목록에서 함수 컴포넌트를 생략하는 것이 안전하다.

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null)

  async function fetchProduct() {
    const response = await fetch('http://myapi/product/' + productId);// productId props 사용
    const json = await response.json();
    setProduct(json)
  }

  useEffect(() => {
    fetchProduct()
  }, []); //  fetchProduct가productId를 사용하므로 안전하지 않다.

추천되는 해결 방법은 함수를 effect 안으로 옮기는 것이다. 그렇게 함으로써, effect가 어떤 props와 state를 사용하는지 쉽게 알수 있고, 모두 선언되었는지 확인할 수 있다.

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null)

  useEffect(() => {
    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId)
      const json = await response.json();
      setProduct(json)
    }

    fetchProduct()
  }, [productId]) // productId만 사용하므로 안전

}

effect 내부의 로컬 변수를 사용해 비순차적인 응답도 처리가 가능하다.

useEffect(() => {
    let ignore = false
    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId)
      const json = await response.json()
      if (!ignore) setProduct(json)
    }

    fetchProduct()
    return () => { ignore = true }
  }, [productId]) // Effect 내부로 함수를 옮겼으므로 ignore은 의존성 배열에 있을 필요가 없다.

함수를 effect 내부로 이동할 수 없는 경우

  • 해당 함수를 컴포넌트 외부로 이동 해 볼수 있다. 이 경우 함수는 props나 state를 참조하지 않도록 보장되며 종속성 목록에 있을 필요도 없다. (이 방법을 사용하여 작성해 본 useEffect)
  • 호출하는 함수가 순수한 계산이고 렌더링하는 동안 호출해도 안전하면, 대신에 effect 외부에서 호출하고 반환된 값에 따라 effect가 달라지도록 할 수 있다.
  • Effect 의존성 배열에 함수를 추가 하되, 정의를 useCallbac 에 감싸줘야 한다. 이렇게 하면 자체 종속성도 변경되지 않는 한 모든 렌더링에서 변경 되지 않는다.
function ProductPage({ productId }) {
  // Wrap with useCallback to avoid change on every render
  const fetchProduct = useCallback(() => {
    // ... Does something with productId ...
  }, [productId]); // All useCallback dependencies are specified

  return <ProductDetails fetchProduct={fetchProduct} />;
}

function ProductDetails({ fetchProduct }) {
  useEffect(() => {
    fetchProduct();
  }, [fetchProduct]); //  All useEffect dependencies are specified
  // ...
}

위의 예에서, 함수를 dependencies list에 추가를 해줘야한다. 그래야 ProductPage의 productId props가 변경되면 productDetails 컴포넌트에서 자동으로 다시 가져오기가 실행된다.

effect 종속성이 너무 자주 변경되는 경우

state가 너무 자주 변경되어 dependencies list에서 생략하고 싶겠지만, 이러한 경우에 일반적으로 버그가 발생한다.

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // This effect depends on the `count` state
    }, 1000);
    return () => clearInterval(id);
  }, []); // 🔴 Bug: `count` is not specified as a dependency

  return <h1>{count}</h1>;
}

dependencies list에 count를 지정하면 버그가 수정되지만, 변경될때마다 간격이 재설정된다. 이를 해결하기 위해 현재 state를 참조하지 않고 state를 변경하는 방법을 사용할 수 있다.

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // 외부 변수 count를 사용하지 않는다
    }, 1000);
    return () => clearInterval(id);
  }, []); //  Counter 함수의 변수를 사용하지 않으므로 빈 배열로 놔둬도 안전하다.

  return <h1>{count}</h1>;
}

sideEffect 자료 출처: https://www.daleseo.com/react-hooks-use-effect/
useEffect 자료 출처 : https://reactjs.org/docs/hooks-intro.html

profile
내가 다시 볼려고 작성하는 블로그. 아직 열심히 공부중입니다 잘못된 내용이 있으면 댓글로 지적 부탁드립니다.

0개의 댓글