Hooks - useEffect

흑우·2023년 8월 9일

useEffect(setup, dependencies?)

  • useEffect는 컴포넌트를 외부 시스템과 동기화할 수 있는 React 훅입니다.

참조

선언하기

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);
  // ...
}
  • 컴포넌트의 최상위 레벨에서 useEffect를 호출하여 Effect를 선언합니다.

매개변수

setup

  • Effect의 로직이 포함된 함수입니다. 셋업 함수는 선택적으로 클린업 함수를 반환할 수도 있습니다.
  • React는 컴포넌트가 DOM에 추가되면 셋업 함수를 실행합니다.
  • 의존성이 변경되어 다시 렌더링할 때마다 React는 (클린업 함수가 있는 경우) 먼저 이전 값으로 클린업 함수를 실행한 다음, 새 값으로 셋업 함수를 실행합니다.
  • 컴포넌트가 DOM에서 제거되면, React는 마지막으로 클린업 함수를 실행합니다.

dependencies (option)

  • setup 코드 내에서 참조된 모든 반응형 값의 목록입니다. 반응형 값은 props, state, 컴포넌트 본문 내부에서 직접 선언한 모든 변수와 함수를 포함합니다.
  • React용으로 구성된 린터는 모든 반응형 값이 의존성에 잘 지정되었는지 확인합니다. 의존성 목록에는 고정된 수의 항목이 있어야 하며 [dep1, dep2, dep3]과 같이 인라인으로 작성해야 합니다.
  • React는 각 의존성에 대해 Object.is로 이전 값과 비교합니다. 의존성을 전혀 지정하지 않으면 컴포넌트를 다시 렌더링할 때마다 Effect가 다시 실행됩니다.

반환값

  • useEffect는 undefined를 반환합니다.

😀 주의사항

  • useEffect는 훅이므로 컴포넌트의 최상위 레벨 또는 자체 훅에서만 호출할 수 있습니다.
    • 반복문이나 조건문 내부에서는 호출할 수 없습니다. 필요한 경우 새 컴포넌트를 추출하고 state를 그 안으로 옮기세요.
  • 외부 시스템과 동기화하려는 목적이 아니라면 Effect가 필요하지 않을지도 모릅니다.
  • Strict 모드가 켜져 있으면 React는 첫 번째 실제 셋업 전에 개발 전용의 셋업+클린업 사이클을 한 번 더 실행합니다.
    • 이는 클린업 로직이 셋업 로직을 “미러링”하고 셋업이 수행 중인 모든 작업을 중지하거나 취소하는지를 확인하는 스트레스 테스트입니다.
    • 문제가 발생하면 클린업 기능을 구현해야 합니다.
  • 의존성 중 일부가 컴포넌트 내부에 정의된 객체 또는 함수인 경우 Effect가 필요 이상으로 자주 다시 실행될 위험이 있습니다. (중요합니다!!)
    • 이 문제를 해결하려면 불필요한 객체 및 함수 의존성을 제거하세요.
    • 혹은 Effect 외부에서 state 업데이트 추출 및 비반응형 로직을 제거할 수도 있습니다.
  • Effect가 상호작용(예: 클릭)으로 인한 것이 아니라면, React는 브라우저가 Effect를 실행하기 전에 업데이트된 화면을 먼저 그리도록 합니다.
    • Effect가 시각적인 작업(예: 툴팁 위치 지정)을 하고 있고, 지연이 눈에 띄는 경우(예: 깜박임), useEffect를 useLayoutEffect로 대체해야 합니다.
  • 상호작용(예:클릭)으로 인해 Effect가 발생한 경우에도, 브라우저는 Effect 내부의 state 업데이트를 처리하기 전에 화면을 다시 그릴 수 있습니다.
    • 보통 이게 기대하는 동작일 것입니다. 만약 브라우저가 화면을 다시 칠하지 못하도록 차단해야 하는 경우라면 useEffect를 useLayoutEffect로 바꿔야 합니다.
  • Effects는 클라이언트에서만 실행됩니다. 서버 렌더링 중에는 실행되지 않습니다. (Next를 사용하실 때는 컴포넌트 최상단에 'use client'를 적으셔야 합니다!!!)

😀 사용법

1. 외부 시스템에 연결하기

  • 때로는 컴포넌트가 페이지에 표시되는 동안 네트워크, 일부 브라우저 API 또는 타사 라이브러리에 연결 상태를 유지해야 할 수도 있습니다. 이러한 시스템은 React에서 제어되지 않으므로 외부(external) 라고 합니다.
  • 컴포넌트를 외부 시스템에 연결하려면 컴포넌트의 최상위 레벨에서 useEffect를 호출하세요.
function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
  	const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]);
  // ...
}
  • useEffect에는 두 개의 인자를 전달해야 합니다.
    • 첫 번째: 해당 시스템에 연결하는 셋업 코드가 포함된 셋업 함수.
      • 해당 시스템과의 연결을 끊는 클린업 코드가 포함된 클린업 함수를 반환해야 합니다.
    • 두 번쨰: 해당 함수 내부에서 사용되는 컴포넌트의 모든 값을 포함한 의존성 목록.

React는 필요할 때마다 셋업 및 클린업 함수를 호출하는데, 이는 여러 번 발생할 수 있습니다.

  • 컴포넌트가 페이지에 추가될 때 (마운트) 마다 셋업 코드를 실행합니다.
  • 의존성이 변경된 컴포넌트를 다시 렌더링할 때마다
    • 먼저 이전 props와 state로 클린업 코드를 실행합니다.
    • 그런 다음 새 props와 state로 셋업 코드를 실행합니다.
  • 컴포넌트가 페이지에서 제거되면 (마운트 해제) 마지막으로 한 번 클린업 코드를 실행합니다.

위의 예제 설명

  1. 위의 ChatRoom 컴포넌트가 페이지에 추가되면 초기 serverUrl 및 roomId로 채팅방에 연결됩니다.
  2. 다시 렌더링한 결과 serverUrl 또는 roomId가 변경되면(예: 사용자가 드롭다운에서 다른 채팅방을 선택하는 경우) Effect는 이전 채팅방과의 연결을 끊고 다음 채팅방에 연결합니다.
  3. ChatRoom 컴포넌트가 페이지에서 제거되면 Effect는 마지막으로 연결을 끊습니다.

버그를 찾는 데 도움을 주기 위해 개발 환경에서 React는 실제 셋업 전에 셋업 및 클린업을 한 번 더 실행합니다.

  • 이로 인해 눈에 보이는 문제가 발생하면 클린업 함수에 일부 로직이 누락된 것입니다
  • 클린업 함수는 셋업 함수가 수행하던 작업을 중지하거나 취소해야 합니다.
  • 사용자 경험상 상용에서 셋업이 한 번 호출되는 것과, 개발 환경에서 셋업 → 클린업 → 셋업 순서로 호출되는 것을 구분할 수 없어야 합니다.

모든 Effect를 독립적인 프로세스로 작성하고 한 번에 하나의 셋업/클린업 주기만 생각하세요. 필요한 만큼 자주 셋업과 클린업을 실행하더라도 Effect는 정상적으로 작동합니다.

Note

  • Effect를 사용하면 컴포넌트를 외부 시스템(예: 채팅 서비스)과 동기화를 유지할 수 있습니다. 여기서 외부 시스템이란 React로 제어되지 않는 코드 조각을 의미합니다.
    • setInterval() 및 clearInterval()로 관리되는 타이머.
    • window.addEventListener() 및 window.removeEventListener()를 사용하는 이벤트 구독.
    • animation.start() 및 animation.reset()과 같은 API가 있는 타사 애니메이션 라이브러리.

외부시스템에 연결하는 예시

  • 전역 브라우저 이벤트 수신하기
export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    function handleMove(e) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
    window.addEventListener('pointermove', handleMove);
    return () => {
      window.removeEventListener('pointermove', handleMove);
    };
  }, []);

  return (
    <div style={{
      position: 'absolute',
      backgroundColor: 'pink',
      borderRadius: '50%',
      opacity: 0.6,
      transform: `translate(${position.x}px, ${position.y}px)`,
      pointerEvents: 'none',
      left: -20,
      top: -20,
      width: 40,
      height: 40,
    }} />
  );
}
  • 이 예제에서 외부 시스템은 브라우저 DOM 자체입니다. 일반적으로 JSX로 이벤트 리스너를 지정하지만, 이런 방식으로는 전역 window 객체를 수신할 수 없습니다.
  • Effect를 사용하면 window 객체에 연결하여 해당 이벤트를 수신할 수 있습니다. pointermove 이벤트를 수신하면 커서(또는 손가락) 위치를 추적하고 빨간색 점이 함께 이동하도록 업데이트할 수 있습니다.

2. 커스텀 훅으로 Effect 감싸기

  • Effect를 수동으로 작성해야 하는 경우가 자주 발생한다면 이는 컴포넌트가 의존하는 일반적인 동작에 대한 커스텀 훅을 생성해야 합니다.
  • 위에서 봤던 ChatRoom 기능을 커스텀 훅으로 만들어 보겠습니다.
function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}


function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  // 사용
  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });
  // ...

Effect를 커스텀 훅으로 감싸는 예시

  • useWindowListener 훅
export function useWindowListener(eventType, listener) {
  useEffect(() => {
    window.addEventListener(eventType, listener);
    return () => {
      window.removeEventListener(eventType, listener);
    };
  }, [eventType, listener]);
}
  • useIntersectionObserver 훅
export function useIntersectionObserver(ref) {
  const [isIntersecting, setIsIntersecting] = useState(false);

  useEffect(() => {
    const div = ref.current;
    const observer = new IntersectionObserver(entries => {
      const entry = entries[0];
      setIsIntersecting(entry.isIntersecting);
    });
    observer.observe(div, {
      threshold: 1.0
    });
    return () => {
      observer.disconnect();
    }
  }, [ref]);

  return isIntersecting;
}

3. React가 아닌 위젯 제어하기

  • 외부 시스템을 컴포넌트의 특정 prop이나 state와 동기화하고 싶을 때가 있습니다.
  • 예를 들어, React 없이 작성된 타사 맵 위젯이나 비디오 플레이어 컴포넌트가 있는 경우, Effect를 사용하여 해당 state를 React 컴포넌트의 현재 state와 일치시키는 메서드를 호출할 수 있습니다.
  • 이 Effect는 map-widget.js에 정의된 MapWidget 클래스의 인스턴스를 생성합니다. Map 컴포넌트의 zoomLevel prop을 변경하면 Effect는 클래스 인스턴스에서 setZoom()을 호출하여 동기화 상태를 유지합니다.
    • 대표적으로는 Google Map 같은 api라고 생각됩니다. 저도 개인 프로젝트에서는 한 번 더 랩핑된 라이브러리를 사용하고 있었는데 이번에 useEffect를 활용하여 직접 구현해보려고 합니다.
export class MapWidget {
  constructor(domNode) {
    this.map = L.map(domNode, {
      zoomControl: false,
      doubleClickZoom: false,
      boxZoom: false,
      keyboard: false,
      scrollWheelZoom: false,
      zoomAnimation: false,
      touchZoom: false,
      zoomSnap: 0.1
    });
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19,
      attribution: '© OpenStreetMap'
    }).addTo(this.map);
    this.map.setView([0, 0], 0);
  }
  setZoom(level) {
    this.map.setZoom(level);
  }
}

export default function Map({ zoomLevel }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    if (mapRef.current === null) {
      mapRef.current = new MapWidget(containerRef.current);
    }

    const map = mapRef.current;
    map.setZoom(zoomLevel);
  }, [zoomLevel]);

  return (
    <div
      style={{ width: 200, height: 200 }}
      ref={containerRef}
    />
  );
}

4. Effect로 데이터 페칭하기

  • Effect를 사용하여 컴포넌트에 대한 데이터를 페치할 수 있습니다. 프레임워크를 사용하는 경우, 프레임워크의 데이터 페칭 메커니즘을 사용하는 것이 Effects를 수동으로 작성하는 것보다 훨씬 효율적입니다.
export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);

  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    };
  }, [person]);

  // ...
  • ignore 변수는 false로 초기화되고 클린업 중에 true로 설정됩니다. 이렇게 하면 네트워크 응답이 보낸 순서와 다른 순서로 도착하더라도 ’조건 경합’이 발생하지 않습니다

async / await 구문을 사용하여 다시 작성할 수도 있지만, 그렇더라도 여전히 클린업 함수는 제공해야 합니다.

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }

    let ignore = false;
    startFetching();
    return () => {
      ignore = true;
    }
  }, [person]);
  • Effects에서 직접 데이터를 페칭하는 작업을 반복적으로 작성하면 나중에 캐싱 및 서버 렌더링과 같은 최적화를 추가하기가 어려워집니다.
  • 직접 만들거나 커뮤니티에서 유지 관리하는 커스텀 훅을 사용하는 것이 더 쉽습니다.

(현재 개인 프로젝트에서는 Effect를 사용해 data를 가져오고 있습니다만 리팩토링을 하며 react-query를 사용할 예정입니다.)

Effect에서 데이터 페칭하는 것을 대체할 좋은 대안은 무엇인가요?

Effect는 상당한 단점들을 가지고 있습니다.

  • Effect는 서버에서 실행되지 않습니다.
    • 즉, 서버에서 렌더링되는 초기 HTML에는 데이터가 없는 로딩 state만 포함됩니다.
    • 클라이언트 컴퓨터는 모든 JavaScript를 다운로드하고 앱을 렌더링해야만 이제 데이터를 로드해야 한다는 것을 알 수 있습니다.
  • Effect에서 직접 패칭하면 "네트워크 워터폴"을 만들기 쉽습니다.
    • 부모 컴포넌트를 렌더링하면 일부 데이터를 페치하고, 자식 컴포넌트를 렌더링하면 자식 컴포넌트가 데이터를 페칭하기 시작합니다.
    • 네트워크가 매우 빠르지 않는 한 모든 데이터를 병렬로 페칭하는 것보다 훨씬 느립니다.
  • Effect에서 직접 패칭한다는 것은 일반적으로 데이터를 미리 로드하거나 캐시하지 않는다는 의미입니다.
    • 예를 들어, 컴포넌트가 마운트를 해제했다가 다시 마운트하면 데이터를 다시 가져와야 합니다.
  • 인체 공학적으로 좋지 않습니다.
    • 조건 경합과 같은 버그가 발생하지 않는 방식으로 fetch 호출을 작성하려면 상용구 코드가 상당히 많이 필요합니다.

다음과 같은 방식을 권장합니다.

  • framework를 사용하는 경우, 프레임워크 빌트인 데이터 페칭 메커니즘을 사용하세요.
    • 최신 React 프레임워크는 효율적이고 위의 함정이 발생하지 않는 통합 데이터 페칭 메커니즘을 갖추고 있습니다.
  • 그게 아니라면 clinet-side 캐시를 사용하거나 구축하는 것을 고려하세요.
    • 인기있는 오픈 소스 솔루션으로는 React Query, useSWR, React Router 6.4+ 등이 있습니다.

5. 반응형 의존성 지정

  • Effect의 의존성을 “선택”할 수 없다는 점에 유의하세요. Effect의 코드에서 사용되는 모든 반응형 값은 의존성으로 선언해야 합니다.
function ChatRoom({ roomId }) { // This is a reactive value
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
    connection.connect();
    return () => connection.disconnect();
  }, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
  // ...
}
  • serverUrl 또는 roomId가 변경되면 Effect는 새 값을 사용하여 채팅에 다시 연결합니다.
  • 반응형 값에는 props와 컴포넌트 내부에서 직접 선언된 모든 변수, 함수가 포함됩니다. roomId와 serverUrl은 반응형 값이기 때문에 의존성 목록에서 제거할 수 없습니다.
    • 만약 이 값을 생략하려고 할 때 린터가 React 용으로 올바르게 구성되어 있다면, 린터는 이를 수정해야 하는 실수로 표시해 줍니다.
  • 의존성을 제거하려면, 의존성이어야 할 필요가 없음을 린터에게 “증명”해야 합니다.
    • 예를 들어, serverUrl을 컴포넌트 밖으로 이동시킴으로써 반응형이 아니며 리렌더링시에도 변경되지 않음을 증명할 수 있습니다.
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...
}

반응형 의존성 전달 예시

  • 의존성 배열 전달하기
    • Effect는 초기 렌더링 후 및 변경된 의존성으로 다시 렌더링한 후에 실행됩니다.
  • 빈 의존성 배열 전달하기
    • Effect는 초기 렌더링 이후에만실행
  • 아예 의존성 배열을 전달하지 않기
    • Effect가 렌더링 및 리렌더링될 때마다 실행

6. Effect의 이전 state를 기반으로 state 업데이트하기

  • Effect의 이전 state를 기반으로 state를 업데이트하려는 경우 문제가 발생할 수 있습니다.
  • count는 반응형 값이므로 의존성 목록에 지정되어야 합니다. 다만 이로 인해 count가 변경될 때마다 Effect를 다시 클린업하고 셋업해줘야 합니다. 이는 이상적이지 않습니다.
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1); // You want to increment the counter every second...
    }, 1000)
    return () => clearInterval(intervalId);
  }, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
  // ...
}
  • 이 문제를 해결하려면 setCount에 c => c + 1 state 업데이터를 전달하세요.
export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ Pass a state updater
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ Now count is not a dependency

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

7. 불필요한 객체 의존성 제거하기

  • Effect가 렌더링 중에 생성된 객체 또는 함수에 의존하는 경우 필요 이상으로 자주 실행될 수 있습니다.
    • 예를 들어, options 객체는 각 렌더링마다 다른 값이므로, 이 Effect는 매 렌더링시에 다시 연결하게 됩니다.
const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const options = { // 🚩 This object is created from scratch on every re-render
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options); // It's used inside the Effect
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // 🚩 As a result, these dependencies are always different on a re-render
  // ...
  • 렌더링 중에 생성된 객체를 의존성으로 사용하지 마세요. 대신 Effect 내에서 객체를 생성하세요.
useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  • 이제 Effect 내부에 options 객체를 만들었으므로 Effect는 오직 roomId 문자열에만 의존하게 되었습니다
  • 이 수정 덕에 input에 타이핑해도 채팅이 다시 연결되지 않습니다.

8. 불필요한 함수 의존성 제거하기

  • Effect가 렌더링 중에 생성된 객체 또는 함수에 의존하는 경우 필요 이상으로 자주 실행될 수 있습니다.
    • 예를 들어, createOptions 함수가 렌더링할 때마다 다르기 때문에 이 Effect는 렌더링할 때마다 다시 연결됩니다.
function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  function createOptions() { // 🚩 This function is created from scratch on every re-render
    return {
      serverUrl: serverUrl,
      roomId: roomId
    };
  }

  useEffect(() => {
    const options = createOptions(); // It's used inside the Effect
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
  // ...
  • 이 함수를 Effect의 의존성으로 사용하면 Effect가 다시 렌더링할 때마다 다시 실행됩니다.
  • 렌더링 중에 생성된 함수를 의존성으로 사용하지 마세요. 대신 Effect 내에서 선언하세요.
function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    function createOptions() {
      return {
        serverUrl: serverUrl,
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

9. Effect에서 최신 props 및 state 읽기 (해당 기능은 아직 미지원 기능입니다.)

  • 기본적으로 Effect에서 반응형 값을 읽을 때엔 이를 의존성으로 추가해야 합니다.
  • 이렇게 하면 Effect가 해당 값의 모든 변경에 “반응”하도록 할 수 있습니다. 대부분의 의존성에서 원하는 동작입니다.
  • 그러나 때로는 Effect에 “반응”하지 않고도 Effect에서 최신 props와 state를 읽고 싶을 때가 있습니다.
    • 예를 들어, 페이지 방문 시마다 장바구니에 있는 품목의 수를 기록한다고 가정해 보겠습니다.
function Page({ url, shoppingCart }) {
  useEffect(() => {
    logVisit(url, shoppingCart.length);
  }, [url, shoppingCart]); // ✅ All dependencies declared
  // ...
}
  • url 이 변경될 때마다 새 페이지 방문을 기록하되 shoppingCart 만 변경되는 경우는 기록하지 않으려면 어떻게 해야 하나요?
  • 반응성 규칙을 위반하지 않으면서 shoppingCart를 의존성에서 제외할 수는 없습니다. 그러나 코드가 Effect 내부에서 호출되더라도 변경 사항에 “반응”하지 않도록 표현할 수 있습니다.
  • useEffectEvent 훅을 사용하여 Effect Event를 선언하고 shoppingCart를 읽는 코드를 그 안으로 이동시킵니다.
function Page({ url, shoppingCart }) {
  const onVisit = useEffectEvent(visitedUrl => {
    logVisit(visitedUrl, shoppingCart.length)
  });

  useEffect(() => {
    onVisit(url);
  }, [url]); // ✅ All dependencies declared
  // ...
}
  • Effect Event는 반응형이 아니므로 항상 Effect의 의존성에서 제외해야 합니다.
  • 이를 통해 반응형이 아닌 코드(일부 props 및 state의 최신 값을 읽을 수 있는 코드)를 그 안에 넣을 수 있습니다.
  • 예를 들어, onVisit내부에서 shoppingCart를 읽으면 shoppingCart가 Effect를 다시 실행하지 않도록 할 수 있습니다.

10. 서버와 클라이언트에 서로 다른 콘텐츠 표시하기

  • 서버 렌더링을 사용하는 앱의 경우, 컴포넌트는 두 가지 다른 환경에서 렌더링됩니다. 서버에서는 초기 HTML을 생성하기 위해 렌더링됩니다. 클라이언트에서 React는 렌더링 코드를 다시 실행하여 이벤트 핸들러를 해당 HTML에 첨부할 수 있도록합니다.
  • 그렇기 때문에 하이드레이션이 작동하라면 클라이언트와 서버의 첫 렌더링 결과가 동일해야 합니다.
  • 드물지만 클라이언트에 다른 콘텐츠를 표시해야 하는 경우가 있을 수 있습니다.
    • 예를 들어, 앱이 localStorage에서 일부 데이터를 읽는 경우 서버에서는 이를 수행할 수 없습니다. 일반적으로 이를 구현하는 방법은 다음과 같습니다
function MyComponent() {
  const [didMount, setDidMount] = useState(false);

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

  if (didMount) {
    // ... return client-only JSX ...
  }  else {
    // ... return initial JSX ...
  }
}
  • 앱이 로드되는 동안 사용자는 초기 렌더링 결과물을 봅니다. 그런 다음 앱이 로드 및 hydrated 되면 이제 Effect가 실행되면서 didMount를 true로 설정하여 리렌더링을 촉발합니다.
  • 이로 인해 클라이언트 전용 렌더링 결과물로 전환됩니다. Effect는 서버에서 실행되지 않기 때문에, 서버 렌더링 중에는 didMount는 false입니다.
  • 이 패턴은 되도록 사용을 아끼세요. 연결 속도가 느린 사용자는 초기 콘텐츠를 꽤 오랜 시간(수 초) 동안 보게 되므로, 컴포넌트의 모양이 갑작스럽게 변경되는걸 원하지 않을 것이라는 점에 유의하세요.
  • 대부분의 경우 CSS를 사용하여 조건부로 다른 것을 표시함으로써 이러한 필요성을 피할 수 있습니다. (하하하 사실상 쓸 일이 없다는 거죠?)

😀 문제 해결

1. 컴포넌트가 마운트될 때 내 Effect가 두 번 실행됩니다.

  • StrictMode가 켜져 있으면, 개발환경에서 React는 실제 셋업 전에 셋업 및 클린업을 한 번 더 실행합니다.
  • 이로 인해 눈에 보이는 문제가 발생하면 클린업 함수에 일부 로직이 누락된 것입니다.

2. 내 Effect는 리렌더링할 때마다 실행됩니다.

  • 먼저 의존성 배열을 지정하는 것을 잊어버린 건 아닌지 확인하세요!
  • 의존성 배열을 지정했는데도 Effect가 여전히 루프에서 다시 실행된다면, 리렌더링할 때마다 의존성 중 하나가 달라지기 때문일 것입니다.
  • 다시 렌더링할 때마다 다른 의존성을 발견하면 일반적으로 다음 방법 중 하나로 해결할 수 있습니다.
    • Effect의 이전 state를 기반으로 state 업데이트하기
    • 불필요한 객체 의존성 제거하기
    • 불필요한 함수 의존성 제거하기
    • Effect에서 최신 props 및 state 읽기
  • 위 방법들로도 해결되지 않는 경우, 최후의 수단으로 useMemo 혹은 함수의 경우 useCallback으로 감싸세요.

3. 내 Effect가 무한히 재실행됩니다.

  • Effect가 무한히 재실행되는 경우는 다음 두 가지가 모두 참임을 의미합니다
    • Effect가 일부 state를 업데이트하고 있습니다.
    • 이 state는 리렌더링을 일으키며, 이로부터 Effect의 의존성이 변경됩니다.
  • Effect가 외부 시스템에 연결되어 있는지 확인해 보세요.
    • 외부 시스템이 없는 경우 Effect를 완전히 제거하면 로직이 단순화되는지 고려하세요.
    • 외부 시스템과 실제로 동기화하는 경우 Effect가 state를 업데이트해야 하는 이유와 조건에 대해 생각해 보세요.
      • 컴포넌트의 시각적 결과물에 영향을 미치는 변경 사항이 있나요? 렌더링에 사용되지 않는 일부 데이터를 추적해야 하는 경우, 리렌더링을 촉발하지 않는 ref가 더 적합할 수 있습니다
  • 마지막으로, Effect가 적시에 state를 업데이트하고 있지만 여전히 루프가 발생한다면, 해당 state 업데이트로 인해 Effect의 의존성 중 하나가 변경되기 때문일 것입니다.

4. 컴포넌트가 마운트 해제되지 않았는데도 클린업 로직이 실행됩니다.

  • 클린업 기능은 마운트 해제시 뿐만 아니라 변경된 의존성과 함께 다시 렌더링하기 전에 매번 실행됩니다.
  • 또한 개발환경에서는 React는 컴포넌트 마운트 직후에 셋업+클린업을 한 번 더 실행합니다.
  • 클린업 코드는 있는데 그에 대응하는 셋업 코드는 없다면, 일반적으로 문제가 있는 코드입니다.
  • 클린업 로직은 셋업 로직과 “대칭”이어야 하며, 셋업이 수행한 모든 작업을 중지하거나 취소해야 합니다.

5. 내 Effect가 시각적인 작업을 수행하는데, 실행되기 전에 깜박거립니다.

  • Effect로 인해 브라우저가 화면을 그리는 것을 차단해야 하는 경우, useEffect를 useLayoutEffect로 바꾸세요.
  • 대부분의 Effect에는 이 기능이 필요하지 않다는 점에 유의하세요. 오직 브라우저 페인팅 전에 Effect를 실행하는 것이 중요한 경우에만 필요할 것입니다.
  • (예: 사용자가 보기 전에 툴팁의 위치를 미리 측정하고 배치하고자 할 때)
profile
흑우 모르는 흑우 없제~

0개의 댓글