리액트 레퍼런스 Hooks - useEffect

기운찬곰·2023년 10월 8일
post-thumbnail

참고 : https://react.dev/reference/react/useEffect

useEffect Reference

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

useEffect(setup, dependencies?)

컴포넌트의 top level에 있는 useEffect를 호출하여 Effect를 정의할 수 있습니다:

import { useEffect } from 'react';
import { createConnection } from './chat.js';

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

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);
  // ...
}

Parameters

  • setup : Effect의 로직이 적용된 함수입니다. setup 함수은 선택적으로 cleanup 함수를 반환할 수도 있습니다. 컴포넌트가 DOM에 추가되면 React가 setup 함수를 실행합니다. 변경된 종속성(dependencies)을 사용하여 리렌더링할 때마다 React는 먼저 이전 값을 사용하여 cleanup 함수(제공한 경우)를 실행한 다음 새 값을 사용하여 setup 함수를 실행합니다. DOM에서 컴포넌트를 제거한 후 React가 cleanup 함수를 실행합니다.
  • (optional) dependencies : setup 코드 내부에서 참조되는 모든 반응 값의 목록입니다. 반응형 값에는 props, state 및 컴포넌트 내부에 직접 선언된 모든 변수와 함수가 포함됩니다. linter가 리액트 용으로 설정된 경우, 모든 반응 값이 종속성으로 올바르게 지정되었는지 확인합니다. 종속성 목록은 항목 수가 일정해야 하며 [dep1, dep2, dep3] 처럼 인라인으로 작성되어야 합니다. React는 Object.is 비교를 사용하여 각 종속성을 이전 값과 비교합니다. 종석성 배열을 전달하는 것, 빈 배열을 전달하는 것, 종속성이 전혀 없는 것의 차이를 아래에서 확인해보세요.

Returns

useEffect는 undefined를 리턴합니다.

Caveats

  • useEffect는 Hook이므로 컴포넌트의 top level 혹은 자체 Hooks에서만 호출할 수 있습니다. 루프나 조건문 내에서는 호출할 수 없습니다. 필요한 경우 새 컴포넌트를 추출하고 상태를 해당 컴포넌트로 옮깁니다.
  • 일부 외부 시스템과 동기화를 하지 않는 경우 Effect가 필요하지 않을 수 있습니다.
  • Strict 모드가 켜져있으면 React는 첫번째 실제 setup 전에 개발 전용 setup + cleanup 사이클을 한번 더 실행합니다. 이는 cleanup 로직이 setup 로직을 "미러링" 하고 setup이 수행하는 모든 작업을 중지하거나 실행 취소하는지 확인하는 스트레스 테스트입니다. 이로 인해 문제가 발생하면 cleanup 함수를 구현하십시오.
  • 종속성 중 일부가 컴포넌트 내부에 정의된 객체 또는 함수인 경우 Effect가 필요한 것보다 더 자주 다시 실행될 위험이 있습니다. 이 문제를 해결하려면 불필요한 객체 또는 함수 종속성을 제거하세요. Effect 외부에서 상태 업데이트와 비반응형 로직을 추출할 수도 있습니다.
  • Effect가 상호작용(예: 클릭)으로 인해 발생하지 않은 경우 React는 일반적으로 Effect를 실행하기 전에 브라우저가 업데이트된 화면을 먼저 그리도록 합니다. Effect가 시작적인 작업(예: tooltip 위치 지정)을 수행하고 지연이 눈에 띄는 경우(예: 깜박임) useEffect를 useLayoutEffect로 바꾸세요.
  • Effect가 상호 작용(예: 클릭)으로 인해 발생한 경우에도 브라우저는 Effect 내부의 상태 업데이트를 처리하기 전에 화면을 다시 칠할 수 있습니다. 일반적으로 그것이 당신이 원하는 것입니다. 그러나 브라우저가 화면을 다시 그리는 것을 차단해야 하는 경우 useEffect를 useLayoutEffect로 바꿔야 합니다.
  • Effects는 오직 클라이언트에서만 실행됩니다. 서버 렌더링 중에는 실행되지 않습니다.

Usage

Connecting to an external system

일부 컴포넌트는 페이지에 표시되는 동안 네트워크, 일부 브라우저 API 또는 타사 라이브러리에 계속 연결되어 있어야 합니다. 이러한 시스템은 React에 의해 제어되지 않으므로 외부 시스템이라고 합니다.

일부 외부 시스템에 컴포넌트를 연결하려면 컴포넌트의 최상위 수준에서 useEffect를 호출합니다:

import { useEffect } from 'react';
import { createConnection } from './chat.js';

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

  useEffect(() => {
  	const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]);
  // ...
}

Effect를 사용하려면 두 개의 arguments를 통과해야 합니다:

  1. 해당 시스템에 연결되는 setup 코드가 있는 setup 함수입니다. 해당 시스템과의 연결을 끊는 cleanup 코드가 포함된 cleanup 함수를 반환해야 합니다.
  2. 해당 함수 내부에 사용된 컴포넌트의 모든 값을 포함하는 dependencies 목록입니다.

리액트는 필요할 때마다 setup 및 cleanup 함수를 호출하며, 여러 번 발생할 수 있습니다:

  1. 컴포넌트가 페이지에 추가되면 setup 코드가 실행됩니다. (mounts)
  2. dependencies가 변경된 컴포넌트를 다시 렌더링할 때마다 다음을 수행합니다:
    • 먼저, cleanup code가 이전 props과 state로 실행됩니다.
    • 그런 다음, setup code가 새로운 props과 state로 실행됩니다.
  3. 컴포넌트를 페이지에서 제거한 후 마지막으로 cleanup code가 실행됩니다. (unmounts)

위의 예를 위해 이 순서를 설명해 보자.

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

버그를 찾는 데 도움을 주기 위해 개발 단계에서 React는 setup 전에 한 번 더 setup 및 cleanup를 실행합니다. 이는 Effect의 로직이 올바르게 구현되었는지 확인하는 스트레스 테스트입니다. 이로 인해 눈에 띄는 문제가 발생하면 cleanup 함수에 일부 논리가 누락된 것입니다. cleanup 기능은 setup 기능이 수행하던 모든 작업을 중지하거나 실행 취소해야 합니다. 경험상 사용자는 한 번 호출되는 설정(프로덕션에서와 같이)과 setup → cleanup → setup 순서(개발에서와 같이)를 구별할 수 없어야 합니다. 일반적인 솔루션을 확인하세요.

모든 Effect를 독립적인 프로세스로 작성하고 한 번에 단일 setup/cleanup 주기를 생각해 보십시오. 구성 요소가 마운트, 업데이트 또는 마운트 해제 중인지는 중요하지 않습니다. cleanup 로직이 setup 로직을 올바르게 "미러링"하면 Effect는 필요한 만큼 자주 setup 및 cleanup를 실행하는 데 탄력성을 갖습니다.

Wrapping Effects in custom Hooks

Effects는 "탈출구(Escape Hatches)"입니다. "React 외부로 나가야" 할 때나 사용 사례에 더 나은 내장 솔루션이 없을 때 효과를 사용합니다. Effect를 수동으로 작성해야 하는 경우가 많다면 이는 일반적으로 컴포넌트가 의존하는 일반적인 동작에 대한 custom Hooks을 추출해야 한다는 신호입니다.

예를 들어, useChatRoom custom Hook을 사용하면 효과의 논리를 보다 선언적인 API 뒤에 "숨깁니다:

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
  });
  // ...

리액트 생태계에서 사용 가능한 모든 용도의 훌륭한 custom Hooks도 많이 있습니다. custom Hooks에서 Effects 랩핑에 대해 자세히 알아봅니다. ("Reusing Logic with Custom Hooks")

Controlling a non-React widget

때로는 외부 시스템을 컴포넌트의 특정 상태에 동기화하려는 경우도 있습니다.

예를 들어 React 없이 작성된 타사 지도 위젯이나 비디오 플레이어 구성 요소가 있는 경우 Effect를 사용하여 해당 상태를 React 구성 요소의 현재 상태와 일치시키는 메서드를 호출할 수 있습니다. 이 Effect는 map-widget.js에 정의된 MapWidget 클래스의 인스턴스를 생성합니다. Map 컴포넌트의 zoomLevel prop을 변경하면 Effect는 클래스 인스턴스에서 setZoom()을 호출하여 동기화를 유지합니다.

import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';

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}
    />
  );
}

이 예에서는 MapWidget 클래스가 전달된 DOM 노드만 관리하므로 cleanup 함수가 필요하지 않습니다. Map React 구성 요소가 트리에서 제거된 후 DOM 노드와 MapWidget 클래스 인스턴스는 모두 브라우저 JavaScript 엔진에 의해 자동으로 가비지 수집됩니다.

Fetching data with Effects

Effect를 사용하여 컴포넌트에 대한 데이터를 가져올 수 있습니다. 프레임워크(Next.js, Remix 등)를 사용하는 경우 프레임워크의 데이터 가져오기 메커니즘을 사용하는 것이 Effect를 수동으로 작성하는 것보다 훨씬 더 효율적이라는 점에 유의하세요.

Effect에서 데이터를 수동으로 가져오려는 경우 코드는 다음과 같습니다.

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

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]);

  // ...

false로 초기화되고 cleanup 중에 true로 설정되는 ignore 변수에 유의하십시오. 이렇게 하면 코드에 "race conditions"이 발생하지 않습니다. "race conditions"는 네트워크 응답은 보낸 순서와 다른 순서로 도착할 수 있습니다.

async / await 구문을 사용하여 다시 쓸 수도 있지만 cleanup 함수를 제공해야 합니다:

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

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에서 데이터 가져오기를 직접 작성하는 것은 반복적이며 나중에 캐싱 및 서버 렌더링과 같은 최적화를 추가하기 어렵게 만듭니다. 자체적으로 사용하거나 커뮤니티에서 관리하는 custom Hook을 사용하는 것이 더 쉽습니다.


Q. Effects에서 데이터 가져오기를 대체할 수 있는 좋은 대안은 무엇입니까? (상당히 좋은 내용이네요. 👏👏👏)

Effects 내에서 가져오기 호출을 작성하는 것은 특히 완전한 클라이언트 측 앱에서 데이터를 가져오는 데 널리 사용되는 방법입니다. 그러나 이는 매우 수동적인 접근 방식이며 상당한 단점이 있습니다.

  • Effects는 서버에서 실행되지 않습니다. 즉, 초기 서버 렌더링 HTML에는 데이터가 없는 로드 상태만 포함됩니다. 클라이언트 컴퓨터는 모든 JavaScript를 다운로드하고 앱을 렌더링해야 이제 데이터를 로드해야 함을 발견할 수 있습니다. 이는 별로 효율적이지 않습니다.
  • Effects에서 직접 가져오면 "network waterfalls"를 쉽게 만들 수 있습니다. 상위 구성 요소를 렌더링하면 일부 데이터를 가져오고 하위 구성 요소를 렌더링한 다음 데이터 가져오기를 시작합니다. 네트워크 속도가 매우 빠르지 않으면 모든 데이터를 병렬로 가져오는 것보다 훨씬 느립니다.
  • Effects에서 직접 가져오는 것은 일반적으로 데이터를 미리 로드하거나 캐시하지 않는다는 의미입니다. 예를 들어 구성 요소가 마운트 해제된 후 다시 마운트되면 데이터를 다시 가져와야 합니다.
  • 그다지 인체공학적(ergonomic)이지는 않습니다. 경쟁 조건과 같은 버그가 발생하지 않는 방식으로 가져오기 호출을 작성할 때 관련된 상용구 코드가 꽤 많이 있습니다.

이 단점 목록은 React에만 국한된 것이 아닙니다. 모든 라이브러리를 사용하여 마운트할 때 데이터를 가져오는 데 적용됩니다. 라우팅과 마찬가지로 데이터 가져오기도 잘 수행하기가 쉽지 않으므로 다음 접근 방식을 권장합니다.

  • 프레임워크를 사용하는 경우 내장된 데이터 가져오기 메커니즘을 사용하세요. 최신 React 프레임워크(Next.js, Remix 등)에는 효율적이고 위의 함정을 겪지 않는 통합 데이터 가져오기 메커니즘이 있습니다.
  • 그렇지 않으면 클라이언트 측 캐시를 사용하거나 구축하는 것을 고려하십시오. 인기 있는 오픈 소스 솔루션으로는 React Query, useSWR 및 React Router 6.4+가 있습니다. 자체 솔루션을 구축할 수도 있습니다. 이 경우 내부적으로 Effects를 사용할 뿐만 아니라 요청 중복 제거, 응답 캐싱 및 네트워크 폭포 방지(데이터를 미리 로드하거나 데이터 요구 사항을 경로에 끌어올리는 방식)를 위한 논리도 추가합니다.

Specifying reactive dependencies

Effect의 종속성을 "선택"할 수 없다는 점에 유의하세요. 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와 컴포넌트 내부에 직접 선언된 모든 변수 및 함수가 포함됩니다. roomIdserverUrl은 반응형 값이므로 종속성에서 제거할 수 없습니다. 이를 생략하려고 시도하고 linter가 React에 대해 올바르게 구성되었다면, linter는 이를 수정해야 할 실수로 표시합니다.

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
  // ...
}

종속성을 제거하려면 종속성일 필요가 없다는 것을 Linter에 "증명"해야 합니다. 예를 들어, 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
  // ...
}

이제 serverUrl은 반응형 값이 아니므로(다시 렌더링할 때 변경할 수 없음) 종속성일 필요가 없습니다. Effect의 코드가 반응 값을 사용하지 않는 경우 해당 종속성 목록은 비어 있어야 합니다([]).

const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore

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

빈 종속성이 있는 Effect는 컴포넌트의 props이나 state가 변경될 때 다시 실행되지 않습니다.

Updating state based on previous state from an Effect

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.
  // ...
}

count는 반응형 값이므로 종속성 목록에 지정되어야 합니다. 그러나 이로 인해 count가 변경될 때마다 Effect가 cleanup 되고 다시 setup 됩니다. 이는 이상적이지 않습니다. (아니.. 뭐 count가 종속성 목록에 있어도 동작은 한다. 근데 이상적이진 않다는 거지)

이 문제를 해결하려면 c => c + 1 상태 업데이트를 setCount에 전달하세요.

import { useState, useEffect } from 'react';

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>;
}

이제 count + 1 대신 c => c + 1을 전달하므로 Effect는 더 이상 count에 의존할 필요가 없습니다. 이 수정으로 인해 count가 변경될 때마다 interval을 다시 정리하고 설정할 필요가 없습니다.

Removing unnecessary object or function dependencies

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 내부에 개체를 만듭니다.

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

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

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  
  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

이제 Effect 내부에 options 객체를 만들었으므로 Effect 자체는 roomId 문자열에만 의존합니다.

이번 수정으로 인해 input 내용을 입력해도 채팅이 다시 연결되지 않습니다. 다시 생성되는 객체와 달리 roomId와 같은 문자열은 다른 값으로 설정하지 않는 한 변경되지 않습니다. 종속성 제거에 대해 자세히 알아보세요.


이번에는 불필요한 함수에 대해 종속성이 설정된 경우를 살펴보겠습니다. 예를 들어 이 Effect는 렌더링마다 createOptions 함수가 다르기 때문에 렌더링할 때마다 다시 연결됩니다.

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 내부에 선언하세요.

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

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]);

Reading the latest props and state from an Effect

❗️ 본 절에서는 아직 안정적인 버전의 React로 공개되지 않은 실험용 API에 대해 설명합니다.

기본적으로 Effect에서 반응 값을 읽을 때 이를 종속성으로 추가해야 합니다. 이렇게 하면 Effect가 해당 값의 모든 변경에 "반응"하게 됩니다. 대부분의 종속성에서는 이것이 원하는 동작입니다.

그러나 때로는 "반응"하지 않고 Effect의 최신 props 및 상태를 읽고 싶을 수도 있습니다. 예를 들어, 페이지를 방문할 때마다 장바구니에 담긴 항목 수를 기록한다고 가정해 보겠습니다.

function Page({ url, shoppingCart }) {
  useEffect(() => {
    logVisit(url, shoppingCart.length);
  }, [url, shoppingCart]); // ✅ All dependencies declared
  // ...
}

url이 변경될 때마다 새 페이지 방문을 기록하고 싶지만 shoppingCart만 변경된 경우는 기록하지 않으려면 어떻게 해야 합니까? 반응성 규칙을 위반하지 않고는 shoppingCart를 종속성에서 제외할 수 없습니다. 그러나 Effect 내부에서 호출되더라도 코드 조각이 변경 사항에 "반응"하는 것을 원하지 않는다고 표현할 수 있습니다. useEffectEvent Hook를 사용하여 Effect Event를 선언하고 그 안에 shoppingCart를 읽는 코드를 이동합니다.

function Page({ url, shoppingCart }) {
  const onVisit = useEffectEvent(visitedUrl => {
    logVisit(visitedUrl, shoppingCart.length)
  });

  useEffect(() => {
    onVisit(url);
  }, [url]); // ✅ All dependencies declared
  // ...
}

Effect Events는 반응적이지 않으며 항상 Effect의 종속성에서 생략되어야 합니다. 이것이 비반응형 코드(일부 props 및 상태의 최신 값을 읽을 수 있는 코드)를 그 안에 넣을 수 있게 해주는 것입니다. onVisit 내부에서 shoppingCart를 읽으면 shoppingCart가 Effect를 다시 실행하지 않도록 할 수 있습니다.

Displaying different content on the server and the client

앱이 서버 렌더링을 사용하는 경우(직접 또는 프레임워크를 통해) 컴포넌트는 두 가지 다른 환경에서 렌더링됩니다. 서버에서는 초기 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를 사용하여 조건부로 다양한 항목을 표시하면 이러한 필요성을 피할 수 있습니다.


Troubleshooting

My Effect runs twice when the component mounts

Strict Mode가 켜져 있으면 개발 중 React는 실제 setup 전에 setup 및 cleanup를 한 번 더 실행합니다.

이는 Effect의 로직이 올바르게 구현되었는지 확인하는 스트레스 테스트입니다. 이로 인해 눈에 띄는 문제가 발생하면 cleanup 함수에 일부 논리가 누락된 것입니다. cleanup 함수는 setup 함수가 수행하던 모든 작업을 중지하거나 실행 취소해야 합니다. 경험상 사용자는 한 번 호출되는 설정(프로덕션에서와 같이)과 설정 → 정리 → 설정 순서(개발에서와 같이)를 구별할 수 없어야 합니다.

My Effect runs after every re-render

먼저 종속성 배열을 지정하는 것을 잊지 않았는지 확인하세요.

useEffect(() => {
  // ...
}); // 🚩 No dependency array: re-runs after every render!

종속성 배열을 지정했지만 Effect가 여전히 루프에서 다시 실행된다면 이는 다시 렌더링할 때마다 종속성 중 하나가 다르기 때문입니다.

콘솔에 종속성을 수동으로 기록하여 이 문제를 디버깅할 수 있습니다.

  useEffect(() => {
    // ..
  }, [serverUrl, roomId]);

  console.log([serverUrl, roomId]);

그런 다음 콘솔에서 다른 리렌더링의 배열을 마우스 오른쪽 버튼으로 클릭하고 두 항목 모두에 대해 "전역 변수로 저장"을 선택할 수 있습니다. 첫 번째 항목이 temp1로 저장되고 두 번째 항목이 temp2로 저장되었다고 가정하면 브라우저 콘솔을 사용하여 두 배열의 각 종속성이 동일한지 확인할 수 있습니다.

Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

모든 재렌더마다 다른 종속성을 발견하면 대개 다음 방법 중 하나로 수정할 수 있습니다:

  • Updating state based on previous state from an Effect
  • Removing unnecessary object dependencies
  • Removing unnecessary function dependencies
  • Reading the latest props and state from an Effect

마지막 수단으로 (이러한 방법이 도움이 되지 않을 경우) useMemo 또는 useCallback(함수의 경우)으로 랩핑합니다.

My Effect keeps re-running in an infinite cycle

Effect가 무한 순환으로 실행되는 경우 다음 두 가지 사항이 참이어야 합니다:

  • Effect가 일부 상태를 업데이트하고 있습니다.
  • 이 상태는 리렌더링으로 이어지며, 이로 인해 Effect의 종속성이 변경됩니다.

문제 해결을 시작하기 전에 Effect가 일부 외부 시스템(예: DOM, 네트워크, 타사 위젯 등)에 연결되어 있는지 자문해 보세요. Effect에서 상태를 설정해야 하는 이유는 무엇입니까? 해당 외부 시스템과 동기화됩니까? 아니면 이를 통해 애플리케이션의 데이터 흐름을 관리하려고 하시나요?

외부 시스템이 없는 경우 Effect를 완전히 제거하면 논리가 단순화되는지 고려하십시오.

일부 외부 시스템과 실제로 동기화하는 경우 Effect가 상태를 업데이트해야 하는 이유와 조건에 대해 생각해 보세요. 컴포넌트의 시각적 출력에 영향을 미치는 변경 사항이 있습니까? 렌더링에 사용되지 않는 일부 데이터를 추적해야 하는 경우 참조(다시 렌더링을 트리거하지 않음)가 더 적합할 수 있습니다. Effect가 필요 이상으로 상태를 업데이트하고 다시 렌더링을 트리거하지 않는지 확인하세요.

마지막으로, Effect가 적시에 상태를 업데이트하지만 여전히 루프가 있는 경우 해당 상태 업데이트로 인해 Effect의 종속성 중 하나가 변경되기 때문입니다. 종속성 변경 사항을 디버그하는 방법을 읽어보세요.

My cleanup logic runs even though my component didn’t unmount

cleanup 함수는 마운트 해제 중뿐만 아니라 종속성이 변경된 모든 다시 렌더링 전에 실행됩니다. 또한 개발 중에 React는 컴포넌트가 마운트된 직후에 setup + cleanup을 한 번 더 실행합니다.

해당 setup 코드가 없는 cleanup 코드가 있는 경우 일반적으로 code smell 입니다.

useEffect(() => {
  // 🔴 Avoid: Cleanup logic without corresponding setup logic
  return () => {
    doSomething();
  };
}, []);

정리 로직은 설정 로직과 "대칭"이어야 하며, 설정이 수행한 작업을 중지하거나 실행 취소해야 합니다:

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);

My Effect does something visual, and I see a flicker before it runs

Effect가 브라우저가 화면을 그리는 것을 차단해야 하는 경우 useEffect를 useLayoutEffect로 바꾸세요. 대부분의 Effect에는 이것이 필요하지 않습니다. 이는 브라우저를 그리기 전에 Effect를 실행하는 것이 중요한 경우에만 필요합니다. 예를 들어 사용자가 보기 전에 도구 설명을 측정하고 위치를 지정하는 경우입니다.


마치면서

오... 솔직히 useEffect는 꽤나 많이 사용해서 기대하지 않았는데 알게 모르게 도움이 많이 되는 글이네요.

profile
부계정

0개의 댓글