useEffect

홍준섭·2023년 2월 26일

react

목록 보기
5/29

useEffect

useEffect(setup,dependencies?)

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]);
  // ...
}
  • setup: Effect의 논리가 포함된 함수.
  • dependencies: 코드 내부에서 참조되는 모든 반응 값 목록.

주의사항

  • useEffect는 구성 요소 또는 자체 Hooks의 최상위 수준에서만 호출할 수 있다.
  • 일부 외부 시스템과 동기화하지 않으려는 경우 아마도 Effect가 필요하지 않다
  • Strict mode가 켜져있으면 React는 첫 번째 실제 설정 전에 하나의 추가 개발 전용 설정 + 정리 주기를 실행한다.
  • 종속성 중 일부가 구성 요소 내부에 정의된 개체 또는 함수인 경우 필요 이상으로 Effect가 자주 다시 실행될 위험이 있다.
  • Effect가 상호작용에 의해 발생하지 않은 경우 React는 브라우저가 Effect를 실행하기 전에 업데이트된 화면을 먼저 칠하도록 한다.
  • Effect가 상호 작용에 의해 발생한 경우에도 브라우저는 Effect 내에서 상태 업데이트를 처리하기 전에 화면을 다시 칠할 수 있다.
  • 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]);
  // ...
}

custom Hooks 에서 Effects 래핑

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

non-React widget 제어

non-React widget의 경우 Effect를 사용하여 state를 React 구성 요소의 현재 상태와 일치시키는 메서드를 호출 할 수 있다.

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

  // ...

Effect 내에서 fetch로 데이터를 가져오는 것은 클라이언트 측 앱에서 인기 있는 방법이다. 그러나 이것은 매우 수동적인 접근 방식이며 상당한 단점이 있다.

  • Effect는 서버에서 실행되지 않는다. 즉 초기 서버 렌더링 HTML에는 데이터가 없는 로드 상태만 포함된다. 클라이언트 컴퓨터는 이제 데이터를 로드해야 한다는 사실을 발견하기 위해서만 모든 JavaScript를 다운로드하고 앱을 렌더링 해야한다.
  • Effect에서 직접 가져오면 network waterfalls을 쉽게 만들 수 있다. 상위 구성 요소를 렌더링하면 일부 데이터를 가져오고 하위 구성 요소를 렌더링한 다음 데이터를 가져오기 시작한다. 네트워크가 매우 빠르지 않으면 모든 데이터를 병렬로 가져오는 것보다 훨씬 느리다
  • Effects에서 직접 가져오는 것은 일반적으로 데이터를 미리 로드하거나 캐시하지 않는다는 것을 의미한다. 예를 들어 구성요소가 마운트 해제되었다가 다시 마운트되면 데이터를 다시 가져와야한다.

이 단점들은 React에만 국한되지 않는다. 모든 라이브러리를 사용하여 마운트 시 데이터를 가져오는 데 적용된다. 라우팅과 마찬가지로 데이터 가져오기가 쉽지 않으므로 다음과 같은 접근 방식을 권장한다

  • 프레임 워크를 사용하는 경우 내장된 데이터 가져오기 매커니즘을 사용해라.
  • 그렇지 않으면 클라이언트측 캐시를 사용하거나 구축하는 것이 좋다. 널리 사용되는 오픈 소스 솔루션에는 React Query, useSWR 및 React Router 6.4+가 포함된다.

반응적 종속성 지정

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 구성 요소 밖으로 이동하여 반응하지 않고 다시 렌더링해도 변경되지 않음을 증명할 수 있다.

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의 이전 상태를 기반으로 상태 업데이트

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를 종속성 목록에 지정해야 한다. 그러나 이로 인해 변경될 때마다 Effect가 정리되고 다시 설정된다. 이 문제를 해결하기 위해 setState를 다음과 같이 전달한다 prev => prev + 1

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); 
    }, 1000)
    return () => clearInterval(intervalId);
  }, []);
  // ...
}

불필요한 객체 종속성 제거

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

그렇기에 렌더링 중에 생성된 개체를 종속성으로 사용하지 말고 Effec 내부에 객체를 만든다.

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

export default function App() {
 const [roomId, setRoomId] = useState('general');
 return (
   <>
     <label>
       Choose the chat room:{' '}
       <select
         value={roomId}
         onChange={e => setRoomId(e.target.value)}
       >
         <option value="general">general</option>
         <option value="travel">travel</option>
         <option value="music">music</option>
       </select>
     </label>
     <hr />
     <ChatRoom roomId={roomId} />
   </>
 );
}

불필요한 함수 종속성 제거

렌더링 중에 생성된 함수를 종속성으로 사용하지 말고 대신 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]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

서버와 클라이언트에 다른 콘텐츠 표시

앱에서 서버 렌더링을 사용하는 경우 구성 요소가 서로 다른 두 환경에서 렌더링된다. 서버에서 렌더링하여 초기 HTML을 생성하고 클라이언트에서 React는 렌더링 코드를 다시 실행하여 해당 HTML에 이벤트 핸들러를 연결할 수 있다. 이것이 잘 작동하려면 초기 렌더 출력이 클라이언트와 서버에서 동일해야 한다.
그러나 클라이언트에 다른 콘텐츠를 표시해야 하는 경우가 있을 수 있다. 그런 경우 이와 같이 구현한다

function MyComponent() {
  const [didMount, setDidMount] = useState(false);

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

  if (didMount) {
    // ... return client-only JSX ...
  }  else {
    // ... return initial JSX ...
  }
}

trouble shooting

구성 요소가 마운트 되면 Effect가 두 번 실행 된다.

Strict Mode가 켜져 있으면 React는 실제 설정 전에 한 번 더 설정 및 정리를 실행한다. 이는 Effect의 로직이 올바르게 구현되었는지 확인하는 스트레스 테스트이다.

다시 렌더링할 때마다 effect가 실행된다

  1. 종속성 배열을 지정하는 것을 잊지 않았는지 확인한다
  2. 종속 항목을 콘솔에 수동으로 기록하여 이 문제를 디버깅한다
  3. 콘솔에서 다른 리렌더링의 배열을 마우스 오른쪽 버튼으로 클릭하고 둘 다에 대해 "전역 변수로 저장"을 선택할 수 있다. 첫 번째는 temp1 두번째는 temp2로 가정하면 브라우저 콘솔을 사용하여 두 배열의 각 종속성이 동일한지 확인 할 수 있다.
  4. 다시 렌더링할 때마다 다른 종속성을 찾으면 일반적으로 다음 방법 중 하나로 수정할 수 있다.
  • Effect의 이전 상태를 기반으로 상태 업데이트
  • 불필요한 개체 종속성 제거
  • 불필요한 함수 종속성 제거
  • Effect에서 최신 props및 state 읽기
  • 최후의 수단으로 생성을 useMemo 또는 useCallback으로 감싼다

Effect가 무한 주기로 다시 실행된다

Effect가 무한 주기로 실행되는 경우 다음 두 가지가 참이어야 한다.

  • Effect가 일부 state를 업데이트 하고 있다
  • 그 state는 재렌더링으로 이어져 Effect의 종속성이 변경된다.

내 구성 요소가 마운트 해제되지 않은 경우에도 내 정리 논리가 실행된다

정리 기능은 마운트 해제 중뿐만 아니라 종속성이 변경된 모든 재렌더링 전에 실행된다.

profile
개발 공부중입니다

0개의 댓글