이것이 Effect ..? - NEW 리액트 공식문서

hongregii·2023년 3월 28일
0

아니, 공식문서에서 useEffect가 이렇게 늦게 나온다고? 원래 안그랬잖아...
심지어 useRef보다도 늦게 나오고, 심심지어 둘 다 리액트 밖으로 나가고 싶을 때 이런 탭에 있다니!
의역이다. 원문은 Escape Hatches.

차근차근 Effect가 뭔지 다시 한 번 살펴보자. 새로 바뀐 리액트 공식문서 선생님이 자세히 설명해 줄 것이다.


몇몇 컴포넌트들은 외부 시스템과 동기화 Synchronize 해야 한다. 예를 들어, 리액트 State에 기반해서 리액트가 아닌 컴포넌트를 바꾸고 싶을 때나, 서버와 통신할 때, 컴포넌트가 화면에 나타나면 분석 로그를 띄우고 싶을 때 등.

Effect는 렌더링 잉후에 어떤 코드를 실행하게 해준다. 컴포넌트를 리액트 밖의 어떤 시스템과 동기화 할 수 있게끔...

이번 장에서는..

  • Effect가 뭔지
  • Effect vs 다른 events
  • Effect 선언하기
  • 불필요한 Effect 건너뛰기
  • 개발 환경에서 Effect가 두 번 작동하는 이유, 그리고 고치는 법
    을 배워보자.

Effect, 누구냐 너

Effect까지 가기 전에, 리액트 컴포넌트 내의 두 가지 로직과 친해져야 한다.

  • 렌더링 코드 는 컴포넌트 최상위 레벨에 살고 있다. 여기에서 props, state를 가공하고, 화면에 뿌려줄 JSX를 리턴한다. 렌더링 코드는 순수 함수여야 한다. 수학 공식처럼, 오직 결과를 계산 할 뿐, 다른 것을 하면 안된다.
  • 이벤트 핸들러 는 컴포넌트 안의 함수로, 계산만 해주는 것이 아니라 어떤 일을 한다. 이벤트 핸들러는 input 필드를 업데이트하고, HTTP POST 요청을 submit하고, 다른 화면으로 이동하는 등 로직을 처리한다. 이벤트 핸들러는 버튼 클릭, 타이핑과 같은 특정한 액션에 의한 "부작용 side effects"를 가진다. (부작용 = 프로그램의 state를 바꿈)

이것으로 충분하지 않을 때가 있다. ChatRoom 컴포넌트가 있는데, 화면에 보여지면 무조건 채팅 서버에 연결돼있어야 한다고 치자. 서버 연결은 "순수한 계산"은 아니다. (부작용임!) 따라서 렌더링 도중에는 일어나면 안됨. 거기다 ChatRoom이 화면에 띄워지는 이벤트 (클릭 같은) 도 없다고 한다면?

Effect를 사용하면 이벤트가 아니라 렌더링 그자체에 의해 일어난 부작용을 지정할 수 있다. 채팅방에서 메세지를 보내는 것은 이벤트다. 사용자가 [보내기]버튼을 눌렀을 때 일어나기 때문. 그러나, 서버 연결은 Effect 효과/작용이다. 어떤 상호작용에 의한 일인지에 관계없이 일어나야 하기 때문. Effect는 화면 업데이트의 [커밋] 끝에서 동작한다. 이 때가 외부 시스템 (네트워크, 서드 파티 라이브러리 등)과 리액트 컴포넌트를 동기화시키기가 좋은 때다.

메모

이 공식문서에서 대문자 E로 시작하는 Effect는 리액트의 Effect. 렌더링에 의한 부작용(side effect)이다.
이 문서에서 일반적인 프로그래밍 개념의 이펙트는 "부작용"이라고 부른다.

Effect, 그거 필요합니까?

무지성으로 Effect를 추가하지 마라. Effect가 리액트 밖으로 나가서, 외부 시스템과 동기화시켜야 할 때만 사용해야 한다는 점을 잊지 말아야 한다. 개발자가 작성한 State가 다른 State에 영향을 주고 끝날 때, 이럴 때는 Effect가 필요하지 않을 가능성이 크다.

Effect 써보기

3단계로 사용해보자.

  1. Effect 선언. Default로, Effect는 매 렌더링마다 작동한다.
  2. Effect 의존성 명시. 모든 Effect는 렌더링 이후 필요할 때만 돌아가야 한다. 예를 들어, fade in 애니메이션은 컴포넌트가 나타날 때 만 작동해야 함. 채팅방에 접속/해제는 컴포넌트가 나타나거나 사라질 때만 작동해야 함. 의존성 dependencies를 지정하면 된다.
  3. 필요하면, cleanup을 추가하자. 어떤 Effect는 언제 멈출지, 되돌릴지, 취소할지를 명시해 줘야 함. 예를 들어, "연결"에는 "연결 해제"가, "구독"에는 "구독 취소"가, "가져오기"에는 "취소하기" 또는 "무시하기"가 필요하다. cleanup function을 리턴하면 된다.

자세히 들여다보면,

1단계 : Effect 선언

useEffect를 임포트하자.

import { useEffect } from 'react';

그 다음, 컴포넌트 최상단에서 훅을 호출하고, Effect 안에 코드를 넣자.

function MyComponent() {
  // 최상단에 선언
  useEffect(() => {
    // 여기 넣으면 *매 렌더링 마다* 실행
  });
  return <div />;
}

컴포넌트가 렌더링 될 때마다, 화면이 업데이트 된 뒤에 useEffect안의 코드가 실행된다. 즉, useEffect화면에 렌더링이 반영될 때 까지 어떤 코드가 실행되는 것을 멈춰놓는다!

다음 예시를 보자. <VideoPlayer>라는 리액트 컴포넌트가 있다고 치자. isPlaying prop을 넘겨줘서 재생중인지 멈춰있는지 컨트롤 하고 싶다면,

<VideoPlayer isPlaying={isPlaying} />;
// VideoPlayer.js
function VideoPlayer({ src, isPlaying }) {
  // TODO : isPlaying 이면 뭔가 하는 코드 작성
  return <video src={src} />;
}

이렇게 쓸 수 있을 것이다. VideoPlayer 컴포넌트는 built-in <video> 태그를 렌더링한다.

그러나, 브라우저의 <video> 태그는 당연히 isPlaying 을 가지고 있지 않다! 비디오를 재생/멈춤 하고 싶으면 DOM 요소가 가지고 있는 play()pause()메서드를 사용하는 수밖에 없다. 따라서 isPlaying prop을 video태그와 동기화해줘야 언제 play() 또는 pause()를 해야 할지 컨트롤할 수 있게 되는 것.

외부 API와 연결해야 하므로, 먼저 <video> DOM 노드의 ref를 가져와야 한다.
그 다음에는 렌더링 중에 play()pause()를 호출하고 싶겠지만, 틀렸다.

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  if (isPlaying) {
    ref.current.play();  // 렌더링이 돌아갈 때는 ref를 불러봐야 소용 없다.
  } else {
    ref.current.pause(); // 마찬가지
  }

코드가 터지는 이유? 렌더링이 돌아가는 중에 DOM 요소를 가지고 뭔가를 하려고 했기 때문. 리액트에서 렌더링은 순수 함수여야 한다. JSX를 계산해주는 것만 해야 하고 DOM 조작같은 부작용을 가지고 있으면 안된다.

더욱이, VideoPlayer가 처음 호출됐을 때, DOM은 존재하지도 않는다! 함수 컴포넌트가 JSX를 리턴하기 전까지는 play()pause()를 할 video가 뭔지 리액트가 알 수 없다.

해결책 : useEffect로 감싸서 렌더링 로직에서 제외시키기.

import { useEffect, useRef } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  // 바로여기
  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

Effect 안에 DOM 업데이트를 감싸면, 리액트가 화면 업데이트를 먼저 하고, 그 뒤에 Effect가 돌아간다.

렌더링 과정을 자세히 보면 이렇다.
화면 업데이트video태그가 DOM 안에서 ref같은 props를 잘 먹고 들어가 있음 → Effect 발동isPlaying 에 따라 Effect가 play()pause()를 호출함

완성 코드.

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

이 예시에서의 "외부 시스템"은 바로 Browser Media API다. 레거시한 jQeuery 플러그인같은 것들과 리액트를 연결해서 같이 쓰고 싶을 때도 이런 접근방식을 사용할 수 있다.

실제로는 비디오 플레이어를 컨트롤하는 것이 이것보다 훨씬 더 복잡하다는 것을 알아둬라. play() 호출이 실패할 수 있고, 사용자가 리액트 버튼이 아니라 다른 browser built-in 컨트롤로 재생하려고 할 수도 있고, 그렇다. 이 예시는 예시일 뿐!

useEffect 무한루프 주의보

Default로 Effect는 매 렌더링 이후에 작동한다. 이렇게 코딩하면 무한루프가 생긴다 :

const [count, setCount] = useState(0);
useEffect(()=> {
  setCount(count + 1);
});

Effect는 렌더링의 결과로 일어난다. setState는 렌더링을 유발한다. Effect 안에 즉시 setState를 넣는 것은 보조배터리 선을 자기한테 꽂는 행위와 같다.
Effect 작동 → state 값 설정 → 리렌더링 발생 → Effect 작동 → ....
Effect는 외부 시스템이랑 컴포넌트를 동기화하는데 주로 사용되어야 한다. 외부 시스템이 없고, 어떤 state값에 따라 state를 재설정하고 싶으면 Effect가 필요 없을 확률이 크다.

2단계 : Effect 의존성 지정

Effect는 Default로 매 렌더링마다 일어난다. 개발자가 원하는 작동이 그렇지 않을 경우가 많음.

  • 느릴 때가 있다. 외부 시스템과 동기화 하는 것이 매번 빠릿빠릿 제깍제깍 되는 것이 아님. 필요하지 않은 경우에는 안하는 게 좋을 수 있다. 예를 들어, 키스트로크 마다 서버와 연결하기? 느려짐.
  • 틀렸을 수 있다. 예를 들어, 키스트로크 마다 컴포넌트에 fade-in 애니메이션을 넣는다? "나타나기" 애니메이션은 '나타날 때' 한번만 일어나야 옳다.

가장 흔한 잘못된 예 - console.log 를 잘못 쓴 예시를 보자. 타이핑하면 Effect가 다시 돌아가게 된다.

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      console.log('video.play() 호출');
      ref.current.play();
    } else {
      console.log('video.pause() 호출');
      ref.current.pause();
    }
  }); // useEffect에 의존성 변수 없음.

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
    // 얘는(input) isPlaying 상태랑 아무 상관 없는 놈인데 setState가 들어있어서 타이핑 칠때마다 useEffect가 발동함.
      <input value={text} onChange={e => setText(e.target.value)} /> 
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button> // 버튼 누르면 isPlaying 상태 바꿈.
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

의존성 배열useEffect의 두 번째 인자로 넣어서 지정하면 이렇게 불필요한 Effect 실행을 막을 수 있다. [] 비어있는 배열을 추가해보자.

useEffect(() => {
// Effect 코드
}, []);

에러가 뜬다. React Hook useEffect has a missing dependency: 'isPlaying'.

왜냐. Effect 코드가 isPlaying prop에 의존 하고 있는데, 그 의존성이 지정되지 않았기 때문.
의존성 배열에 isPlaying을 추가하자.

useEffect(() => {
  if (isPlaying) { // 여기서 isPlaying 을 쓰면!
    // ...
  } else {
    // ...
  }
}, [isPlaying]); // 여기서 의존성을 추가해야 함!

의존성이 선언됐으니, 에러가 없다. 리액트에게 isPlaying 값이 이전 렌더링의 값과 똑같으면 Effect를 다시 실행시킬 필요 없다고 알려주는 것임. 이제 타이핑을 쳐도 Effect가 재실행되지 않고, 오직 Play/Pause를 누를 때만 실행된다.

의존성 배열에는 여러 변수가 들어가도 됨. 모든 변수에 변동이 없을 때만 Effect 재실행을 스킵할 것이다. 즉 하나라도 변동이 생기면 Effect는 실행된다. 이 비교는 Object.js 비교를 통해 이루어짐.

주의할 점은 : 의존성을 마음대로 고를 수 없다는 점이다. 리액트가 Effect 안에서 필요하다고 유추한 변수와 의존성 배열이 맞지 않으면 문법 에러가 뜰 것이다! 어떤 Effect 코드가 재실행되지 않게 하려면, Effect 코드 그 자체를 수정해야 한다. 그 변수가 안들어가게끔.

Ref 형은 나가있어

Effect 코드에 ref가 들어가는데, 의존성 배열에는 없다. Ref값은 리렌더링이 일어나도 똑같이 유지되기 때문에, 변할 일이 없다. 리액트가 알아서 ref는 제외하고 생각함.
단, 부모 컴포넌트로부터 props로 ref가 내려왔으면 의존성 배열에 추가해줘야 함. 부모단에서 ref 안에 다른 값 넣어줬을 수 있기 때문.

3단계 : 필요시 cleanup 함수 추가

다른 예시를 보자. 나타날 때마다 서버와 연결해야 하는 ChatRoom 컴포넌트가 있다. createConnection() API가 connect()disconnect() 메서드를 리턴한다고 한다.
useEffect를 사용해보자.

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

모든 리렌더링마다 서버에 연결하면 느릴 것이 분명. 의존성 배열을 추가하자.

useEffect(() => {
  const connection = createConnection();
  connection.connect();
}, []);

Effect 코드는 아무 props나 state를 사용하지 않기 때문에 의존성 배열은 []. 리액트에게 컴포넌트가 "마운트 될 때만 실행해줘잉"이라고 알려주는 것이다. - 즉 화면에 처음 나타날 때만.

그런데, 코드를 실행해보면 Connecting...이라는 메세지가 콘솔에 두번 뜨는 것을 확인할 수 있다!? (connection.connect() 코드 안에 console.log(Connecting...)이 들어있다고 치자)

마운트 될 때만 실행되면 console.log(Connecting...) 가 한 번만 떠야 하는 것 아님? 사실 리액트는 개발 모드에서만 Effect를 두 번 실행시킨다 ㅋㅎㅋㅎ. 이 얘기는 조금 밑에서 하자.

Effect 코드를 자세히 보면, disconnect 하는 코드가 없다. 사용자가 처음 화면에 컴포넌트를 띄워서 (마운트) 서버에 연결한 뒤, 다른 화면으로 나갔다가 다시 그 컴포넌트를 띄웠다고 치자. disconnect가 없어서 연결이 두 번 일어난다. 이런 자잘한 오작동은 수작업 테스트코드 없이 탐지하기 어려운데, 리액트는 개발자들이 이런 현상 때문에 고생할까봐 일부러 개발 모드에서 Effect를 두 번 실행시켜 준다고 한다..

아무튼, 이런 현상 (연결만 두번 되기) 을 막는 방법은? 컴포넌트가 화면에서 내려갈 때 disconnect 해주는 코드를 useEffectreturn 안에 추가하면 된다. 이것이 cleanup function.

useEffect(() => {
  const connection = createConnection();
  connection.connect();
  return () => { // return 으로 cleanup func
    connection.disconnect();
  };
}, []);

Effect가 다시 돌아가기 전에 매번 cleanup function이 호출되고, 컴포넌트가 unmount (화면에서 내려가기) 될 때 마지막으로 호출된다.
위 코드를 개발 환경에서 실행하면 console은 이렇다:

"Connecting..."
"Disconnected."
"Connecting..."

디버깅 용도다. 리액트가 일부러 unmount 하고 remount 시킴. 그래서 그 이전과 이후에 차이가 있는지, 앱이 고장나지는 않았는지 확인하라는 것이다~

Build 이후 production 환경에서는 "Connecting..."이 한 번만 뜬다. Strict Mode를 끄면 개발 환경에서도 한 번만 뜨긴 하지만, 리액트는 그렇게 하는 것을 추천하지 않는다. Remount 행동은 cleanup이 필요한 Effect를 찾아내는데 좋다.

(개발 환경에서) Effect 동작이 이상하잖아요

리액트가 개발 환경에서 두 번 마운트 하는 것은 정상이다. 제대로 된 질문은 이것일 것이다 : Remount 이후에 잘 작동하도록 Effect를 수정하는 방법은?

보통은 cleanup function이다. Effect가 하던 작동을 멈추거나 취소시켜주기 때문.

그러니까 즉, 개발 환경에서는 Effect가 재실행될 때마다 정리(cleanup) 함수가 호출되는 것이 일반적이며, 이 때문에 Effect가 두 번 실행될 수 있다. 하지만 이러한 경우에도 사용자가 그 차이를 느낄 수 없도록 해야 한다.

대부분의 Effect는 이렇게 쓰여질 것이다 :

리액트 밖 위젯 컨트롤의 경우

지도같이 리액트 밖 UI 위젯을 추가해야 할 때가 있다. 지도 컴포넌트에 setZoomLevel() 메서드가 있는데, 리액트 코드의 zoomLevel 상태랑 동기화시키고 싶다고 하자. Effect는 이렇게 짤 것이다.

useEffect(()=> {
  const map = mapRef.current;
  map.setZoomLevel(zoomLevel);
}, [zoomLevel]);

cleanup은 필요 없다. 개발 환경에서 Effect가 두 번 호출된다. 그러나 같은 값으로 setZoomLevel을 호출하는 것은 아무 작용을 안함. 개발 환경에서 아주 조금 느려질 수 있어도, prod 환경에서는 리마운트 안되니까 괜찮다.

그런데 두 번 호출하면 안되는 API들이 있다. <dialog> 요소는 built-in인데, showModal이라는 메서드가 있다. 모달은 두 번 연속으로 열지 못함. 한 번 열면 한 번 닫아줘야 한다. 이럴 때는 cleanup function을 써서 닫아줘라.

useEffect(() => {
  const dialog = dialogRef.current;
  dialog.showModal();
  return () => dialog.close(); // 클린업 팡션!
}, []);

개발 환경에서는 Effect가 showModal()을 호출한 직후에 close() 하고, 다시 showModal()할 것이다. prod 환경에서는 showModal() 한번만 할 것임.. 둘이 동작이 결국에는 같아야 옳게 된 코드라는 말이다!

이벤트 구독의 경우

Effect가 어떤 것을 구독할 때는 cleanup function에서 반드시 구독을 해제해줘야 한다.

useEffect(() => {
  function handleScroll(e) {
    console.log(window.scrollX, window.scrollY);
  }
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll); // 구독 해제!
}, []);

이벤트를 구독한다는 표현이 생소한데, addEventListener()라고 생각하면 될 듯 하다. cleanup 함수를 리턴하지 않으면, 이벤트리스너가 쌓일 수도? 동작이 두배, 세배로 이상해질 수 있다.

애니메이션의 경우

Effect로 애니메이션을 추가할 때는 cleanup 함수가 animation 값들을 초기값으로 돌려줘야 한다.

useEffect(() => {
  const node = ref.current;
  node.style.opacity = 1; // 애니메이션 등록!
  return () => {
    node.style.opacity = 0; // 초기화
  };
}, []);
  • 개발 환경 : opacity 101
  • prod : opacity 01

데이터 fetch의 경우

Effect가 무언가를 fetch할 경우, cleanup 함수는 fetch를 중단하거나 fetch 결과를 무시해야 한다!!!

가장 많이 사용하는 경우일 텐데..

useEffect(() => {
  let ignore = false; // 지역변수로 선언

  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  }

  startFetching();

  return () => {
    ignore = true;
  };
}, [userId]);

네트워크 요청을 "되돌리기" 할 수는 없는 노릇. 그러나 cleanup 함수는 그 이미 끝난(중요하지 않아진) fetch가 더 이상 앱에 영향을 주지 않게끔 확실히 처리해야 한다. userId가 "Alice"에서 "Bob"으로 바뀌면, cleanup 함수는 "Bob" 이후에 온 "Alice" resoponse를 무시하게 만든다.

개발 환경에서 Network 탭에 fetch가 두 개 있을 것이다. 문제 없다. 첫 번째 Effect는 즉시 cleanup 되기 때문에 ignore값이 true가 됨. 추가 요청이 있어도 if (!ignore) 여기서 체크하기 때문에 state에 영향을 안줄 것.

prod 환경에서는 요청이 한번이다. 개발 환경에서 네트워크 요청이 두 번 가는 게 짜증나면, 요청을 useEffect에 넣지 말고 request를 한 번만 보낸 뒤 캐싱하는 다른 방법을 써라. 예컨대

const todos = useSomeDataLibrary(`/api/user...`);

캐싱은 좋은 방법이다. 사용자가 [뒤로가기]를 눌렀다 다시 돌아와도 요청을 또 보내는 게 아니라 저장된 캐시에서 가져오게 된다. 이러한 캐시를 직접 만들어도 되고, 다른 좋은 라이브러리들을 사용해도 된다.

Effect fetch의 대안

Effect 안에 fetch를 넣으면 생기는 단점들 :

  • Effect는 서버에서 실행되는 게 아님!
    클라이언트 컴퓨터가 리액트 JS를 다 다운받고 앱 렌더링 다하면 이제야 또 서버에서 데이터를 받아와야 함. 그닥 효과적이지는 않다.
  • "네트워크 폭포수 도표 Network Waterfalls.
    부모 렌더링 → 데이터 fetch → 자식 렌더링 → 데이터 fetch ... 네트워크가 느린 경우 한번에 fetch하는 것보다 느릴 수 밖에.
  • 너 캐싱 / preload 안하지?
    언마운트 / 마운트 될 때마다 fetch하는 것은 비효율적이다! 캐싱은 CSR의 장점
  • 충분히 에르고노믹하지 않고 있어
    fetch를 자꾸 쓰는 것은 예쁘지 않다. race condition (동시성 제어 문제) 해결하기 위해서 비슷한 보일러플레이트 코드를 자꾸 작성하게 되는데, 좋지 않다고 함.

리액트에만 적용되는 이야기도 아니다. 어떤 라이브러리든, fetching 에는 다 통하는 이야기. 라우팅처럼 fetching 잘하는 것은 그닥 대단한 일은 아니기 때문에,

아래 방식을 사용하길 권한다 :

  • Next.js / Remix 같은 Framework의 내장 툴을 잘 사용해라 (ㅋㅋㅋ)
  • 클라이언트 사이드 캐시 사용하거나 만들기. 리액트 쿼리 React Query, useSWR, React Router v6.4+같은 옵션이 있다. 직접 만들어도 좋다.

싫으면 그냥 Effect에 넣으라고 한다 (...)

분석 보내기의 경우

이 코드는 페이지 방문 시, 분석 내용을 POST로 전송한다.

useEffect(() => {
  logVisit(url); // POST 요청
}, [url]);

개발 환경에서 logVisit는 url이 바뀔 때마다 두 번씩 호출될 것이다. 이것을 고치고 싶겠지만, 리액트는 걍 냅두는 것을 추천한다. 이전의 다른 예시들과 같이, 사용자는 어차피 다른 점을 못 느끼기 때문. 실질적으로, logVisit은 개발 환겨에엇 아무것도 하면 안된다. 왜냐? log 분석을 개발 환경에서 왜함. 어차피 리액트 코드에서 ctrl + s 를 누를 때마다 컴포넌트가 리마운트되기 때문에, 개발 환경에서는 두번 log가 찍히는건 마찬가지다.

production 에서는 두 번 일어나지 않는다.

디버깅을 하고 싶으면, production 모드로 빌드해서 staging 환경에서 배포해보라고 한다. 또는 Strict mode를 잠깐 끄고 하라고 한다.

Effect가 아닌 경우 : 앱 처음 실행할 때

어떤 로직은 앱이 처음 시작될 때에만 한 번 실행돼야 한다. 이런 애들은 컴포넌트 밖에 놓을 수 있다 :

if (typeof window !== 'undefined') { // 브라우저에서 실행중인지 확인하는 조건문
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // 어쩌구저쩌구
}

브라우저가 페이지를 로드하기 전에 단 한번 돌아가게 할 수 있다.

Effect가 아닌 경우 : 상품 구매하기

cleanup 함수를 만든 뒤에도, Effect가 두 번 실행되는 현상을 막지 못할 때가 있다. 예를 들어, Effect가 상품을 구매하는 POST 요청을 보내는 경우가 있다.

useEffect(()= > {
  // 땡! 개발 환경에서는 두번 실행된다고 ~~
  fetch('/api/buy', { method : "POST" });
}, []);

두 번 구매하고 싶지는 않겠지. 그래서 이 로직을 Effect 안에 넣으면 안되는 것임! 사용자가 다른 페이지에 갔다가 다시 돌아온다면? Effect가 실행되고 또 구매할 것이다. 이런 거는 페이지에 visit할 때마다 실행시키면 안된다. 버튼 클릭 하면 구매하게 하라고~~

같이쓰기는 다음 문서에 따로 하겠다..

profile
잡식성 누렁이 개발자

0개의 댓글