React 32. Removing Effect Dependencies

뚜루미·2024년 4월 29일

React

목록 보기
32/39
post-thumbnail

Effect를 작성하면 린터는 Effect의 의존성 목록에 Effect가 읽는 모든 반응형 값을 포함했는지 확인합니다. 이렇게 하면 Effect가 컴포넌트의 최신 props 및 state와 동기화 상태를 유지할 수 있습니다. 불필요한 의존성으로 인해 Effect가 너무 자주 실행되거나 무한 루프를 생성할 수도 있습니다. 이 가이드에 따라 Effect에서 불필요한 의존성을 검토하고 제거하세요.

Dependencies should match the code

Effect를 작성할 때는 먼저 Effect가 수행하기를 원하는 작업을 시작하고 중지하는 방법을 지정합니다:

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  	// ...
}

그런 다음 Effect 의존성을 비워드면 ( [] ) 린터가 올바른 의존성을 제안합니다:

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Fix the mistake here!
  return <h1>Welcome to the {roomId} room!</h1>;
}

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

// chat.js
export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}

Effect는 반응형 값에 “반응”합니다. roomId 는 반응형 값이므로, 린터는 이를 의존성으로 지정했는지 확인합니다. roomId 가 다른 값을 받으면 React는 Effect를 다시 동기화합니다. 이렇게 하면 채팅이 선택된 방에 연결된 상태를 유지하고 드롭다운에 ‘반응’합니다.

To remove a dependency, prove that it’s not a dependency

Effect의 의존성을 “선택”할 수 없다는 점에 유의하세요. Effect의 코드에서 사용되는 모든 반응형 값은 의존성 목록에 선언되어야 합니다. 의존성 목록은 주변 코드에 의해 결정됩니다:

반응형 값에는 props와 컴포넌트 내부에서 직접 선언된 모든 변수 및 함수가 포함됩니다. roomId 는 반응형 값이므로 의존성 목록에서 제거할 수 없습니다. 린터가 허용하지 않습니다.

그리고 린터가 맞을 것입니다! roomId 는 시가닝 지남에 따라 변경될 수 있으므로 코드에 버그가 발생할 수 있습니다.

의존성을 제거하려면 해당 컴포넌트가 의존성이 될 필요가 없다는 것을 린터에 “증명”하세요. 예를 들어, roomId 를 컴포넌트 밖으로 이동시켜도 반응하지 않고 리렌더링 시에도 변경되지 않음을 증명할 수 있습니다:

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

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

이제 roomId 는 반응형 값이 아니므로 의존성이 될 필요가 없습니다.

이것이 빈 ( [] ) 의존성 목록을 지정할 수 있는 이유입니다. Effect는 더 이상 반응형 값에 의존하지 않으므로 컴포넌트의 props나 state가 변경될 때 Effect를 다시 실행할 필요가 없습니다.

To change the dependencies, change the code

작업 흐름에서 패턴을 발견했을 수도 있습니다.

  1. 먼저 Effect의 코드 또는 반응형 값 선언 방식을 변경합니다.
  2. 그런 다음, 변경한 코드에 맞게 의존성을 조정합니다.
  3. 의존성 목록이 마음에 들지 않으면 첫 번째 단계로 돌아가서 코드를 다시 변경합니다.

마지막 부분이 중요합니다. 의존성을 변경하려면 먼저 주변 코드를 변경하세요. 의존성 목록은 Effect의 코드에서 사용하는 모든 반응형 값의 목록이라고 생각하면 됩니다. 이 목록에 무엇을 넣을지는 사용자가 선택하지 않습니다. 이 목록은 코드를 설명합니다. 의존성 목록을 변경하려면 코드를 변경하세요.

이것은 방정식을 푸는 것처럼 느껴질 수 있습니다. 예를 들어, 의존성 제거와 같은 목표를 설정하고 그 목표에 맞는 코드를 “찾아야” 합니다. 모든 사람이 방정식을 푸는 것을 재미있어하는 것은 아니며, Effect를 작성할 때도 마찬가지입니다.

기존 코드베이스가 있는 경우 이와 같이 린터를 억제하는 Effect가 있을 수 있습니다.

useEffect(() => {
  // ...
  // 🔴 Avoid suppressing the linter like this:
  // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

의존성이 코드와 일치하지 않으면 버그가 발생할 위험이 매우 높습니다. 린터를 억제하면 Effect가 의존하는 값에 대해 React에 “거짓말”을 하게 됩니다.

Why is suppressing the dependency linter so dangerous?

린터를 억제하면 매우 직관적이지 않은 버그가 발생하여 찾아서 수정하기가 어렵습니다. 한 가지 예를 들어보겠습니다:

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
	setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

“마운트할 때만” Effect를 실행하고 싶다고 가정해 봅시다. 빈 ( [] ) 의존성이 그렇게 한다는 것을 읽었으므로 린터를 무시하고 [] 의존성을 강제로 지정하기로 결정했습니다.

이 카운터는 두 개의 버튼으로 설정할 수 있는 양만큼 매초마다 증가해야 합니다. 하지만 이 Effect가 아무 것도 의존하지 않는다고 React에 “거짓말”을 했기 때문에, React는 초기 렌더링에서 계속 onTick 함수를 사용합니다. 이 렌더링에서 count0 이었고 increment1 이었습니다. 그래서 이 렌더링의 onTick 은 항상 매초마다 setCount(0 + 1) 를 호출하고 항상 1 이 표시됩니다. 이와 같은 버그는 여러 컴포넌트가 분산되어 있을 때 수정하기가 어렵습니다.

린터를 무시하는 것보다 더 좋은 해결책은 항상 있습니다. 이 코드를 수정하려면 의존성 목록에 onTick 을 추가해야 합니다.

의존성 린트 오류는 컴파일 오류로 처리하는 것이 좋습니다. 이를 억제하지 않으면 이와 같은 버그가 발생하지 않습니다. 이 페이지의 나머지 부분에서는 이 경우와 다른 경우에 대한 대안을 설명합니다.

Removing unnecessary dependencies

코드를 반영하기 위해 Effect의 의존성을 조정할 때마다 의존성 목록을 살펴보십시오. 이러한 의존성 중 하나라도 변경되면 Effect가 다시 실행되는 것이 합리적일까요? 가끔 대답은 “아니오”입니다:

  • 다른 조건에서 Effect의 다른 부분을 다시 실행하고 싶을 수도 있습니다.
  • 일부 의존성의 변경에 “반응”하지 않고 “최신 값”만 읽고 싶을 수도 있습니다.
  • 의존성은 객체나 함수이기 때문에 의도치 않게 너무 자주 변경될 수 있습니다.

올바른 해결책을 찾으려면 Effect에 대한 몇 가지 질문에 답해야 합니다. 몇 가지 질문을 살펴봅시다.

Should this code move to an event handler?

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

form을 상상해 봅시다. 제출할 때 submitted state 변수를 true 로 설정합니다. POST 요청을 보내고 알림을 표시해야 합니다. 이 로직은 submittedtrue 가 될 때 “반응”하는 Effect 안에 넣었습니다:

function Form() {
  const [submitted, setSubmitted] = useState(false);

  useEffect(() => {
    if (submitted) {
      // 🔴 Avoid: Event-specific logic inside an Effect
      post('/api/register');
      showNotification('Successfully registered!');
    }
  }, [submitted]);

  function handleSubmit() {
    setSubmitted(true);
  }

  // ...
}

나중에 현재 테마에 따라 알림 메시지의 스타일을 지정하고 싶으므로 현재 테마를 읽습니다. theme 는 컴포넌트 본문에서 선언되었기 때문에 이는 반응형 값이므로 의존성에 추가합니다:

function Form() {
  const [submitted, setSubmitted] = useState(false);
  const theme = useContext(ThemeContext);

  useEffect(() => {
    if (submitted) {
      // 🔴 Avoid: Event-specific logic inside an Effect
      post('/api/register');
      showNotification('Successfully registered!', theme);
    }
  }, [submitted, theme]); // ✅ All dependencies declared

  function handleSubmit() {
    setSubmitted(true);
  }  

  // ...
}

이렇게 하면 버그가 발생하게 됩니다. 먼저 from을 제출한 다음 어두운 테마와 밝은 테마 간 전환한다고 가정해 보겠습니다. theme 가 변경되고 Effect가 다시 실행되어 동일한 알림이 다시 표시됩니다!

여기서 문제는 이것이 애초에 Effect가 아니어야 한다는 점입니다. 이 POST 요청을 보내고 특정 상호작용인 form 제출에 대한 응답으로 알림을 표시하고 싶다는 것입니다. 특정 상호작용에 대한 응답으로 일부 코드를 실행하려면 해당 로직을 이벤트 핸들러에 직접 넣어야 합니다.

function Form() {
  const theme = useContext(ThemeContext);

  function handleSubmit() {
    // ✅ Good: Event-specific logic is called from event handlers
    post('/api/register');
    showNotification('Successfully registered!', theme);
  }  

  // ...
}

이제 코드가 이벤트 핸들러에 있고, 이는 반응형 코드가 아니므로 사용자가 form을 제출할 때만 실행됩니다.

Is your Effect doing several unrelated things?

다음으로 스스로에게 물어봐야 할 질문은 Effect가 서로 관련이 없는 여러 가지 작업을 수행하고 있는지 여부입니다.

사용자가 도시와 지역을 선택해야 하는 배송 form을 만든다고 가정해 보겠습니다. 선택한 country 에 따라 서버에서 cities 목록을 페치해서 드롭다운에 표시합니다:

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  const [city, setCity] = 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

  // ...

Effect에서 데이터를 페칭하는 좋은 예시입니다. country props에 따라 cities state를 네트워크와 동기화하고 있습니다. ShippingForm 이 표시되는 즉시 country 가 변경될 때마다 데이터를 가져와야 하므로 이벤트 핸들러에서는 이 작업을 수행할 수 없습니다.

이제 도시 지역에 대한 두 번째 셀렉트박스를 추가하여 현재 선택된 cityareas 을 페치한다고 가정해 보겠습니다. 동일한 Effect 내에 지역 목록에 대한 두 번째 fetch 호출을 추가하는 것으로 시작할 수 있습니다:

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가 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는 country 가 변경될 때만 다시 실행되고, 두 번째 Effect는 city 가 변경될 때 다시 실행됩니다. 목적에 따라 분리했으니, 서로 다른 두 가지가 두 개의 개별 Effect에 의해 동기화됩니다. 두 개의 개별 Effect에는 두 개의 개별 의존성 목록이 있으므로 의도치 않게 서로를 촉발하지 않습니다.

최종 코드는 원본보다 길지만 Effect를 분할하는 것이 여전히 정확합니다. 각 Effect는 독립적인 동기화 프로세스를 나타내야 합니다. 이 예제에서는 한 Effect를 삭제해도 다른 Effect의 로직이 깨지지 않습니다. 즉, 서로 다른 것을 동기화하므로 분할하는 것이 좋습니다. 중복이 걱정된다면 반복되는 로직을 커스텀 Hook으로 추출하여 이 코드를 개선할 수 있습니다.

Are you reading some state to calculate the next state?

이 Effect는 새 메시지가 도착할 때마다 새로 생성된 배열로 message state 변수를 업데이트합니다.

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

messages 변수를 사용하여 모든 기존 메시지로 시작하는 새 배열을 생성하고 마지막에 새 메시지를 추가하빈다. 하지만 messages 는 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
  // ...

그리고 messages 를 의존성으로 만들면 문제가 발생하빈다.

메시지를 수신할 때마다 setMessages() 는 컴포넌트가 수신된 메시지를 포함하는 새 messages 배열로 리렌더링하도록 합니다. 하지만 이 Effect는 이제 messages 에 따라 달라지므로 Effect도 다시 동기화됩니다. 따라서 새 메시지가 올 때마다 채팅이 다시 연결됩니다. 사용자가 원하지 않을 것입니다!

이 문제를 해결하려면 Effect 내에서 messages 를 읽지 마세요. 대신 업데이터 함수를 setMessages 에 전달하세요:

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

[…msgs, receivedMessage] 와 같은 업데이터 함수만 전달하면 됩니다. React는 업데이터 함수를 대기열에 넣고 다음 렌더링 중에 msgs 인수를 제공합니다. 이 때문에 Effect 자체는 더 이상 messages 에 의존할 필요가 없습니다. 이 수정으로 인해 채팅 메시지를 수신해도 더 이상 채팅이 다시 연결되지 않습니다.

Do you want to read a value without “reacting” to its changes?

사용자가 새 메시지를 수신할 때 isMutedtrue 가 아닌 경우 사운드를 재생하고 싶다고 가정해 보겠습니다.

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

이제 Effect의 코드에서 isMuted 를 사용하므로 의존성에 추가해야 합니다:

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

문제는(사용자가 “Muted” 토글을 누르는 등) isMuted 가 변경될 때마다 Effect가 다시 동기화되고 채팅에 다시 연결된다는 점입니다. 이는 바람직한 사용자 경험이 아닙니다.

이 문제를 해결하려면 Effect에서 반응해서는 안 되는 로직을 추출해야 합니다. 이 Effect가 isMuted 의 변경에 “반응”하지 않기를 원합니다. 이 비반응 로직을 Effect Event로 옮기면 됩니다:

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 Event를 사용하면 Effect를 반응형 부분( roomId 와 같은 반응형 값과 그 변경에 “반응”해야 하는)과 비반응형 부분( onMessageisMuted 를 읽는 것처럼 최신 값만 읽는)으로 나눌 수 있습니다. 이제 Effect Event 내에서 isMuted 를 읽었으므로 Effect의 의존성이 될 필요가 없습니다. 그 결과, “Muted” 설정을 켜고 끌 때 채팅이 다시 연결되지 않아 원래 문제가 해결되었습니다!

Wrapping an event handler from the props

컴포넌트가 이벤트 핸들러를 props로 받을 때 비슷한 문제가 발생할 수 있습니다:

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

부모 컴포넌트가 렌더링할 때마다 다른 onReceiveMessage 함수를 전달한다고 가정해 보겠습니다.

<ChatRoom
  roomId={roomId}
  onReceiveMessage={receivedMessage => {
    // ...
  }}
/>

onReceiveMessage 는 의존성이므로 부모가 리렌더링할 때마다 Effect가 다시 동기화됩니다. 그러면 채팅에 다시 연결됩니다. 이 문제를 해결하려면 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는 반응하지 않으므로 의존성으로 지정할 필요가 없습니다. 그 결과, 부모 컴포넌트가 리렌더링할 때마다 다른 함수를 전달하더라도 채팅이 더 이상 다시 연결되지 않습니다.

Separating reactive and non-reactive code

이 예제에서는 roomId 가 변경될 때마다 방문을 기록하려고 합니다. 모든 로그에 현재 notificationCount 를 포함하고 싶지만 notificationCount 변경으로 로그 이벤트가 촉발하는 것은 원하지 않습니다.

해결책은 다시 비반응형 코드를 Effect Event를 분리하는 것입니다:

function Chat({ roomId, notificationCount }) {
  const onVisit = useEffectEvent(visitedRoomId => {
    logVisit(visitedRoomId, notificationCount);
  });

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

로직이 roomId 와 관련하여 반응하기를 원하므로 Effect 내부에서 roomId 를 읽습니다. 그러나 notificationCount 를 변경하여 추가 방문을 기록하는 것은 원하지 않으므로 Effect Event 내부에서 notificationCount 를 읽습니다. Effect Event를 사용하여 Effect에서 최신 props과 state를 읽는 방법에 대해 자세히 알아보세요.

Does some reactive value change unintentionally?

Effect가 특정 값에 “반응”하기를 원하지만, 그 값이 원하는 것보다 더 자주 변경되어 사용자의 관점에서 실제 변경 사항을 반영하지 못할 수도 있습니다. 예를 들어, 컴포넌트 본문에 options 객체를 생성한 다음 Effect 내부에서 해당 객체를 읽는다고 가정해 보겠습니다:

function ChatRoom({ roomId }) {
  // ...
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    // ...

이 객체는 컴포넌트 본문에서 선언되므로 반응형 값입니다. Effect 내에서 이와 같은 반응형 값을 읽으면 의존성으로 선언합니다. 이렇게 하면 Effect가 변경 사항에 “반응”하게 됩니다:

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

의존성으로 선언하는 것이 중요합니다! 이렇게 하면 예를 들어, roomId 가 변경되면 Effect가 새 options 으로 채팅에 다시 연결됩니다. 하지만 위 코드에도 문제가 있습니다. 이를 확인하려면 아래 샌드박스의 input에 타이핑하고 콘솔에서 어떤 일이 발생하는 지 살펴보세요:

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

// chat.js
export function createConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}

위의 샌드박스에서 입력은 message state 변수만 업데이트합니다. 사용자 입장에서는 이것이 채팅 연결에 영향을 미치지 않아야 합니다. 하지만 message 를 업데이트할 때마다 컴포넌트가 리렌더링됩니다. 컴포넌트가 리렌더링되면 그 안에 있는 코드가 처음부터 다시 실행됩니다.

ChatRoom 컴포넌트를 리렌더링할 때마다 새로운 options 객체가 처음부터 새로 생성됩니다. React는 options 객체가 마지막 렌더링 중에 생성된 options 객체와 다른 객체임을 인식합니다. 그렇기 때문에 ( options 에 따라 달라지는) Effect를 다시 동기화하고 사용자가 입력할 때 채팅이 다시 연결됩니다.

이 문제는 객체와 함수에만 영향을 줍니다. JavaScript에서는 새로 생성된 객체와 함수가 다른 모든 객체와 구별되는 것으로 간주됩니다. 그 안의 내용이 동일할 수 있다는 것은 중요하지 않습니다.

// 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 내부로 이동하거나 원시 값을 추출해 보세요.

Move dynamic objects and functions outside your Effect

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

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

이렇게 하면 린터가 반응하지 않다는 것을 증명할 수 있습니다. 리렌더링의 결과로 변경될 수 없으므로 의존성이 될 필요가 없습니다. 이제 ChatRoom 을 리렌더링해도 Effect가 다시 동기화되지 않습니다.

이는 함수에도 적용됩니다.

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

createOptions 는 컴포넌트 외부에서 선언되므로 반응형 값이 아닙니다. 그렇기 때문에 Effect의 의존성에 지정할 필요가 없으며, Effect가 다시 동기화되지 않는 이유이기도 합니다.

Move dynamic objects and functions inside your Effect

객체가 roomId 프로퍼티처럼 리렌더링의 결과롤 변경될 수 있는 반응형 값에 의존하는 경우, 컴포넌트 외부로 끌어낼 수 없습니다. 하지만 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 입니다. roomId 는 객체나 함수가 아니기 때문에 의도치 않게 달라지지 않을 것이라고 확신할 수 있습니다. JavaScript에서 숫자와 문자열은 그 내용에 따라 비교됩니다.

// 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 드롭다운을 변경하면 다시 연결됩니다.

이는 함수에서도 마찬가지입니다.

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

Read primitive values from objects

가끔 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 이 실제로 다른 경우 채팅이 다시 연결됩니다.

Calculate primitive values from functions

함수에 대해서도 동일한 접근 방식을 사용할 수 있습니다. 예를 들어, 부모 컴포넌트가 함수를 전달한다고 가정해 보겠습니다:

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

의존성을 만들지 않으려면 (그리고 리렌더링할 때 다시 연결되는 것을 방지하려면) Effect 외부에서 호출하세요. 이렇게 하면 객체가 아니면 Effect 내부에서 읽을 수 있는 roomIdserverUrl 값을 얻을 수 있습니다:

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로 함수를 감싸세요.

0개의 댓글