본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -Removing Effect Dependencies(Effect 의존성 제거하기)

- Effect를 작성하면 린터는 Effect의 의존성 목록에 Effect가 읽는 모든 반응형(예: props 및 state)을 포함했는지 확인한다.

- 이때 불필요한 의존성으로 인해 Effect가 너무 자주 실행되거나 무한 루프를 생성할 수도 있다.


To remove a dependency, prov that it's not a dependency(의존성을 제거하려면 의존성이 아님을 증명하세요)

- Effect의 의존성을 "선택"할 수는 없다.

- Effect의 코드에서 사용되는 모든 반응형 값은 의존성 목록에 선언되어야 한다.

- 반응형 값에는 props와 컴포넌트 내부에서 직접 선언된 모든 변수 및 함수가 포함된다.

 

- 만약 의존성을 제거하려면 해당 의존성이 의존성 배열에 추가될 필요가 없다는 것을 린터에게 "증명"해야 한다.


To change the dependencies, change the code(의존성을 변경하려면 코드를 변경하세요)

- 의존성을 변경하려면 먼저 주변 코드를 변경해야 한다.

- 의존성 목록은 Effect의 코드에서 사용하는 모든 반응형 값의 목록이라고 생각하면 된다.


Is your Effect doing several unrelated things?(Effect가 여러 관련 없는 일을 하고 있나요?)

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);

  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    // 🔴 Avoid: A single Effect synchronizes two independent processes
    if (city) {
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
    }
    return () => {
      ignore = true;
      // 이 코드에서 ignore 변수와 그것을 변경하는 클린업 함수의 역할은 데이터를 비동기적으로
      // 처리하는 과정에서 발생할 수 있는 문제를 해결하기 위함이다.
      // ignore 변수는 이펙트(Effect)가 실행되고 있는 도중에 컴포넌트가 언마운트되는 경우를
      // 처리하기 위해 사용된다.
      // 이펙트 내에서 비동기 작업이 수행될 때, 비동기 작업이 완료되기 전에 컴포넌트가 언마운트되면
      // 해당 작업이 불필요하게 처리되는 것을 방지하기 위해 사용되는 것이다.
      // 클린업 함수(return 구문 내의 함수)에서 ignore 변수를 true로 변경함으로써,
      // 컴포넌트가 언마운트되거나 다시 렌더링되는 경우에는 더 이상 이펙트 내부의 코드 블록이 실행되지
      // 않도록 합니다. 이렇게 하면 비동기 작업이 진행 중일 때 컴포넌트가 언마운트되는 상황에서 작업이
      // 계속 진행되는 것을 방지할 수 있다.
      // 즉, ignore 변수와 클린업 함수를 사용하여 컴포넌트가 언마운트되는 동안 비동기 작업이 계속되지
      // 않도록 보장하여 불필요한 리소스 낭비를 방지하는 것이 목적이다.
    };
  }, [country, city]); // ✅ All dependencies declared

  // ...

- 위 예제는 사용자가 도시와 지역을 선택해야 하는 배송 form을 만드는 예제이다.

- 도시 지역에 대한 두 개의 셀렉트 박스가 있고, 현재 선택된 city의 areas를 페치하는 로직까지 포함되어 있는 예제인데, 위 코드에서는 city state 변수를 사용하므로 의존성 목록에 city가 추가되어 있는데, 사용자가 다른 도시를 선택하면 Effect가 다시 실행되어 fetchCities(country)를 호출하는 문제가 발생한다. 즉, 불필요하게 도시 목록을 여러번 다시 페치하게 된다.

 

- 위 코드의 문제점은 서로 관련이 없는 두 가지를 동기화하고 있다는 것이다.

1. country props를 기반으로 cities state를 네트워크에 동기화하려고 한다.

2. city state를 기반으로 areas state를 네트워크에 동기화하려고 한다.

 

- 위 코드의 문제를 해결하기 위해서는 두 개의 Effect로 분할하고, 각 Effect는 동기화해야 하는 props에 반응해야 한다.

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [country]); // ✅ All dependencies declared

  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);
  useEffect(() => {
    if (city) {
      let ignore = false;
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [city]); // ✅ All dependencies declared

  // ...

- 서로 관련 없는 두 가지의 Effect를 분리하게 되면 각 Effect는 독립적인 동기화 프로세스를 나타내야 한다. 즉, 하나의 Effect를 삭제하더라도 다른 Effect의 로직이 깨지지 않는다.


Are you reading some state to calculate the next state?(다음 state를 계산하기 위해 어떤 state를 읽고 있나요?)

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages([...messages, receivedMessage]);
    });
    return () => connection.disconnect();
  }, [roomId, messages]); // ✅ All dependencies declared
  // ...

- 위 예제에서는 Effect가 새 메시지가 도착할 때마다 새로 생성된 배열로 message state 변수를 업데이트 한다.

- 따라서, messages를 의존성으로 만들고 이전 message state와 새로운 message를 넣은 messages 배열을 새로 만들게 된다.

- 하지만, 위 예제의 경우 메시지를 수신할 때마다 setMessage()는 컴포넌트가 수신된 메시지를 포함하는 새 message 배열로 리렌더링하도록 하고, Effect는 messages에 따라 달라지므로 Effect도 다시 동기화된다. 즉, 새 메시지가 올 때마다 채팅이 다시 연결되는 문제가 발생한다.

- 이 문제를 해결하기 위해서는 Effect 내에서 message를 읽는 것이 아닌, 업데이터 함수를 setMessage에 전달해야 한다.

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]);
    });
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // .

- 이렇게되면 Effect가 messages 변수를 전혀 읽지 않고, 업데이터 함수만 전달하면 된다.

- React는 업데이터 함수를 대기열에 넣고 다음 렌더링 중에 msgs 인수를 제공하기 때문에, Effect 자체는 더 이상 messages에 의존할 필요가 없다.(???)


Do you want to read a value without "reacting" to its changes?(값의 변경에 "반응"하지 않고 값을 읽고 싶으신가요?)

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isMuted, setIsMuted] = useState(false);

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]);
      if (!isMuted) {
        playSound();
      }
    });
    return () => connection.disconnect();
  }, [roomId, isMuted]); // ✅ All dependencies declared
  // ...

- 위 예제는 새로운 메시지가 왔을 때, isMuted의 값에 따라 알림소리 여부를 결정하는 예제이다.

- 위 예제의 문제점은 새로운 메시지가 올때뿐만 아니라, isMuted가 변경될 때마다 Effect가 다시 동기화되고 채팅에 다시 연결된다는 점이다.

import { useState, useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [isMuted, setIsMuted] = useState(false);

  const onMessage = useEffectEvent(receivedMessage => {
    setMessages(msgs => [...msgs, receivedMessage]);
    if (!isMuted) {
      playSound();
    }
  });

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      onMessage(receivedMessage);
    });
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

- 따라서, 위와 같이 Effect에서 반응해서는 안되는 로직(isMuted 의존)을 추출하고 비반응 로직을 Effect Event로 옮기면 된다.


Wrapping an event handler from the props(props를 이벤트 핸들러로 감싸기)

- 자식 컴포넌트가 이벤트 핸들러를 props로 받을 때, 부모 컴포넌트가 렌더링 할때 마다 다른 함수를 전달하기 때문에 위 예제와 같이 비슷한 문제가 생길 수 있다.

- 즉, 부모가 리렌더링할 때마다 Effect가 다시 동기화 되고, 의도와 달리 채팅에 다시 연결된다.

function ChatRoom({ roomId, onReceiveMessage }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      onReceiveMessage(receivedMessage);
    });
    return () => connection.disconnect();
  }, [roomId, onReceiveMessage]); // ✅ All dependencies declared
  // ...
  
// 부모 컴포넌트에서 랜더링이 될 때마다 새로운 이벤트 핸들러를 전달함.  
  <ChatRoom
  roomId={roomId}
  onReceiveMessage={receivedMessage => {
    // ...
  }}
/>

- 위 문제 역시 이벤트 핸들러를 Effect Event를 사용해서 감싸주는 방법으로 해결할 수 있다.

function ChatRoom({ roomId, onReceiveMessage }) {
  const [messages, setMessages] = useState([]);

  const onMessage = useEffectEvent(receivedMessage => {
    onReceiveMessage(receivedMessage);
  });

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      onMessage(receivedMessage);
    });
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

- 즉, Effect Event는 반응하지 않기 때문에 의존성을 지정할 필요가 없고, 부모 컴포넌트가 리렌더링할 때마다 다른 함수를 전달하더라도 채팅이 더 이상 연결되지 않는다.


Dose some reactive value change unintentionally?(일부 반응형 값이 의도치 않게 변경되나요?)

- Effect가 특정 값에 "반응"하기를 원하지만, 그 값이 원하는 것보다 자주 변경되어 사용자의 관점에서 실제 변경 사항을 반영하지 못할 수도 있다.

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

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

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

  // Temporarily disable the linter to demonstrate the problem
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

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

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

- 위 예제에서, options 객체는 컴포넌트 본문에서 선언되므로 반응형 값이고, Effect에서 이와 같은 반응형 값을 읽으면 의존성으로 선언한다.

- 하지만, input에 값을 입력하는 경우 message state 변수가 업데이트 되고, 컴포넌트는 리렌더링 되는데, 이때 그 안에 있는 코드가 처음부터 다시 싫랭된다.

- 즉, ChatRoom 컴포넌트를 리렌더링할 때마다 새로운 options 객체가 처음부터 새로 생성되기 때문에, React는 options를 항상 새로운 객체로 판단하고 Effect를 다시 동기화하고 사용자가 입력할 때 채팅이 다시 연결되는 문제가 발생한다.

- 위와 같은 문제가 발생하는 이유는, JavaScript에서는 새로 생성된 객체와 함수가 다른 모든 객체와 구별되는 것으로 간주되기 때문이다.

- 즉, 객체 및 함수를 Effect의 의존성으로 사용하지 않는 것이 좋다. 대신 컴포넌트 외부나 Effect 내부로 이동하거나 원시 값을 추출하는 것이 좋다.


Move static objects and functions outside your component(정적 객체와 함수를 컴포넌트 외부로 이동)

- 객체가 props 및 state에 의존하지 않는 경우, 해당 객체를 컴포넌트 외부로 이동할 수 있다.

- 앞선 예제에서 발생하는 문제, 즉 컴포넌트가 리렌더링 될때마다 options 객체가 새로운 객체로 판단되어 Effect가 다시 동기화되는 것을 막기 위해서, options 객체를 컴포넌트 외부로 이동시키는 방법으로 해결할 수 있다.

const options = {
  serverUrl: 'https://localhost:1234',
  roomId: 'music'
};

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

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

Move dynamic objects and functions inside your Effect(Effect 내에서 동적 객체 및 함수 이동)

- 만약 객체 및 함수가 반응형 값에 의존하는 경우에는, 앞선 방법(컴포넌트 외부로 이동시키는 방법)을 사용할 수 없다.

- 대신, 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]); // ✅ All dependencies declared
  // ...

- 위 코드를 보면, options는 Effect 내부에서 선언되었기 때문에, 더 이상 Effect의 의존성이 아니다.

- 이 방법은 함수에서도 마찬가지인데, Effect 내에서 로직을 그룹화하기 위해 자신만의 함수를 작성할 수 있고, Effect 내부에서 선언하는 한 반응형 값이 아니므로 Effect의 의존성이 될 필요가 없다.

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

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

  useEffect(() => {
  // createOptions 함수는 Effect 내부에서 선언되었기 때문에 반응형이 아니다.
    function createOptions() {
      return {
        serverUrl: serverUrl,
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

Read primitive values from objects(객체에서 원시 값 읽기) / Calcultae primitive values from functions(함수에서 원시값 계산)

- props에서 객체를 받는 경우, 렌더링 중에 부모 컴포넌트가 객체를 생성하는데, 이는 곧 부모 컴포넌트가 리렌더링할 때마다 Effect가 다시 연결된다는 뜻이다.

- 따라서, props로 객체를 받더라도 Effect 외부의 객체에서 정보를 읽고 객체 및 함수 의존성을 피해야 한다.

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

  // Effect 외부에서 객체 정보를 읽기
  const { roomId, serverUrl } = options;
  useEffect(() => {
    const connection = createConnection({
      roomId: roomId,
      serverUrl: serverUrl
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]); // ✅ All dependencies declared
  // ...

 - 함수에 대해서도 동일한 접근 방식을 사용할 수 있다.

// 부모 컴포넌트에서 함수를 props로 전달
<ChatRoom
  roomId={roomId}
  getOptions={() => {
    return {
      serverUrl: serverUrl,
      roomId: roomId
    };
  }}
/>

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

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

- 하지만, 위 예제에서의 함수는 순수 함수에서만 작동하기 때문에, 대신 함수가 이벤트 핸들러이지만 변경 사항으로 인해 Effect가 다시 동기화되는 것을 원하지 않는 경우에는 Effect Event 함수로 감싸야 한다.


* 참고 : React 공식문서(https://react-ko.dev/learn)