sohyeon kim

[React] 리액트 공식문서 정리 4(4) : 탈출구 - useEffect 의존성 배열에서 불필요한 요소 제거하기 본문

React

[React] 리액트 공식문서 정리 4(4) : 탈출구 - useEffect 의존성 배열에서 불필요한 요소 제거하기

aotoyae 2024. 12. 6. 14:44
728x90

 

 

💡 리액트 공식문서 정리 : 탈출구 - Effect 의존성 제거하기

effect 를 작성하면 린터는 effect 의 의존성 배열에 effect 가 읽는 모든 반응형 값(props 나 state 등)을 포함했는지 확인한다.

린터에 따라 반응형 값을 의존성 배열에 포함하면 effect 가 컴포넌트의 최신 props 및 state 와 동기화 상태를 유지할 수 잇게 된다.

하지만 불필요한 의존성으로 인해 effect 가 너무 자주 실행되거나 무한 루프를 생성할 때도 있다.

effect 에서 불필요한 의존성을 검토하고 제거하는 방법에 대해 알아보자.

 

1️⃣ 의존성은 항상 코드와 일치해야 한다.

effect 를 작성할 땐 먼저 effect 가 수행하기를 원하는 작업을 시작하고 중지하는 방법을 지정한다.

그 다음 effect 의존성 배열을 비워두면([]) 린터가 올바른 의존성을 제안한다.

린터에 표시된 내용에 따라 의존성 배열을 채우자.

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

Effect 는 반응형 값에 '반응'한다.

  • roomId 는 반응형 값이므로(리렌더링으로 인해 변경될 수 있음) 린터는 이를 의존성으로 지정했는지 확인한다.
  • roomId 가 다른 값을 받으면 React 는 Effect 를 다시 동기화한다.
  • 그럼 채팅이 선택된 방에 연결된 상태를 유지하고 드롭다운에 '반응'한다.

🚨 린터를 억제한다면, 매우 혼란스러운 버그가 발생하므로 피해야 한다.

 

2️⃣ 의존성을 제거하려면 의존성이 아님을(린터에게 해당 의존성이 필요하지 않음을) 증명하라. 

effect 의 의존성은 '선택'할 수 없다.

effect 의 코드에서 사용되는 모든 반응형 값은 의존성 배열에 선언되어야 하며, 이 배열은 주변 코드에 의해 결정된다.

 

위 코드에서 의존성을 제거하려면
예시로, roomId 를 컴포넌트 밖으로 이동시켜 반응형 값이 아니며, 리렌더링 시에도 변경되지 않음을 증명할 수 있다.

const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore

function ChatRoom() {
  useEffect(() => {
    // ...
  }, []); // ✅ All dependencies declared
  // ...
}

 

3️⃣ 의존성을 변경하려면 코드를 변경하라.

  1. 먼저 effect 의 코드 or 반응형 값 선언 방식을 변경한다.
  2. 그 다음, 변경한 코드에 맞게 의존성을 조정한다.
  3. 의존성 배열이 마음에 들지 않으면 첫 단계로 돌아간다.(그리고 코드를 다시 수정한다.)

마지막 부분이 중요하다. 의존성을 변경하려면 먼저 주변 코드를 변경하라.

의존성 배열은 effect 의 코드에서 사용하는 모든 반응형 값의 목록이라고 생각하면 된다. 이 배열에 무엇을 넣을 지는 사용자가 선택하지 않는다. 의존성 제거란 목표를 설정햇다면, 그 목표에 맞는 코드를 '찾아야' 한다.

 

4️⃣ 불필요한 의존성 제거하기

의존성 배열에 불필요한 값이 포함된 경우와 해결책을 찾기 위한 질문들을 살펴보자.

  • 다른 조건에서 effect 의 다른 부분을 다시 실행하고 싶은 경우
  • 일부 의존성의 변경에 '반응'하지 않고 '최신' 값만 읽고 싶은 경우
  • (의존성은 객체나 함수이기 때문에) 의도치 않게 너무 자주 변경되는 경우

이 코드를 이벤트 핸들러로 옮겨야 하나?

가장 먼저 고려해야 할 것은 이 코드가 effect 가 되어야 하는지 여부이다.

특정 상호작용에 대한 응답으로 코드를 실행하려면 해당 로직은 해당 이벤트 핸들러에 넣어야 한다.

 

Effect 가 서로 관련이 없는 여러 가지 작업을 수행하나?

사용자가 도시와 지역을 선택해야 하는 배송 폼을 만든다고 가정해 보자. 선택한 country 에 따라 서버에서 cities 목록을 가져와 드롭다운에 표시한다. form 이 표시되는 즉시 그리고 country 가 변경될 때마다 데이터를 가져와야 하므로 이는 effect 에서 실행된다. 또한 effect 가 city state 변수를 사용하므로 의존성 배열에 city 를 추가한다.

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;
    };
  }, [country, city]); // ✅ All dependencies declared

  // ...

 

의존성 배열에 두 값이 포함되어 사용자가 다른 도시를 선택하면 effect 가 재실행되어 fetchCities(country) 를 호출하는 문제가 발생했다. 불필요하게 도시 목록을 여러 번 가져오고 있다.

 

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

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

로직을 두 개의 effect 로 분리하고, 각 effect 는 동기화해야 하는 props 에 반응해야 한다.

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  useEffect(() => {
     // ...
    };
  }, [country]); // ✅ All dependencies declared

  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);
  useEffect(() => {
    if (city) {
       // ...
    }
  }, [city]); // ✅ All dependencies declared

  // ...

 

이제 첫 번째 effect 는 country 가 변경될 때만, 두 번째는 cityr 가 변경될 때만 다시 실행된다. 목적에 따라 분리했으니, 서로 다른 두 가지가 두 개의 개별 effect 에 의해 동기화된다. 각각의 effect 에는 각각의 의존성 배열이 있으므로 의도치 않게 서로를 트리거하지 않는다.

 

최종 코드는 이전 코드보다 길지만 effect 를 분리하는 것이 명확한 코드이다. 각 effect 는 독립적인 동기화 프로세스를 나타내야 한다.

이 예시에선 한 effect 를 삭제해도 다른 effect 의 로직이 깨지지 않는다. 즉, 서로 다른 것을 동기화하므로 분할하는 것이 좋다.

중복이 걱정된다면 반복되는 로직을 커스텀 훅으로 추출해 코드를 개선할 수 있다. 🔗

 

다음 state 를 계산하기 위해 어떤 state 를 읽고 있나?

아래 effect 는 새 메시지가 도착할 때마다 새로 생성된 배열로 messages state 를 업데이트한다. messages 변수를 사용해 기존 메시지로 시작하는 새 배열을 생성하고 마지막에 추가한다. message 는 effect 에서 읽는 반응형 값이므로 의존성이어야 한다.

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

 

이 때 문제가 발생한다. 메시지를 수신할 때마다 setMessages() 는 컴포넌트가 수신된 메시지를 포함하는 새 messages 배열로 리렌더링하도록 한다. 하지만 이 effect 는 이제 messages 에 따라 달라지므로 effect 도 다시 동기화된다. 따라서 새 메시지가 올 때마다 채팅이 다시 연결된다. 이를 해결하려면 effect 내에서 messages 를 읽지 말고, 업데이터 함수를 setMessages 에 전달하자.

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

 

이제 effect 는 message 변수에 전혀 의존하지 않고, React 는 업데이터 함수를 대기열에 넣고 다음 렌더링 중 msgs 인수를 제공한다.

이 덕분에 메시지를 수신해도 더 이상 채팅이 다시 연결되지 않는다.

 

값의 변경에 '반응'하지 않고 값을 읽고 싶은가?

 useEffectEvent API 활용(개발 중) 🔗

 

일부 반응형 값이 의도치 않게 변경되나?

effect 가 특정 값에 '반응'하기를 원하지만, 그 값이 생각보다 더 자주 변경되어 사용자 관점에서 실제 변경 사항을 반영하지 못할 수도 있다. 예시로, 컴포넌트에 options 객체를 생성하고, effect 내에서 해당 객체를 읽는다고 가정해 보자.

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

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // ✅ All dependencies declared
  
  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

 

이 객체는 컴포넌트 내에서 선언되므로 반응형 값이다. effect 내에서 이 값을 읽으면 의존성으로 선언해야 하고, effect 는 그 값에 '반응'하게 된다. 그럼 roomId 가 변경된다면 effect 가 새 options 로 채팅에 다시 연결한다.

하지만 문제가 발생한다. 위 코드에서 input 입력은 message state 만 업데이트 한다. 사용자 관점에서 이는 채팅 연결과 아무 상관이 없다. 하지만 message 를 업데이트할 때마다 컴포넌트가 리렌더링되고 그 안에 있는 코드가 처음부터 다시 실행된다.

 

ChatRoom 컴포넌트를 리렌더링할 때마다 새 options 객체가 생성된다. React 는 options 가 마지막 렌더링 중 생성된 options 와 다른 객체임을 인식한다. 그러므로 (options 에 따라 달라지는) effect 를 다시 동기화하고 사용자가 입력할 때 채팅이 다시 연결된다.

 

이 문제는 개체와 함수에만 영향을 준다. JS 에서는 새로 생성된 객체와 함수가 다른 모든 객체와 구별되는 것으로 간주된다.

그 안의 내용이 동일할 수 있다는 것은 아무 상관이 없다.

// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// These are two different objects!
console.log(Object.is(options1, options2)); // false

 

객체 및 함수 의존성으로 인해 effect 가 필요 이상으로 자주 재동기화될 수 있다.

그렇기에 가능하면 객체와 함수를 effect 의 의존성으로 사용하지 않는 것이 좋다. 대신 컴포넌트 외부나 effect 내로 이동하거나 원시 값을 추출해 보자.

 

정적 객체와 함수를 컴포넌트 외부로 이동

객체가 props 및 state 에 의존하지 않는 경우 해당 객체를 컴포넌트 외부로 이동할 수 있다. 그럼 린터에게 반응형 값이 아닌 것을 알려주므로 의존성이 될 필요가 없다. 이제 ChatRoom 을 재렌더링해도 effect 가 다시 동기화되지 않는다.

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

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

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

 

이는 함수에도 적용된다.

function createOptions() {
  return {
    serverUrl: 'https://localhost:1234',
    roomId: 'music'
  };
}

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

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

 

Effect 내에서 동적 객체 및 함수 이동

객체가 roomId props 처럼 리렌더링의 결과로 변경될 수 있는 반응형 값에 의존한다면, 컴포넌트 외부로 끌어낼 수 없다.

하지만 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 에서 사용하는 반응형 값은 roomId 이다. 이는 객체나 함수가 아니기 때문에 의도치 않게 달라지지 않을 것이라 확신할 수 있다. JS 에서 숫자와 문자열은 그 내용에 따라 구분된다.

// During the first render
const roomId1 = 'music';

// During the next render
const roomId2 = 'music';

// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true

 

위 수정 덕분에 입력을 수정해도 더 이상 채팅은 다시 연결되지 않으며, roomId 변경 시에만 재연결된다.

 

객체에서 원시 값 읽기

가끔 props 에서 객체를 받을 수도 있다. 이는 렌더링 중 부모 컴포넌트가 객체를 생성한다는 점에서 위험하다.

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

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

 

이렇게 하면 부모가 리렌더링될 때마다 effect 가 다시 연결된다. 이를 해결하기 위해 effect 외부의 객체에서 정보를 읽고 객체 및 함수의존성을 피하자.

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

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

 

이 로직은 약간 반복적이다. effect 외부 객체에서 일부 값을 읽은 다음 effect 내부에 동일 값을 가진 객체를 만들고 있다.

하지만 effect 가 실제로 어떤 정보에 의존하는지 명확하게 알 수 있다. 부모에 의해 의도치 않게 객체가 다시 생성된 경우 채팅이 다시 연결되지 않으며, options.roomId 또는 options.serverUrl 이 실제로 다른 경우 채팅이 재연결된다.

 

함수에서 원시값 계산

함수에 대해서도 동일한 접근 방식을 사용할 수 있다. 부모 컴포넌트가 함수를 전달한다고 가정해 보자.

<ChatRoom
  roomId={roomId}
  getOptions={() => {
    return {
      serverUrl: serverUrl,
      roomId: roomId
    };
  }}
/>

 

의존성을 만들지 않으려면(리렌더링 시 재연결을 방지하려면) effect 외부에서 호출하자.

이렇게 하면 객체가 아니며 effect 내부에서 읽을 수 있는 roomId 및 serverUrl 값을 얻을 수 있다.

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 이벤트로 함수를 감싸자. 🔗

 

 

 

🔗 https://ko.react.dev/learn/removing-effect-dependencies

 

Effect 의존성 제거하기 – React

The library for web and native user interfaces

ko.react.dev

 

 

 

728x90
반응형