React에서 사이드 이펙트를 제거하는 방법

seonja kim·2021년 11월 27일
3
post-thumbnail

부제 : 당신의 리액트 애플리케이션에서 메모리 누수를 막아요.

이 글은 ReactONE의 How to Cleanup Side Effects in React를 번역한 글입니다. 원본은 제목 링크를 클릭해서 확인해주세요.

오역은 알려주시면 바로 시정하도록 하겠습니다.

리액트에서는 컴포넌트가 렌더링된 후에 필요한 작업이 있거나 사이드 이펙트를 일으킬 필요가 있을 때 useEffect를 사용합니다. 사이드 이펙트는 서버에서 데이터 가져오기, 로컬 스토리지에서 정보를 가져오거나 저장하기, 이벤트 리스너 또는 구독(어떤 이벤트가 발생할 때마다 서버로 그 결과를 보내는 것을 의미) 설정일 수 있습니다.

useEffect()를 사용하면 함수형 컴포넌트에서 컴포넌트 라이프 사이클을 관리할 수 있습니다. useEffect()는 componentDidMount , componentDidUpdate 및 componentWillUnmount를 조합해놓았다고 할 수 있습니다.

그러나 때로는 컴포넌트 생명주기와 사이드 이펙트 생명주기(시작, 진행 중, 완료)가 교차하는 지점에서 문제가 발생할 수 있습니다.

사이드 이펙트가 완료되었을 때, 이미 언마운트된 컴포넌트의 상태(state)를 업데이트하려고 하고 React 경고가 나타납니다.

메모리 누수 경고

이 게시물에서는 위의 경고가 나타나는 경우와 메모리 누수를 방지하기 위해 React에서 사이드 이펙트를 올바르게 정리하는 방법에 대해 설명합니다.


1- 이슈

먼저 예제의 문제 상황을 재현해보겠습니다. 이 예는 Users 또는 hello world 텍스트에 대한 정보를 보여줍니다. 사용자 목록은 fetch 요청을 사용하여 로드됩니다.

function App() {
  const [display, setDisplay] = useState("users");
  return (
    <div className='App'>
      <button
        onClick={() => {
          setDisplay("users");
        }}>
        display users
      </button>
      <button
        onClick={() => {
          setDisplay("posts");
        }}>
        display hello message
      </button>
      <>{display === "users" ? <Users /> : <Hello />}</>
    </div>
  );
}
App 컴포넌트

export default function Hello() {
    
  return (
    <p>
      Hello, World !!
    </p>
  );
}
Hello 컴포넌트
export default function Users() {
  const [list, setList] = useState(null);

  useEffect(() => {
    (function () {
      try {
        fetch(`https://jsonplaceholder.typicode.com/users`)
          .then((response) => response.json())
          .then((json) => setList(json));
      } catch (e) {
        // Handle the error
      }
    })();
  });
  return (
    <div>
      {list === null ? (
        <p>Loading users...</p>
      ) : (
        <>
          {list.map((item) => {
            return <pre key={item.id}>{item.name}</pre>;
          })}
        </>
      )}
    </div>
  );
};
Users 컴포넌트

Users의 fetch 함수 실행이 완료되기 전에 hello 메시지 버튼을 클릭하면 콘솔에 경고 메시지가 나타납니다.

이 경고의 이유는 해당 Users 컴포넌트가 이미 언마운트되었지만 사이드 이펙트가 완료되어 상태를 업데이트하려고 시도하기 때문입니다.

컴포넌트가 언마운트될 때, 사이드 이펙트를 취소하여 이 문제를 해결할 수 있습니다. 다음 섹션에서 더 살펴보겠습니다.

2- 정리 (Clean Up)

다행히 useEffect(callback, 종속성)를 사용하면 사이드 이펙트를 쉽게 정리할 수 있습니다. 콜백 함수가 함수를 반환하면 React는 이를 이벤트 정리로 사용합니다.

useEffect(() => { 
    
    // 여기에서 사이트 이펙트가 발생합니다.
    
    return () => { 
        // 여기에 사이드 이펙트가 발생하는 함수를 정리합니다. 
    }
    // 의존성 배열
}, [])

2-1 fetch 요청 정리

먼저 DOM 요청을 중단할 수 있는 컨트롤러를 만든 다음 컨트롤러를 fetch 요청과 연결합니다. 마지막으로 정리 기능은 컴포넌트가 언마운트되는 경우 요청을 중단합니다.

 useEffect(() => {
    //컨트롤러를 만듭니다.
    let controller = new AbortController();
    (async () => {
      try {
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/posts`,
          {
            // 만든 컨트롤러와 fetch 함수를 연결합니다.
            signal: controller.signal,
          },
        );
        setList(await response.json());
        controller = null;
      } catch (e) {
        // Handle the error
      }
    })();
    //컴포넌트가 언마운트될 때, 요청을 중단합니다.
    return () => controller?.abort();
  });
컴포넌트가 언마운트될 때, 요청을 중단합니다.

2-2 props 또는 state 업데이트에 대한 정리

사이드 이펙트가 prop 또는 state 값에 의존하고 있을 경우, fetch 요청 중단이 필요한 경우가 있습니다. 앞서 언급했듯이 useEffect()가 이 경우를 처리할 수 있습니다. 다음의 props의 id를 기반으로 직원의 세부사항을 fetch 요청하는 User 컴포넌트를 살펴보겠습니다.

  
export default function User({ id }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let controller = new AbortController();
    (async () => {
      try {
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/users/${id}`,
          {
            signal: controller.signal,
          },
        );
        setUser(await response.json());
        controller = null;
      } catch (e) {
        // Handle the error
      }
    })();
    // clean up function
    return () => controller?.abort();
    // add a dependency array
  }, [id]);

  return (
    <div>
      {user === null ? (
        <p>Loading user's data ...</p>
      ) : (
        <pre key={user.id}>{user.name}</pre>
      )}
    </div>
  );
};
props 또는 state 업데이트에 대한 정리

2-3 타이머 정리

타이머 기능을 사용할 때 clearTimeout(timerId)를 사용하여 언마운트시 타이머 기능을 삭제할 수 있습니다.

예를 들어, 매초 자동으로 증가하는 카운터를 봅시다.

export default function AutoIncrementaedCounter() {
  const [counterValue, setCounterValue] = useState(0);

  useEffect(() => {
    //3초마다 counter를 1씩 증가시킨다.
    let timerId = setTimeout(() => {
      setCounterValue(counterValue + 1);
      timerId = null;
    }, 3000);
    // 컴포넌트를 언마운트할 때, 정리는 여기서
    return () => clearTimeout(timerId);
  });
  return <p>{counterValue}</p>;
}
컴포넌트가 마운트 해제될 때 타이머를 정리합니다.

2-4 구독 정리

외부 데이터에 대한 구독을 설정시, 컴포넌트 마운트 해제될 때 정리하는 것이 중요합니다. 예를 들어 웹 소켓.

export default function Component() {
  const [url] = useState("");
  useEffect(() => {
    const webSocket = new WebSocket(url);
    // do stuff here

    // clean up when component unmount
    return () => webSocket.close();
  },);

  // ...
}
컴포넌트가 마운트 해제될 때 웹 소켓 연결을 정리합니다.

3. 결론

일부 이펙트는 메모리 누수를 방지하기 위해 정리가 필요할 수 있습니다. useEffect()를 사용하면 구성 요소가 렌더링된 후 다양한 종류의 사이드 이펙트를 수행한 다음 종류에 따라 제거할 수 있습니다.

profile
Adventurer

0개의 댓글