useEffect가 두 번 실행되는 현상을 어떻게 막을 수 있을까요 🤔

sumi-0011·2024년 8월 28일
0

문제 상황 😮

먼저, 다음과 같은 코드를 보시죠

useEffect(() => {;
    console.log('useEffect 실행');
}, []);

이 코드를 실행하면 useEffect가 한 번만 실행될 것으로 예상하게 됩니다.
하지만 실제로 콘솔을 확인해보면, useEffect 실행이 두 번 출력되는 것을 볼 수 있습니다. 왜 이런 일이 발생하는 걸까요? 🤷‍♂️

원인: React.StrictMode 🔍

이 현상은 React 18에서 도입된 Strict Mode 때문에 발생합니다.
React 공식 문서에서는 Strict Mode에 대해 다음과 같이 설명하고 있습니다.

"<StrictMode>를 사용하면 개발 초기에 구성 요소의 일반적인 버그를 찾을 수 있습니다."

"Strict Mode 검사는 개발 중에만 실행되지만, 코드에 이미 존재하지만 프로덕션에서 안정적으로 재현하기 어려울 수 있는 버그를 찾는 데 도움이 됩니다.
Strict Mode를 사용하면 사용자가 버그를 신고하기 전에 버그를 수정할 수 있습니다."

참고 : https://react.dev/reference/react/StrictMode

그래서 useEffect가 두번 실행되는 것을 어떻게 막을 수 있을까요? 💡

1. React.StrictMode 제거

Strict Mode를 제거하면 문제를 해결할 수 있지만, 잠재적 버그를 놓칠 수 있어 권장되지 않습니다.

2. 클린업 함수를 이용한 상태 초기화

간단한 콘솔 출력이나 상태 설정의 경우 두 번 실행되어도 큰 문제가 없습니다.
하지만 API 호출이나 반드시 한 번만 실행되어야 하는 동작이 두 번 실행되면 문제가 될 수 있습니다.

이를 해결하기 위해, useEffect의 클린업 함수에서 이전에 설정된 값을 초기화해야 합니다. 특히 API 요청의 경우, 중복 요청으로 인한 문제를 방지하기 위해 이전 요청을 적절히 처리해야 합니다.

3. AbortController 사용하기

useEffect가 두번 실행되는 것을 막기 위한 방법 중 하나는 AbortController를 사용하는 것입니다.

function Test() {
  useEffect(() => {
    const controller = new AbortController();

    console.log('doSomethingAsync: ');
    doSomethingAsync(
      { value: 'test' },
      { signal: controller.signal }
    ).then((result) => {
      console.log('result: ', result);
    });

    return () => {
      controller.abort();
    };
  }, []);
  return <div></div>;
}
function doSomethingAsync(payload: unknown, { signal }: { signal?: AbortSignal } = {}): Promise<unknown> {
  if (signal?.aborted) {
    return Promise.reject(new AbortException());
  }

  return new Promise((resolve, reject) => {
    const abortHandler = () => {
      reject(new AbortException());
    };

    setTimeout(() => {
      signal?.removeEventListener('abort', abortHandler);
      resolve(payload);
    }, 0);

    signal?.addEventListener('abort', abortHandler);
  });
}

class AbortException extends DOMException {
  constructor() {
    super('Aborted', 'AbortError');
  }
}

이 방법을 사용하면 useEffect는 두 번 실행되지만, 중요한 비동기 작업은 한 번만 완료합니다.

결과 콘솔

![[Pasted image 20240828124528.png]]

AbortController를 이용한 해결 방법의 동작 원리

  1. useEffect 내에서 AbortController를 생성합니다.
  2. doSomethingAsync 함수를 호출할 때 controller의 signal을 전달합니다.
  3. useEffect의 클린업 함수에서 controller.abort()를 호출합니다.

이 방법이 효과적인 이유는 다음과 같습니다.

  • React.StrictMode에서 useEffect가 두 번 실행되더라도, 첫 번째 실행의 클린업 함수가 두 번째 실행 전에 호출됩니다.
  • 첫 번째 실행에서 시작된 비동기 작업은 setTimeout으로 인해 지연됩니다.
  • 두 번째 실행 전에 첫 번째 작업이 abort됩니다.
  • 두 번째 실행에서 시작된 작업만 완료되므로, 결과적으로 한 번만 실행된 것처럼 보입니다.

이렇게 하면 useEffect 자체는 두 번 실행되지만, doSomethingAsync가 취소되지 않고 실행되는 것은 단 한 번입니다.

다만, useEffect를 두번 실행하지 않기위해 사용하기에는 과도한 개발인것 같아요. 🤔

4. useRef 이용하기 ✨

function useEffectOnce(callback: () => void) {
  const ref = useRef(false);

  useEffect(() => {
    if (ref.current) return;
    ref.current = true;

    if (typeof callback === 'function') {
      callback();
    }
  }, []);
}

useEffectOnce를 이용한 해결 방법의 동작 원리

  1. useRef를 사용해 효과 실행 여부를 추적합니다.
  2. useEffect 내에서 ref 값을 확인하여 이미 실행됐다면 바로 반환합니다.
  3. 아직 실행되지 않았다면 ref 값을 true로 설정하고 콜백을 실행합니다.
  4. 빈 의존성 배열([])을 사용해 컴포넌트 마운트 시에만 실행되도록 합니다.

이 방법이 효과적인 이유는 다음과 같습니다.

  • Strict Mode에서 useEffect가 두 번 호출되더라도, ref 값 덕분에 콜백은 한 번만 실행됩니다.
  • useRef를 사용해 컴포넌트 생명주기 동안 값을 유지하므로, 리렌더링에도 안전합니다.
  • 타입 체크를 통해 콜백이 확실히 함수일 때만 실행하므로 타입 안정성이 보장됩니다.

이렇게 하면 useEffect는 여전히 Strict Mode에서 두 번 실행되지만, 우리가 원하는 로직은 딱 한 번만 실행되는 거죠!

사용예시

function MyComponent() {
  useEffectOnce(() => {
    console.log('이 메시지는 단 한 번만 출력됩니다!');
    // 여기에 초기 데이터 로딩이나 이벤트 리스너 등록 같은 작업을 넣으면 됩니다.
  });

  return <div>My Component</div>;
}

useEffectOnce는 AbortController를 사용한 방법에 비해 더 간단하고 직관적이에요.

다만, 모든 상황에 이 훅을 사용하는 것은 주의해야 해야합니다. 때로는 useEffect의 재실행이 필요한 경우도 있을테니까요.


긴 글을 읽어주셔서 감사합니다.
추가적인 질문이나 의견이 있다면 언제든 공유해 주세요! 💬🚀


참고 : https://medium.com/@tangiblej/cancel-a-promise-inside-react-useeffect-12a101606b72

profile
안녕하세요 😚

0개의 댓글