[React] useEffect

Gyuhan Park·2024년 5월 19일
0

공식문서

목록 보기
10/10

📘 useEffect

외부 시스템과 컴포넌트를 동기화하는 React Hook

✅ setup

Effect의 로직이 포함된 함수
정리 함수를 반환할 수 있음 (optional)
컴포넌트가 DOM에 추가된 이후 에 setup 함수 실행
의존성의 변화에 따라 리렌더링 되었을 경우, 정리함수를 반환할 경우 이전 렌더링에 사용된 값으로 정리함수를 실행한 후, 새로운 값으로 setup 함수 실행
컴포넌트가 DOM에서 제거된 경우에도 정리 함수 실행

✅ dependencies (optional)

setup 코드 내에서 참조된 모든 반응형 값의 배열**
반응형 값 : props, state, 컴포넌트 내부에 선언된 모든 변수나 함수 등
lint는 모든 반응형 값들이 의존성에 제대로 명시되어 있는지 검증
각각의 의존성들을 [Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 로 이전 값과 비교 (얕은 복사)

의존성을 생략할 경우, Effect는 컴포넌트가 리렌더링될 때마다 실행

useEffect(setup, dependencies?)
  • 외부 시스템과 컴포넌트를 동기화할 필요가 없는 경우, Effect를 선언할 필요가 없을 수 있음
  • Strict Mode를 사용할 경우, React는 실제 첫 번째 설정 함수가 실행되기 이전에 개발 모드에만 한정하여 한 번의 추가적인 설정 + 정리 사이클을 실행
  • 의존성이 객체이거나 컴포넌트 내부에 선언된 함수일 경우, Effect가 필요 이상으로 재실행될 수 있으므로 의존성 제거 또는 추출
  • 브라우저가 업데이트된 화면을 그리도록 허용한 후 Effect를 실행.
    • Effect로 수행하는 작업이 시각적인 효과가 있고 지연이 눈에 띄게 발생한다면useEffect 대신 [useLayoutEffect](https://ko.react.dev/reference/react/useLayoutEffect) 사용 (예를 들어, 툴팁 배치 등 화면에 어떤 변화를 주는 경우)
  • 클릭과 같은 상호작용에 의해 일어나더라도 브라우저는 Effect 내부의 state가 업데이트되기 이전에 repaint. 브라우저가 화면을 리페인팅 하는 것을 블로킹하고 싶다면 useEffect 대신 [useLayoutEffect](https://ko.react.dev/reference/react/useLayoutEffect)를 사용

📘 사용 방법

✅ 외부 시스템과 연결

일부 컴포넌트들은 페이지에 표시되는 동안 네트워크나 브라우저 API 등과 연결이 유지되어야함
외부 시스템 : React에 의해 제어되지 않는 모든 코드

  • 네트워크, 브라우저 API, DOM, setTimeout, addEventListener 등
    컴포넌트를 외부 시스템과 연결하려면 컴포넌트의 최상위 레벨에서 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]);
  // ...
}

마운트 : 컴포넌트가 화면에 추가되었을 때 setup 함수 실행
리렌더링 : 의존성이 변경될 경우, 컴포넌트가 리렌더링 될 때마다 아래 동작 수행

  • 정리함수가 이전 props와 state로 실행
  • 설정함수가 새로운 props와 state로 실행
    언마운트 : 컴포넌트가 화면에서 제거된 이후에 정리 함수가 마지막으로 실행

✅ 커스텀 Hook을 Effect로 감싸기

Effect를 자주 작성해야 한다면 컴포넌트가 의존하고 있는 공통적인 동작들을 커스텀 Hook으로 추출해야 한다는 신호일 수 있음

커스텀 Hook은 Effect의 로직을 조금 더 선언적인 API로 보일 수 있도록 숨겨줌

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

  useEffect(() => {
  	const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]);
  // ...
}
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
  });
  // ...

✅ 리액트로 작성되지 않은 위젯 제어하기

컴포넌트의 prop 또는 state를 외부 시스템과 동기화해야할 때가 있음

  • React 없이 작성된 third-party 지도 위젯
  • 비디오 플레이어 컴포넌트

이 컴포넌트의 state를 현재 React 컴포넌트의 state와 일치하도록 하기 위해 Effect를 사용할 수 있음
이 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}
    />
  );
}

✅ Effect를 이용한 데이터 페칭

컴포넌트에 데이터를 페칭하기 위해 Effect를 사용할 수 있음
ignore 변수의 초기값을 false로 설정하고, 정리 함수에서 true로 설정하는 로직 → race condition에 빠지지 않도록 보장
⇒ 네트워크 요청을 보낸 순서와 응답을 반는 순서가 다르게 동작할 수 있으므로 이러한 처리가 필요

Effect에서 직접 데이터 페칭 로직을 작성하면 나중에 캐싱 기능이나 서버 렌더링과 같은 최적화를 추가하기 어려워짐.
custom Hook이나 라이브러리를 사용하는 편이 더 간단.

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에서 데이터를 페칭하는 좋은 대안은 무엇인가?

Effect 내부에서 fetch 호출을 작성하는 것은 클라이언트 사이드 앱에서 가장 인기 있는 방법
But, 매우 수동적인 접근 방식이며, 큰 단점 존재

  • 서버에서는 실행되지 않음
    • 모든 자바스크립트를 당누로드 받고 앱을 렌더링한 다음 데이터 로드
  • Effect 내부에서 직접 페칭하는 것은 network waterfall가 생성되기 쉬움
    • 부모 컴포넌트 렌더링 → 데이터 페칭 → 자식 컴포넌트 렌더링 …
    • 부모 컴포넌트의 데이터 페칭 속도가 느릴 경우 자식 컴포넌트도 느려짐
  • Effect 내부에서 직접 데이터를 페칭하는 것은 보통 데이터를 미리 로드하거나 캐싱하지 않는다는 것을 의미
  • race condition 같은 버그가 발생하지 않도록 주의해야되서 사용자가 불편함
  • 프레임워크에 내장된 데이터 페칭 메커니즘 활용
  • 클라이언트 측 캐시 사용
    • React Query
    • useSWR
    • React Router 6.4+
  • 이펙트를 내부적으로 사용하면서도 데이터 preload 또는 데이터 요구사항을 라우트로 호이스팅하는 방법을 통해 중복 요청 방지, 응답 캐싱 및 네트워크 폭포 효과 방지를 구현할 수 있음

✅ 반응형값 의존성 지정

💡 **Effect의 의존성을 선택할 수 없다**

Effect 코드에서 사용하는 모든 반응형 값은 의존성으로 선언되어야함
Effect의 의존성 배열은 코드에 의해 결정됨 (개발자 선택 ❌)
의존성을 제거하려면 그것이 의존성이 되지 않아야함을 린터에 증명
→ 컴포넌트 밖으로 이동하여 그것이 반응적이지 않고 리렌더링될 때 변경되지 않을 것임을 증명

// before
function ChatRoom({ roomId }) { // 이것은 반응형 값입니다
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // 이것도 반응형 값입니다

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // 이 Effect는 이 반응형 값들을 읽습니다
    connection.connect();
    return () => connection.disconnect();
  }, [serverUrl, roomId]); // ✅ 그래서 이 값들을 Effect의 의존성으로 지정해야 합니다
  // ...
}

// after
const serverUrl = 'https://localhost:1234'; // 더 이상 반응형 값이 아님

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ 모든 의존성이 선언됨
  // ...
}

의존성이 비어있는 Effect는 props나 state가 변경되도 다시 실행되지 않음

const serverUrl = 'https://localhost:1234'; // 더 이상 반응형 값이 아님
const roomId = 'music'; // 더 이상 반응형 값이 아님

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ 모든 의존성이 선언됨
  // ...
}

✅ Effect에서 이전 state를 기반으로 state 업데이트하기

count가 반응형 값이므로 반드시 의존성 배열에 추가해야함
count가 변경될 때마다 Effect가 정리된 후 다시 설정되는 것을 무한 반복

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1); // 초마다 카운터를 증가시키고 싶습니다...
    }, 1000)
    return () => clearInterval(intervalId);
  }, [count]); // 🚩 ... 하지만 'count'를 의존성으로 명시하면 항상 인터벌이 초기화됩니다.
  // ...
}

state 변경함수를 setCount에 추가

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ State 업데이터를 전달
    }, 1000);
    return () <=> clearInterval(intervalId);
  }, []); // ✅ 이제 count는 의존성이 아닙니다

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

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

렌더링 중에 생성된 객체에 의존하는 경우, 너무 자주 실행될 수 있음
매 렌더링 후에 다시 연결됨 (렌더링마다 options 객체가 다름)

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

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

  const options = { // 🚩 이 객체는 재 렌더링 될 때마다 새로 생성됩니다
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options); // 객체가 Effect 안에서 사용됩니다
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // 🚩 결과적으로, 의존성이 재 렌더링 때마다 다릅니다
  // ...

객체가 반응형 값에 의존 ❌ → 객체를 컴포넌트 외부로 이동
객체가 반응형 값에 의존 → 객체를 Effect 내에서 생성

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

	...
}

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

렌더링 중에 생성된 함수에 의존하는 경우, 너무 자주 실행될 수 있음

매 렌더링 후에 다시 연결됨 (렌더링마다 createOptions 함수가 다름)

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

  function createOptions() { // 🚩 이 함수는 재 렌더링 될 때마다 새로 생성됩니다
    return {
      serverUrl: serverUrl,
      roomId: roomId
    };
  }

  useEffect(() => {
    const options = createOptions(); // 함수가 Effect 안에서 사용됩니다
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🚩 결과적으로, 의존성이 재 렌더링 때마다 다릅니다
  // ...

리렌더링마다 함수를 처음부터 생성하는 것 자체로는 문제가 되지 않지만, Effect의 의존성으로 사용하는 경우 리렌더링 후마다 Effect가 다시 실행됨
→ Effect 내에서 함수 선언

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

📘 트러블 슈팅

✅ Effect가 컴포넌트 마운트 시 2번 동작합니다.

개발 환경에서 Strict Mode가 활성화되면 React는 실제 설정 이전에 설정과 정리를 한번 더 실행함 → Effect 로직이 올바르게 구현되었는지 확인하는 스트레스 테스트

사용자가 설정이 한번 호출되는 것(배포 환경과 같이)과 설정 → 정리 → 설정 순서로 호출되는 것을 구별할 수 없어야 한다는 것

✅ Effect가 매 리렌더링마다 실행됩니다.

의존성 배열을 명시했음에도 Effect가 반복해서 실행된다면 의존성이 렌더링마다 다르기 때문
렌더링마다 다른 의존성을 찾아냈다면 다음 중 하나의 방법으로 수정할 수 있음

  • Effect에서 이전 state를 기반으로 state 업데이트하기
  • 불필요한 객체 의존성 제거하기
  • 불필요한 함수 의존성 제거하기
  • Effect에서 최신 props와 state를 읽기
  • useMemo, useCallback (최후의 수단)

✅ Effect가 무한 반복됩니다.

Effect가 무한 반복되려면 다음 두 가지 조건이 충족되어야 합니다..

  • Effect에서 state를 업데이트함.
  • 변경된 state가 리렌더링을 유발하며, 이에 따라 Effect의 종속성이 변경됨.

문제를 해결하기 전에 Effect가 외부 시스템에 연결되어 있는지 자문하기

  • Effect에서 왜 state를 변경했는가?
  • 변경된 state가 외부 시스템과 동기화됐는가?
  • Effect를 통해 애플리케이션의 데이터 흐름을 관리하려고 하는가?

외부 시스템이 없다 → Effect를 제거하는 방법 고려

외부 시스템과 동기화 중이다 → Effect가 state를 언제 어떤 조건에서 업데이트해야 하는지에 대해 고려

  • 컴포넌트의 시각적 출력에 영향을 주는 state가 변하는가?
    • 렌더링에 사용되지 않는 데이터를 추적해야 한다면 ref가 더 적합할 수 있음
  • Effect가 필요 이상으로 state를 업데이트하는지 확인
  • 해당 state의 업데이트가 Effect의 종속성의 변경을 야기했을 수 있음

✅ 컴포넌트가 마운트 해제되지 않았음에도 정리 함수가 실행됩니다.

정리 함수는 마운트 해제 시 뿐만 아니라 변경된 종속성으로 인한 모든 리렌더링 전에 실행
개발 환경에서는 컴포넌트가 마운트된 직후에 한 번 더 설정과 정리 실행
정리 로직은 설정 로직과 ‘대칭’이어야 하며 설정이 수행한 것을 중지하거나 되돌릴 수 있어야함

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

✅ Effect가 시각적인 작업을 수행하며, 실행되기 전에 깜빡임이 보입니다.

Effect가 브라우저가 화면을 그리는 것을 차단해야 하는 경우 useEffect[useLayoutEffect](https://ko.react.dev/reference/react/useLayoutEffect)로 대체

대부분의 Effect에는 필요하지 않음
브라우저 페인팅 이전에 Effect를 실행하는 것이 중요한 경우에만 필요

profile
단단한 프론트엔드 개발자가 되고 싶은

0개의 댓글