I code, therefore I exist.

웹 프론트 엔드 개발을 공부하고 있는 Ocean이라고 합니다. 만나서 반갑습니다.

WEB/REACT

useEffect

Ocean 2024. 6. 18. 19:41

useEffect란?

useEffect는 외부 시스템과 컴포넌트를 동기화하는 React Hook입니다.

 

React의 함수형 컴포넌트는 순수 함수를 지향합니다. 여기서 순수 함수란 같은 입력이 주어졌을 때 언제나 같은 출력을 반환하는 함수를 말하며, React의 함수형 컴포넌트의 순수성같은 props와 같은 state 일 때, 언제나 같은 출력을 기대하는 것을 말합니다. 컴포넌트를 엄격하게 순수함수로 작성하면 코드베이스가 점점 커지더라도 예상밖의 동작이나 당황케하는 버그를 피할 수 있습니다.

 

하지만 React의 함수형 컴포넌트도 외부 환경에 영향을 받을 수 있습니다. 예를 들면 서드 파트 라이브러리를 연결하거나, API Request에 따른 데이터를 렌더링하거나, 데이터베이스를 연결하는 동작들이 있을 수 있습니다. 

이러한 외부 요인의 상호작용을 통해 발생할 수 있는 불확실성을 Side Effect라고 표현합니다. useEffect는 이러한 Side Effect를 관리하기 위해 사용하는 Hook입니다.

 

기본 규칙

컴포넌트의 최상위 레벨에서 useEffect를 호출하여 Effect를 선언할 수 있습니다.

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

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

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

 

매개변수

1. setup 콜백 함수 (Required) : Effect의 로직이 포함된 함수입니다. 설정 함수는 선택적으로 clean up 함수를 반환할 수 있습니다. React는 컴포넌트가 DOM에 추가된 이후에 설정 함수를 실행합니다. 의존성의 변화에 따라 컴포넌트가 리렌더링이 되었을 경우, React는 이전 렌더링에 사용된 값으로 정리 함수를 실행한 후 새로운 값으로 설정 함수를 실행합니다. 컴포넌트가 DOM에서 제거된 경우에도 정리 함수를 실행합니다.

 

2. dependencies, 의존성 배열 (Optional) : 설정 함수의 코드 내부에서 참조되는 모든 반응형 값들이 포함된 배열로 구성됩니다. 반응형 값에는 props와 state, 모든 변수 및 컴포넌트 body에 직접적으로 선언된 함수들이 포함됩니다. lint가 React 환경에 맞게 설정되어 있을 경우, Lint는 모든 반응형 값들이 의존성에 제대로 명시되어 있는지 검증할 것입니다. React는 의존성들을 Object.is 비교법을 통해 이전 값과 비교합니다. 의존성 값들이 이전 값과 비교했을 때 다를 경우 해당 설정 함수를 다시 실행합니다.

반응형 값이란? State, Props, Context와 같이 UI 렌더링의 트리거가 되는 값을 말합니다.

 

Dependencies?

useEffect의 두번째 인수인 의존성배열은 "선택"할 수 없다는 점에 유의해야 합니다. Effect 코드에서 사용하는 모든 반응형 값은 의존성으로 선언되어야 합니다. 만약 해당 반응형 값들이 Effect 코드 안에 포함되지 않았다면, 린터가 경고 메세지를 표시합니다.

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
  // ...
}

 

위 코드의 serverUrl과 roomId는 각각 State와 Prop이기 때문에 Effect 코드 안에서 사용되었다면 의존성에 추가해야 합니다.

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

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ 모든 의존성이 선언됨
  // ...
}

 

serverUrl을 상수로 재정의하면 더 이상 반응형 값이 아니기 때문에 의존성에서 제거할 수 있습니다.

여기서 Effect 코드 내에 모든 의존성을 제거한다면 의존성배열을 비울 수 있습니다.

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

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ 모든 의존성이 선언됨
  // ...
}

 

의존성이 모두 제거된 빈 배열을 전달한 Effect 코드는 해당 컴포넌트가 화면에 처음 Mount 될 때만 실행하며, 해당 컴포넌트의 set함수가 동작하여 리렌더링되도 더이상 Effect 코드의 설정 함수를 실행하지 않습니다.

또한 의존성배열은 Optional한 매개변수이기 때문에 전달하지 않을수도 있습니다.

위에서 말한 의존성배열을 선택할 수 없다는 의미는, 만약 Effect 코드 안에서 반응형 값이 포함되어 있다면 그 반응형 값을 의존성에서 제외할 수 없다는 의미입니다. 만약 반응형 값이 존재하지 않는다면 의존성 배열을 비워두거나, 아예 전달하지 않을 수 있습니다.

 

따라서 useEffect의 의존성 배열의 상태는 세 가지 가능성을 가지고 있습니다.

1. 의존성 배열을 아예 전달하지 않는다.

function App() {
  const [count, setCount] = useState(0);
  const handleCount = () => {
    setCount(count + 1);
  };
  useEffect(() => {
    console.log('Effect Run!!');
  }); // 컴포넌트 마운트 및 리렌더링 시에 실행
  return (
    <>
      <h1>{count}</h1>
      <button onClick={handleCount}>Count up!</button>
    </>
  );
}

 

useEffect에 두번째 매개변수를 아예 전달하지 않을 경우, 해당 Effect 코드 안에 설정 함수는 컴포넌트가 Mount 될 때와 리렌더링이 일어날 때 모두 실행됩니다.

2. 빈 의존성 배열을 전달한다.

  useEffect(() => {
    console.log('Effect Run!!');
  }, []); // 컴포넌트가 마운트 될 때만 실행, 리렌더링 시에는 실행하지 않는다.

 

빈 의존성 배열을 전달할 경우, 해당 컴포넌트가 Mount될 때만 실행하며 리렌더링이 일어날 때는 실행하지 않습니다.

3. 의존성 배열의 원소를 추가하여 전달한다.

  useEffect(() => {
    console.log('Count is Changed!! ', count);
  }, [count]); // count가 truthy할 때와, 변경될 때 마다 실행

 

위와 같이 Effect 코드 안에 반응형 값인 count state를 사용했으면 반드시 의존성 배열 안에 해당 변수를 넣어야 합니다. 이제는 count의 값이 존재할 때, 즉 truthy할 때 및 변경될 때 Effect 코드 내부의 설정 함수가 작동합니다. 만약 Effect 코드 내부에서 count 변수를 사용하는데 의존성 배열을 비워놓으면, count는 변경되어도 해당 Effect 코드가 실행되지 않아서 Effect 코드 안에 실행되는 console.log의 count와 실제 count가 일치하지 않는 상황이 발생하기 때문에 반드시 의존성 배열 안에 설정 함수에 사용된 반응형 값을 추가해 주어야 합니다.

 

Effect에서 이전 state를 기반으로 state 업데이트하기

만약 Effect에서 이전 state를 기반으로 state를 업데이트하려면 문제가 발생할 수 있습니다.

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1); // 초마다 카운터를 증가시키고 싶습니다...
    }, 1000)
    return () => clearInterval(intervalId);
  }, [count]); // 🚩 ... 하지만 'count'를 의존성으로 명시하면 항상 인터벌이 초기화됩니다.
  // ...
}

 

count를 의존성에 추가하면 해당 Interval이 동작할 때 마다 count의 변경이 발생하여 Effect 코드를 다시 실행하게 됩니다.

이러한 현상을 방지하려면 state 변경 함수를 업데이터 함수로 전달하면 됩니다.

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ State 업데이터를 전달
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ 이제 count는 의존성이 아닙니다

  return <h1>{count}</h1>;
}

 

객체 의존성 주의하기

Effect가 렌더링 중에 생성된 객체나 함수에 의존하는 경우 의도와 다르게 동작할 수 있습니다.

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

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

  const options = { // 🚩 이 객체는 재 렌더링 될 때마다 새로 생성됩니다
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options); // 객체가 Effect 안에서 사용됩니다
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // 🚩 결과적으로, 의존성이 재 렌더링 때마다 다릅니다
  // ...

 

options 객체는 해당 컴포넌트가 재 렌더링 될 때 마다 새롭게 생성되기 때문에, 해당 객체 내부의 값들이 같더라도 Effect가 실행되는 부작용이 있습니다. 위와 같은 경우 options 객체를 Effect 내부에서 선언하는 것이 해결 방법이 될 수 있습니다.

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

 

함수 의존성 주의하기

함수 또한 객체와 마찬가지로 렌더링 중에 선언되는 함수의 경우 Effect의 의존성으로 부여하면, 해당 Effect 코드는 결과적으로 해당 컴포넌트가 재생성될 때 다시 실행됩니다. 해당 문제도 위의 객체와 마찬가지로 함수의 선언부를 Effect 코드 내부로 옮기면 문제가 해결됩니다.

  useEffect(() => {
    function createOptions() {
      return {
        serverUrl: serverUrl,
        roomId: roomId
      };
    }

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

 

 


 

해당 게시글은 리액트 공식 문서를 정리한 글입니다. 추가적인 내용은 아래의 링크를 참고해 주세요. :)

https://ko.react.dev/reference/react/useEffect

 

useEffect – React

The library for web and native user interfaces

ko.react.dev

 

'WEB > REACT' 카테고리의 다른 글

useMemo  (0) 2024.06.19
useCallback  (0) 2024.06.18
useRef  (0) 2024.06.17
useState  (0) 2024.06.16
REACT란 무엇인가?  (0) 2023.07.08