개발일지

useEffect 본문

리액트 공식문서 스터디

useEffect

박수미/ 2024. 6. 4. 23:50

📌 useEffect란?

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

useEffect(setup, dependencies?)

📌 useEffect

컴포넌트의 최상위 레벨에서 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]);
  // ...
}

 

📍 매개변수

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

📍 반환값

useEffect는 undefined를 반환합니다.

 

📍 주의사항

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

📌  사용방법

1️⃣ 외부 시스템에 연결하기

때로는 컴포넌트가 페이지에 표시되는 동안 네트워크, 일부 브라우저 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]);
  // ...
}

 

1. 해당 시스템에 연결하는 셋업 코드가 포함된 셋업 함수

- 해당 시스템과의 연결을 끊는 클린업 코드가 포함된 클린업 함수를 반환해야 합니다.

2. 해당 함수 내부에서 사용되는 컴포넌트의 모든 값을 포함한 의존성 목록.

 

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

1. 컴포넌트가 페이지에 추가될 때(마운트)마다 셋업 코드를 실행합니다.

2. 의존성이 변경된 컴포넌트를 다시 렌더링 할 때마다:

- 먼저 이전 props와 state로 클린업 코드를 실행합니다.

- 그런 다음 새 props와 state로 셋업 코드를 실행합니다.

3. 컴포넌트가 페이지에서 제거되면(마운트 해제) 마지막으로 한번 클리업 코드를 실행합니다.

 

2️⃣ 커스텀 훅으로 Effect 감싸기

Effect는 "탈출구"입니다. React를 벗어나야 할 때, 또는 더 나은 빌트인 솔루션이 없을 때 사용합니다.

Effect를 수동으로 작성해야 하는 경우가 자주 발생한다면 이는 컴포넌트가 의존하는 일반적인 동작에 대한 커스텀 훅을 추출해야 한다는 신호일 수 있습니다.

 

예를 들어, 이 useChatRoom커스텀 훅은 Effect의 로직을 보다 선언적인 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
  });
  // ...

 

3️⃣ React가 아닌 위젯 제어하기

외부 시스템을 컴포넌트의 특정 prop이나 state와 동기화를 하고 싶을 때가 있습니다.

예를 들어, React 없이 작성된 타사 맵 위젯이나 비디오 플레이어 컴포넌트가 있는 경우, Effect를 사용하여 해당 state를 React 컴포넌트의 현재 state와 일치시키는 메서드를 호출할 수 있습니다.

 

4️⃣ Effect로 데이터 페칭하기

Effect를 사용하거나 컴포넌트에 대한 데이터를 패치할 수 있습니다. 프레임워크를 사용하는 경우, 프레임워크의 데이터 페칭 메커니즘을 사용하는 것이 Effects를 수동으로 작성하는 것보다 훨씬 효율적이라는 점에 유의하세요.

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

  // ...

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

Effects에서 직접 데이터를 페칭 하는 작업을 반복적으로 작성하면 나중에 캐싱 및 서버 렌더링과 같은 최적화를 추가하기가 어려워집니다.

 

5️⃣ 반응형 의존성 지정

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와 컴포넌트 내부에서 직접 선언된 모든 변수, 함수가 포함됩니다. roomId와 serverUrl은 반응형 값이기 때문에 의존성 목록에서 제거할 수 없습니다. 만약 이 값을 생략하려고 할 때 린터가 React용으로 올바르게 구성되어 있다면, 린터는 이를 수정해야 하는 실수로 표시해 줍니다:

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

의존성을 제거하려면, 의존성이여야 할 필요가 없음을 린터에게 "증명" 해야 합니다. 예를 들어, 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가 변경되어도 다시 실행되지 않습니다.

 

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

Effect의 이전 state를 기반으로 state를 업데이트하려는 경우 문제가 발생할 수 있습니다:

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를 다시 클린업하고 셋업 해줘야 합니다. 이는 이상적이지 않습니다.

이 문제를 해결하려면 setCount에 c => c + 1 state 업데이터를 전달하세요:

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을 다시 클린업하고 셋업 할 필요가 없습니다.

 

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 내에서 객체를 생성하세요:

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

이제 Effect 내부에 options 객체를 만들었으므로 Effect는 오직 roomId 문자열에마 ㄴ의존하게 되었습니다.

이 수정 덕에 input에 타이핑을 해도 채팅이 다시 연결되지 않습니다. 다시 만들어지는 객체와 달리 roomId와 같은 문자열은 다른 값으로 설정하지 않는 한 변경되지 않습니다.

 

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 내에서 선언하세요:

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

이제 Effect 내에서 createOptions 함수를 정의하면 Effect 자체는 roomId 문자열에만 의존합니다. 이 수정으로 input에 타이핑해도 채팅이 다시 연결되지 않습니다. 새로 생성되는 함수와 달리 roomId와 같은 문자열은 다른 값으로 설정하지 않는 한 변경되지 않습니다.

 

'리액트 공식문서 스터디' 카테고리의 다른 글

useEffect, useLayoutEffect 발표 자료  (1) 2024.06.13
useLayoutEffect  (0) 2024.06.11
2회차 발표  (0) 2024.05.31
useRef  (0) 2024.05.30
useContext  (0) 2024.05.28