ReactNextCentral

Effect: 종속성 제거

Published on
이 문서에서는 리액트의 이펙트에서 불필요한 종속성을 식별하고 제거하는 방법을 안내합니다.
Table of Contents

이펙트를 작성할 때 린터는 이펙트가 읽는 모든 반응형 값(예: props 및 state)을 이펙트의 종속성 목록에 포함했는지 확인합니다. 이렇게 하면 이펙트가 컴포넌트의 최신 props 및 state와 동기화된 상태를 유지할 수 있습니다. 불필요한 종속성은 이펙트를 너무 자주 실행하거나 무한 루프를 생성할 수 있습니다. 이 가이드를 따라 이펙트에서 불필요한 종속성을 검토하고 제거하는 방법을 알아보세요.

의존성은 코드와 맞추어야 합니다.

이펙트를 작성할 때, 먼저 이펙트가 수행해야 할 작업을 시작하고 중지하는 방법을 지정합니다.

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

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

그런 다음 이펙트의 종속성을 비워두면([]), 린터가 올바른 종속성을 제안합니다. Edit crazy-mccarthy-ypr7lf

린터가 제안하는 대로 종속성을 채워 넣으세요.

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ 모든 종속성이 선언되었습니다.
  // ...
}

이펙트는 반응형 값에 "반응"합니다. roomId는 반응형 값입니다(다시 렌더링으로 인해 변경될 수 있음). 린터는 roomId을 종속성으로 지정했는지 확인합니다. roomId이 다른 값을 받으면 리액트는 이펙트를 다시 동기화합니다. 이렇게 하면 채팅이 선택한 방에 연결되어 드롭다운에 "반응"합니다.

Edit festive-sky-fyltuv

종속성을 제거하려면, 해당 종속성이 필요하지 않음을 증명하세요

이펙트의 종속성을 "선택"할 수는 없다는 점에 유의하세요. 이펙트의 코드에서 사용되는 모든 반응형 값은 종속성 목록에 선언되어야 합니다. 종속성 목록은 주변 코드에 의해 결정됩니다.

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

function ChatRoom({ roomId }) { // 이것은 반응형 값입니다
  useEffect(() => {
    // 이 이펙트는 해당 반응형 값읽습니다
    const connection = createConnection(serverUrl, roomId); 
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ 따라서 이 반응형 값을 종속성으로 명시해야 합니다
  // ...
}

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

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
    // 🔴 리액트 Hook useEffect has a missing dependency: 'roomId'
  }, []); 
  // ...
}

린터가 맞을 것입니다! roomId가 시간이 지남에 따라 변경될 수 있기 때문에 코드에 버그가 생길 수 있습니다.

종속성을 제거하려면, 린터에게 종속성이 필요하지 않음을 "증명"하세요. 예를 들어, roomId를 컴포넌트 외부로 이동하여 반응형이 아니며 리렌더링 시 변경되지 않음을 증명할 수 있습니다.

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

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ 모든 종속성이 선언되었습니다
  // ...
}

Edit goofy-mopsa-exqi0o

이제 roomId는 반응형 값이 아니므로(리렌더링 시 변경되지 않음) 종속성으로 필요하지 않습니다. 이제 빈 ([]) 종속성 목록을 지정할 수 있게 되었습니다. 이펙트는 더 이상 컴포넌트의 props 또는 state가 변경될 때마다 다시 실행될 필요가 없기 때문에 종속성이 필요하지 않습니다.

종속성을 변경하려면 코드를 변경하세요

작업 흐름에서 패턴을 알아차렸을 수도 있습니다.

  1. 먼저, 이펙트의 코드를 변경하거나 반응형 값의 선언 방식을 변경합니다.
  2. 그런 다음, 린터를 따라가서 종속성을 변경된 코드와 일치하도록 조정합니다.
  3. 만족스러운 종속성 목록이 아니라면, 첫 번째 단계로 돌아가서 (다시 코드를 변경) 변경합니다.

마지막 부분이 중요합니다. 종속성을 변경하려면 먼저 주변 코드를 변경하세요. 종속성 목록을 이펙트 코드에서 사용하는 모든 반응형 값의 목록으로 생각할 수 있습니다. 당신이 그 목록에 무엇을 넣을지 선택하는 것이 아닙니다. 목록은 코드를 설명합니다. 종속성 목록을 변경하려면 코드를 변경하세요.

이는 방정식을 푸는 것과 비슷하게 느껴질 수 있습니다. 목표(예: 종속성 제거)로 시작하고 그 목표와 일치하는 코드를 "찾아야"합니다. 방정식을 푸는 것을 즐거워하는 사람이 모두가 아니며, 마찬가지로 이펙트 작성에도 동일한 이야기가 될 수 있습니다! 다행히도 아래에 시도할 수 있는 일반적인 팁 목록이 있습니다.

주의하기

기존 코드베이스가 있다면, 다음과 같이 린터를 비활성화한 이펙트가 있을 수 있습니다.

useEffect(() => {
  // ...
  // 🔴 다음과 같이 린터를 비활성화하는 것은 피하세요:
  // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

종속성이 코드와 일치하지 않을 때 버그가 발생할 위험이 매우 높습니다. 린터를 비활성화함으로써, 이펙트가 의존하는 값에 대해 리액트에 "거짓말"을 합니다.

대신, 아래의 기술을 사용하세요.

자세히 알아보기: 의존성 린터를 무시하는 것이 왜 위험한가요?

의존성 린터를 무시하면 찾고 수정하기 어려운 매우 직관적이지 않은 버그가 발생합니다. 다음은 한 가지 예입니다. Edit quirky-surf-w5p3bc

이펙트를 "마운트 시에만" 실행하고 싶다고 가정해 보겠습니다. 빈 ([]) 종속성이 그런 역할을 한다는 것을 읽어 보았으므로, 린터의 경고를 무시하고 강제로 []를 종속성으로 지정하기로 결정했습니다.

이 카운터는 매 초마다 두 개의 버튼으로 설정 가능한 양만큼 증가해야 했습니다. 그러나 이렇게 Effect가 아무 것에도 의존하지 않는다고 리액트에 "거짓말"을 하였기 때문에, 리액트는 항상 초기 렌더링에서의 onTick 함수를 계속 사용하게 됩니다. 해당 렌더링에서 count는 0이고 increment는 1이었습니다. 이렇게 때문에 해당 렌더링의 onTick은 매 초마다 setCount(0 + 1)을 호출하고, 항상 1을 보게 됩니다. 이와 같은 버그는 여러 컴포넌트에 퍼져 있을 때 수정하기가 더 어려워집니다.

린터를 무시하는 대신 항상 더 좋은 해결책이 있습니다! 이 코드를 수정하려면 onTick을 종속성 목록에 추가해야 합니다. (인터벌이 한 번만 설정되도록 하려면 onTick을 이펙트 이벤트로 만드세요.)

의존성 린트 오류를 컴파일 오류로 취급하는 것을 권장합니다. 린터를 무시하지 않으면 이와 같은 버그를 결코 발견하지 못합니다. 이 페이지의 나머지 부분에서는 이와 같은 경우를 포함한 다른 경우에 대한 대안을 문서화합니다.

불필요한 종속성 제거하기

Effect의 종속성을 코드를 반영하는 대로 조정할 때마다 종속성 목록을 살펴보세요. 이러한 종속성이 변경될 때 Effect를 다시 실행하는 것이 의미가 있는지 확인해야 합니다. 때로는 답이 "아니오"일 수 있습니다.

  • 다른 조건에 따라 Effect의 서로 다른 부분을 다시 실행하고 싶을 수 있습니다.
  • 일부 종속성의 최신 값을 읽기만 하고 해당 변경에 "반응"할 필요가 없을 수 있습니다.
  • 객체나 함수이기 때문에 의도치 않게 종속성이 너무 자주 변경될 수 있습니다.

적절한 해결책을 찾으려면 Effect에 대해 몇 가지 질문에 답해야 할 것입니다. 이를 함께 살펴보겠습니다.

이 코드를 이벤트 핸들러로 이동해야 할까요?

가장 먼저 생각해야 할 것은 이 코드가 전혀 Effect여야 할지에 대한 여부입니다.

양식(form)을 상상해보세요. 제출(submit) 시에 submitted 상태 변수를 true로 설정합니다. POST 요청을 보내고 알림을 표시해야 합니다. 이 로직을 submittedtrue일 때 "반응"하는 Effect 내부에 넣었습니다.

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

  useEffect(() => {
    if (submitted) {
      // 🔴 피해야 할 사항: 이벤트별 로직을 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) {
      // 🔴 피해야 할 사항: 이벤트별 로직을 Effect 내부에 작성
      post('/api/register');
      showNotification('Successfully registered!', theme);
    }
  }, [submitted, theme]); // ✅ 모든 종속성 선언

  function handleSubmit() {
    setSubmitted(true);
  }  

  // ...
}

이렇게 하면 버그가 발생합니다. 먼저 양식을 제출한 다음에 다크 테마와 라이트 테마 사이를 전환하면 테마가 변경되고 Effect가 다시 실행되어 같은 알림을 다시 표시할 것입니다!

문제는 이 코드가 처음부터 Effect가 아니어야 한다는 점입니다. 이 코드는 양식 제출에 대한 응답으로 POST 요청을 보내고 알림을 표시하는 것을 원합니다. 이는 특정한 상호작용에 대한 응답으로 일부 코드를 실행하려는 것입니다. 특정한 상호작용에 대한 코드를 실행하려면 해당 로직을 직접 해당 이벤트 핸들러에 넣으세요:

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

  function handleSubmit() {
    // ✅ 좋은 방법: 이벤트별 로직은 이벤트 핸들러에서 호출됩니다.
    post('/api/register');
    showNotification('Successfully registered!', theme);
  }  

  // ...
}

이제 코드가 이벤트 핸들러 안에 있으므로 반응성이 없으므로 사용자가 양식을 제출할 때만 실행됩니다. 이벤트 핸들러와 Effect 중 어떤 것을 선택해야 하는지불필요한 Effect를 삭제하는 방법에 대해 자세히 읽어보세요.

Effect가 관련 없는 여러 작업을 수행하고 있나요?

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

사용자가 도시와 지역을 선택해야 하는 배송 양식을 만든다고 상상해보세요. 사용자는 드롭다운에서 보여질 도시 목록을 선택한 국가에 따라 서버에서 가져옵니다.

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]); // ✅ 모든 종속성 선언

  // ...

이것은 Effect에서 데이터를 가져오는 좋은 예입니다. 도시 상태를 국가 prop과 동기화하고 있습니다. 이 작업은 이벤트 핸들러에서 수행할 수 없으므로 ShippingForm이 표시되자마자 및 국가가 변경될 때마다 가져와야 합니다.

이제 현재 선택된 도시에 대한 지역 목록을 가져와야 하는 두 번째 select 상자를 추가한다고 가정해보겠습니다. 동일한 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);
        }
      });
    // 🔴 피해야 할 사항: 단일 Effect에서 두 개의 독립적인 프로세스를 동기화
    if (city) {
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
    }
    return () => {
      ignore = true;
    };
  }, [country, city]); // ✅ 모든 종속성 선언

  // ...

그러나 이제 Effect가 city 상태 변수를 사용하기 때문에 종속성 목록에 city를 추가해야 했습니다. 그 결과 사용자가 다른 도시를 선택하면 Effect가 다시 실행되고 fetchCities(country)를 호출하게 되어 도시 목록을 여러 번 불필요하게 다시 가져오게 됩니다.

이 코드의 문제는 두 가지 관련 없는 작업을 동기화하고 있다는 것입니다.

  1. 국가 prop에 따라 네트워크에서 도시 상태를 동기화하려고 합니다.
  2. 도시 상태에 따라 네트워크에서 지역 상태를 동기화하려고 합니다.

이러한 로직을 두 개의 Effects로 분할하고, 각각 필요한 prop에 반응하도록 분리하세요.

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]); // ✅ 모든 종속성 선언

  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]); // ✅ 모든 종속성 선언

  // ...

이제 첫 번째 Effect는 국가가 변경될 때만 다시 실행되고, 두 번째 Effect는 도시가 변경될 때만 다시 실행됩니다. 이러한 작업을 목적에 따라 분리했습니다. 두 개의 Effects는 독립적으로 실행되는 것이므로 종속성 목록이 서로 의도치 않게 트리거되지 않습니다.

최종 코드는 원래 코드보다 길지만, 이러한 Effects를 분리하는 것은 여전히 올바른 방법입니다. 각 Effect는 독립적인 동기화 프로세스를 나타내야 합니다. 이 예시에서는 하나의 Effect를 삭제해도 다른 Effect의 로직에 영향을 주지 않습니다. 이는 서로 다른 것을 동기화하므로 분리하는 것이 좋습니다. 중복에 대해 걱정된다면 반복 로직을 사용자 정의 Hook으로 추출하여 코드를 개선할 수 있습니다.

다음 상태를 계산하기 위해 상태를 읽고 있나요?

이 Effect는 새로운 메시지가 도착할 때마다 messages 상태 변수를 새로운 배열로 업데이트합니다.

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

이 Effect는 기존의 모든 메시지로 시작하는 새로운 배열을 생성하고, 그 끝에 새로운 메시지를 추가하기 위해 messages 변수를 사용합니다. 그러나 messages는 Effect에서 읽히는 reactive value이기 때문에 종속성이어야 합니다.

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]); // ✅ 모든 종속성 선언
  // ...

messages를 종속성으로 만들면 문제가 발생합니다.

메시지를 수신할 때마다 setMessages()를 호출하여 새로운 메시지가 포함된 새로운 messages 배열로 컴포넌트가 다시 렌더링됩니다. 그러나 이제 이 Effect는 messages에 종속되었기 때문에 이로 인해 Effect도 재동기화됩니다. 따라서 새로운 메시지마다 채팅이 다시 연결됩니다. 사용자는 이를 원하지 않을 것입니다! Edit intelligent-feather-1kh8bo

이 문제를 해결하기 위해 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]); // ✅ 모든 종속성 선언
  // ...

이제 Effect에서 messages 변수를 전혀 읽지 않는 것에 주목하세요. 이제 msgs => \[...msgs, receivedMessage\]와 같은 업데이터 함수를 전달하기만 하면 됩니다. 리액트는 업데이터 함수를 대기열에 넣고 다음 렌더링 중에 이 함수에 msgs 인자를 제공합니다. 이로 인해 Effect 자체가 더 이상 messages에 의존할 필요가 없습니다. 이 수정으로 인해 채팅 메시지를 받아도 채팅이 다시 연결되지 않습니다.

값의 변경에 "반응"하지 않고 값을 읽고 싶나요?

작업 중

이 섹션은 안정 버전의 리액트에서 아직 출시되지 않은 실험적인 API를 설명합니다.

사용자가 새로운 메시지를 받을 때 사운드를 재생하려고 하지만 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]); // ✅ 모든 종속성 선언
  // ...

문제는 isMuted가 변경될 때마다 (예: 사용자가 "음소거" 토글을 누를 때) Effect가 재동기화되고 채팅이 다시 연결된다는 것입니다. 이는 원하는 사용자 경험이 아닙니다! (이 예시에서는 린터를 비활성화해도 작동하지 않습니다. 비활성화하면 isMuted가 이전 값으로 "멈추게" 됩니다.)

이 문제를 해결하기 위해 반응적이지 않아야 하는 로직을 Effect에서 분리해야 합니다. 이 Effect가 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]); // ✅ 모든 종속성 선언
  // ...

Effect 이벤트를 사용하면 Effect를 반응적인 부분 (roomId와 같은 반응적인 값과 그 변경에 "반응"해야 하는 부분)과 반응적이지 않은 부분 (onMessageisMuted를 읽는 것과 같이 최신 값을 읽기만 하는 부분)으로 분리할 수 있습니다. 이제 Effect 이벤트 내부에서 isMuted를 읽기 때문에 Effect의 종속성이 될 필요가 없습니다. 결과적으로 "음소거" 설정을 토글하면 채팅이 다시 연결되지 않으므로 원래 문제가 해결됩니다!

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]); // ✅ 모든 종속성 선언
  // ...

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

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

onReceiveMessage가 종속성이므로 이는 Effect가 부모 컴포넌트의 재렌더링 후에 재동기화되도록 할 것입니다. 이는 채팅을 다시 연결하게 됩니다. 이 문제를 해결하기 위해 호출을 Effect 이벤트로 래핑합니다.

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]); // ✅ 모든 종속성 선언
  // ...

Effect 이벤트는 반응적이지 않으므로 종속성으로 지정할 필요가 없습니다. 따라서 부모 컴포넌트가 매 재렌더링마다 다른 함수를 전달하더라도 채팅이 다시 연결되지 않게됩니다.

반응적인 코드와 반응하지 않는 코드 분리하기

이 예시에서는 roomId가 변경될 때마다 방문 내역을 기록하려고 합니다. 각 로그에 현재 notificationCount를 포함하려고 하지만 notificationCount의 변경으로 인해 로그 이벤트가 트리거되지 않도록 하려고 합니다.

해결책은 다시 한 번 반응적이지 않은 코드를 Effect 이벤트로 분리하는 것입니다.

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

  useEffect(() => {
    onVisit(roomId);
  }, [roomId]); // ✅ 모든 종속성 선언
  // ...
}

roomId에 따라 로직이 반응적이도록 하려면 Effect 내부에서 roomId를 읽습니다. 그러나 notificationCount의 변경으로 인해 추가적인 방문 기록이 남지 않도록 하려면 Effect 이벤트 내에서 notificationCount를 읽습니다. Effect 이벤트를 사용하여 Effects에서 최신 props와 state를 읽는 방법에 대해 자세히 알아보세요.

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

가끔은 특정 값에 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]); // ✅ 모든 종속성 선언
  // ...

반드시 종속성으로 선언해야 합니다! 예를 들어, roomId가 변경되면 Effect가 새로운 options로 채팅을 다시 연결하도록 보장됩니다. 그러나 위의 코드에는 문제가 있습니다. 아래의 샌드박스에서 입력란에 타이핑해보면 콘솔에서 어떤 일이 일어나는지 확인해보세요:

위의 샌드박스에서 입력란은 메시지 상태 변수만 업데이트합니다. 사용자의 관점에서는 이는 채팅 연결에 영향을 주지 않아야 합니다. 그러나 메시지를 업데이트할 때마다 컴포넌트가 다시 렌더링됩니다. 컴포넌트가 다시 렌더링될 때는 내부 코드가 처음부터 다시 실행됩니다.

ChatRoom 컴포넌트의 각 렌더링마다 새로운 options 객체가 처음부터 생성됩니다. 리액트는 options 객체가 마지막 렌더링 중에 생성된 options 객체와 다른 객체임을 인식합니다. 그래서 options에 종속성을 갖는 Effect가 다시 동기화되고, 타이핑하는 동안 채팅이 다시 연결됩니다.

이 문제는 객체와 함수에만 영향을 미칩니다. JavaScript에서는 각각의 새로 생성된 객체와 함수가 다른 객체와 함수로 간주됩니다. 그 내용이 동일하더라도 상관하지 않습니다!

// 첫 번째 렌더링 중에
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// 다음 렌더링 중에
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// 이 두 개의 객체는 서로 다른 객체입니다!
console.log(Object.is(options1, options2)); // false

객체와 함수를 Effect의 종속성으로 사용하면 필요 이상으로 Effect가 더 자주 동기화될 수 있습니다.

가능한 경우, Effect의 종속성으로 객체와 함수를 피하는 것이 좋습니다. 대신 컴포넌트 외부로 이동하거나 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();
  }, []); // ✅ 모든 종속성 선언
  // ...

이렇게 하면, 렌더링할 때 Effect가 동기화되지 않는 것을 linter에 증명할 수 있습니다. 객체는 다시 렌더링으로 인해 변경되지 않기 때문에 종속성으로 선언할 필요가 없습니다. 이제 ChatRoom 을 다시 렌더링해도 Effect가 동기화되지 않습니다.

함수에 대해서도 동일한 방식으로 적용할 수 있습니다.

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

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

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ 모든 종속성 선언
  // ...

createOptions 함수가 컴포넌트 외부에 선언되었으므로 반응적인 값이 아닙니다. 이 때문에 Effect의 종속성으로 명시할 필요가 없고, Effect를 동기화시키지 않게 됩니다.

동적인 객체와 함수를 Effect 내부로 이동하세요

만약 객체가 re-render로 인해 변경될 수 있는 반응적인 값에 의존한다면, 해당 객체를 컴포넌트 외부로 가져올 수 없습니다. 그러나 객체를 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]); // ✅ 모든 종속성 선언
  // ...

이제 options는 Effect 내부에서 선언되었기 때문에 Effect의 종속성이 아니게 되었습니다. 대신, Effect에서 사용하는 유일한 반응적인 값은 roomId입니다. roomId은 객체나 함수가 아니기 때문에 의도치 않게 변경되지 않을 것이라고 확신할 수 있습니다. JavaScript에서는 숫자와 문자열은 내용에 따라 비교됩니다.

// 첫 번째 렌더링 중에
const roomId1 = 'music';

// 다음 렌더링 중에
const roomId2 = 'music';

// 이 두 개의 문자열은 동일합니다!
console.log(Object.is(roomId1, roomId2)); // true

이러한 수정으로 인해, 입력란을 편집해도 채팅이 다시 연결되지 않습니다. 그러나 roomId 드롭다운을 변경하면 예상한 대로 채팅이 다시 연결됩니다.

Edit cocky-moon-9k56ji

함수에 대해서도 동일한 원리로 적용할 수 있습니다.

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 내부에서 여러분만의 함수를 작성하여 로직을 그룹화할 수 있습니다. 이 함수들이 Effect 내부에서 선언되었다면 반응적인 값이 아니기 때문에 Effect의 종속성으로 선언할 필요가 없습니다.

객체에서 원시값 읽기

가끔씩 props로부터 객체를 받을 수 있습니다.

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

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // ✅ 모든 종속성 선언
  // ...

여기에서의 위험은 부모 컴포넌트가 렌더링 중에 객체를 생성할 수 있다는 점입니다.

<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]); // ✅ 모든 종속성 선언
  // ...

로직이 약간 반복적이 됩니다 (Effect 외부에서 객체로부터 값을 읽고, 그 값들과 동일한 값을 가진 객체를 Effect 내부에서 생성). 그러나 이렇게 하면 Effect가 실제로 어떤 정보에 의존하는지 명확하게 알 수 있습니다. 부모 컴포넌트에서 실수로 객체가 재생성되면 채팅이 다시 연결되지 않습니다. 그러나 options.roomIdoptions.serverUrl이 정말로 다른 값이라면 채팅이 다시 연결될 것입니다.

함수에서 원시값 계산하기

같은 방법은 함수에도 적용될 수 있습니다. 예를 들어, 부모 컴포넌트가 함수를 전달하는 경우를 가정해봅시다.

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

이 함수를 종속성으로 만들지 않고(그리고 다시 렌더링할 때마다 다시 연결되지 않도록 하기 위해), Effect 외부에서 호출하세요. 이렇게 하면 객체가 아닌 roomIdserverUrl 값을 얻을 수 있으며, 이 값을 Effect 내부에서 읽을 수 있습니다.

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]); // ✅ 모든 종속성 선언
  // ...

이 방법은 순수 함수에만 작동합니다. 순수 함수는 렌더링 중에 호출해도 안전하기 때문입니다. 함수가 이벤트 핸들러인 경우이지만 해당 변경 사항이 Effect를 다시 동기화시키지 않으려면, 대신 Effect 이벤트로 래핑하세요.

요약

  • 종속성은 항상 코드와 일치해야 합니다.
  • 만족스럽지 않은 종속성이 있을 때 수정해야 할 것은 코드입니다.
  • 린터를 억제하는 것은 혼란스러운 버그를 야기하므로 항상 피해야 합니다.
  • 종속성을 제거하려면 해당 종속성이 필요하지 않음을 린터에 "증명"해야 합니다.
  • 특정 상호작용에 대한 응답으로 코드를 실행해야 한다면 해당 코드를 이벤트 핸들러로 이동시키세요.
  • Effect의 서로 다른 부분이 서로 다른 이유로 다시 실행되어야 한다면, 여러 개의 Effect로 분할하세요.
  • 이전 상태를 기반으로 상태를 업데이트하려면 업데이터 함수를 전달하세요.
  • "반응"하지 않고 최신 값을 읽고 싶다면 Effect에서 Effect 이벤트를 추출하세요.
  • JavaScript에서 객체와 함수는 서로 다른 시간에 생성되었다면 다른 것으로 간주됩니다.
  • 객체 및 함수 종속성을 피하려고 노력하세요. 이들을 컴포넌트 외부로 이동하거나 Effect 내부로 이동하세요.