useEffect 정리

현수·2023년 6월 4일
0

React Hooks

목록 보기
3/4
post-thumbnail

역할

컴포넌트가 마운트, 업데이트, 언마운트 되었을때 특정 로직을 실행할 수 있게한다.

사용법

useEffect(setup, dependencies?)
  • setup: 콜백함수로 실행될 로직이다. 선택적으로 return을 통해 cleanup 기능을 사용할 수 있다. 컴포넌트가 DOM에 추가 될때(마운트) 한번 실행하고 종속성에 값이 있으면 렌더링 과정에서 해당 종속 값이 변화할때마다 로직을 실행한다. 컴포넌트가 DOM에서 제거된 후(언마운트)에는 cleanup 기능이 동작한다.
  • optional dependencies: setup 로직 내부에서 사용하고있는 변수 목록이고 이는 state, props 그리고 컴포넌트 내부에서 선언된 변수와 함수가 해당한다. 여기에 속한 값의 변경이 감지되면 setup 로직을 실행한다.

사용예

외부 시스템에 연결

일부 컴포넌트는 페이지에 표시되는 동안 네트워크, 일부 브라우저 API 또는 타사 라이브러리에 연결되어야한다. 이러한 시스템은 React에서 제어되지 않아 외부 시스템이라고한다.

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

위의 코드는 서버와 연결 및 해제 기능을 하며 useEffect의 콜백함수에서 return 하는 함수는 컴포넌트가 언마운트 될때 로직을 정리하는 정리코드이다. 동작과정은 다음과 같다.

  1. 컴포넌트가 마운트될때 첫번째 인자 콜백함수가 실행된다.
  2. 종속성이 변경될때마다:
  • 먼저 이전 컴포넌트의 상태의 정리코드가 실행된다.
  • 이후 새로운 컴포넌트에서 콜백함수의 로직이 실행된다.
  1. 컴포넌트가 언마운트될 때 마지막으로 정리코드가 실행된다.

Custum Hooks의 래핑 효과

useEffect 를 커스텀 Hooks 방법을 통해 분리하고 재사용할 수 있다.

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

React가 아닌 위젯 제어

타사 맵 위젯이나 React 없이 구현된 비디오 플레이어 컴포넌트가 있는 경우 useEffect를 통해 state 를 React의 컴포넌트와 연결 시킬 수 있다.

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

위의 코드에서 useEffectuseRef 를 통해 접근한 내부 컴포넌트를 가지고 MapWidget 클래스의 인스턴스를 생성했다. 이제 zoomLevel state 가 변경되면 인스턴스의 setZoom() 이 호출되어 위젯이 동작한다.

이 예시는 cleanup 이 필요없는데 MapWidget 은 오직 컴포넌트의 DOM 요소에만 종속되어있기 때문에 Map 컴포넌트가 언마운트된다면 MapWidget 의 인스턴스는 JS엔진의 가비지 컬렉터에 의해 자동적으로 정리가 되기 때문이다.

데이터 가져오기

컴포넌트에 대한 데이터를 가져오기 위해 useEffect를 사용할 수 있다.

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

  // ...

종속성 지정

useEffect 내부에서 사용하는 모든 reactive 값은 종속성으로 선언해야한다. reactive 값은 props와 컴포넌트 내부에서 선언된 변수와 함수를 의미한다.

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

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // useEffect에서 reactive 값을 사용
    connection.connect();
    return () => connection.disconnect();
  }, [serverUrl, roomId]); // ✅ useEffect에 종속된 변수
  // ...
}

reactive가 아닌 값을 사용할 경우 종속을 제거해야한다.

const serverUrl = 'https://localhost:1234'; // reactive 값이 아님
const roomId = 'music'; // reactive 값이 아님

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ 모든 종속성 정리
  // ...
}

useEffect 내부에서 setState 사용

useEffect 내부에서 setState를 사용할 경우 state을 종속성에 등록해야한다. 그러나 이경우 state가 변경될때마다 cleanup이 동작하므로 이상적인 방법이 아니다.

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

대신 setState 내부에서 콜백 함수를 사용하여 종속성을 해결할 수 있다.

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ 상태 업데이트 전달
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ 이제 count는 종속되지 않는다

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

불필요한 객체, 함수 종속성 제거

컴포넌트 내부에서 생성한 객체 또는 함수에 종속되는 경우 useEffect가 자주 실행될 수 있다. 컴포넌트가 렌더링 될때마다 객체는 새로운 참조값을 가지기 때문이다.

컴포넌트 내부에서 useEffect 내부로 객체 생성을 옮김으로써 해결할 수 있다.

// 컴포넌트 내부에서 생성된 객체
const serverUrl = 'https://localhost:1234';

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

  const options = { // 🚩 렌더링 될때 마다 새로운 참조값
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options); // It's used inside the Effect
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // 🚩 렌더링할 때마다 매번 재실행
  // ...
// useEffect 내부에서 생성된 객체
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]);
}
// 컴포넌트 내부에서 생성된 함수 객체
function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  function createOptions() { // 🚩 렌더링 될때 마다 새로운 참조값
    return {
      serverUrl: serverUrl,
      roomId: roomId
    };
  }

  useEffect(() => {
    const options = createOptions(); // useEffect 내부에서 사용
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🚩 렌더링할 때마다 매번 재실행
  // ...
// useEffect 내부에서 생성된 함수 객체
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]);
}

서버와 다른 콘텐츠 표시

클라이언트에서 렌더링 구성에 시간이 걸릴 경우 서버에서 보내준 미리 완성된 렌더링 페이지를 초반에 보여주고 클라이언트의 렌더링 페이지가 제작이 완료되었을 때 페이지를 교체하는 방법을 useEffect 를 이용해 구현할 수 있다.

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

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

  if (didMount) {
    // ... 클라이언트의 JSX 리턴 ...
  }  else {
    // ... 초기 JSX 리턴 ...
  }
}

레퍼런스

리액트 공식 문서 - useEffect

0개의 댓글