재밌게 합시다

기록하기, 가시화하기

React

useEffect 제대로 이해하기

은또디 2022. 9. 21. 09:54

https://beta.reactjs.org/learn/synchronizing-with-effects
위 글을 제가 이해한 바대로 정리한 내용입니당 ‘٩꒰。•◡•。꒱۶’
React Docs BETA 라는 사이트인데, 기존 공식 문서보다 훨씬 이해하기 쉽고 친절하게 잘 되어 있어 React의 기본 원리를 이해하기에 좋습니다! React를 대강 쓸 줄은 아는데 깊이가 부족하다고 느껴지시는 분들에게 추천합니당 왕 추천 ❤️‍🔥

👩🏻‍💻 Effect란?

two types of logic inside React components

  • Rendering code: describes the UI, must be pure
  • Event handlers: caused by a specific user action

Effect는 둘 중 어떤 로직에도 속하지 않는다.

 

(예시) ChatRoom
ChatRoom component를 예시로 들어보자.


해당 컴포넌트는 사용자의 화면에 나타날 때마다 chat server에 연결을 해야 함! 그러나 이 작업은 pure하지 않기 때문에 Rendering code로 작성할 수도 없고, 유저의 액션 없이 이뤄져야 하기 때문에 Event handler로 작성할 수도 없다. 👉🏻 이게 바로 Effect의 존재 이유다.

Effects let you specify side effects that are caused by rendering itself, rather than by a particular event. Effects run at the end of the rendering process after the screen updates. Effects run as a result of rendering.

 

채팅 메시지 전송: 유저가 전송 버튼을 클릭햇을 때 발생해야 하므로 event에 해당
채팅 서버에 연결(setting up a server condition): effect에 해당

Effect를 사용해야 하는 상황

다음과 같은 상황에 Effect를 사용한다.

  • browser APIs
  • third-party wedgets
  • network

Effects should usually synchronize your components with an external system.
If there’s no external system and you only want to adjust some state based on other state, you might not need an Effect.

useEffect와 무한 렌더링

아래 코드는 무한 렌더링을 발생시킨다.

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

By default, Effects run after every render.


render는 아래와 같은 경우에 발생한다.

  • state가 바뀌었을 때
  • props가 바뀌었을 때
  • 부모 컴포넌트가 리렌더링 되었을 때

setCount(count+1)은 리렌더링을 발생시킨다. 리렌더링 발생 시 useEffect 내의 코드가 실행되고, state가 변경되므로 리렌더링이 일어나고, 리렌더링 후 다시 useEffect 코드가 실행된다.
(The Effect runs, it sets the stat✍🏻e, which cause a re-render, which causes the Effect to run, it sets the state again, this causes another re-render, and so on.)

 

다시 한 번 명심할 것!

If there’s no external system and you only want to adjust some state based on other state, you might not need an Effect.

✍🏻 Effect 작성하기

✔️ Step 1: Declare an Effect

(예시) 비디오 플레이어

아래와 같은 VideoPlayer 컴포넌트가 있다고 가정해보자.

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);
  return <video ref={ref} src={src} loop playsInline />;
}

우리가 하고 싶은 것
: isPlaying 이라는 React prop에 따라 external system(browser API)인 video tag의 재생 여부를 컨트롤하는 것! video tag의 재생 여부를 컨트롤 하기 위해서는 브라우저 API인 play(), pause() 를 사용해야 한다. => Effect로 작성해야 한다. 즉, React component와 external system을 synchronize하는 것


이때 아래와 같이 코드를 작성하면?

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

  if (isPlaying) {
    ref.current.play();  // Calling these while rendering isn't allowed.
  } else {
    ref.current.pause(); // Also, this crashes.
  }

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

컴포넌트 내에 작성한 코드는 rendering 중에 실행된다. 리액트에서 렌더링은 pure calculation of JSX여야 하므로 위 코드는 에러를 발생시킨다. 렌더링 중에 DOM node(ref.current)에 접근하고자 하는 것이기 때문이다.

 

결정적으로 만약 VideoPlayer 컴포넌트가 처음 호출된 것이라면, 해당 DOM node는 존재하지도 않을 것이다. 존재하지 않는 DOM node에 대해 메소드를 호출하려는 시도이므로 당연히 에러가 발생한다. 따라서 우리는 해당 작업을 Effect로 작성해야 한다. Effect로 작성할 경우 순서가 다음과 같다.

 

  1. VideoPlayer component renders
    React will update the screen
    = ensure the <video> tag is in the DOM with the right props.
  2. React will run your Effect
    Effect will call play() or pause() depending on the value of prop.
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 />;
}

✔️ Step 2: Specify the Effect dependencies

By default, Effects run after every render.


props가 바뀔 때마다, state가 바뀔 때마다, 부모 컴포넌트가 리렌더링될 때마다 React는 Effects를 수행한다. 이때 dependency 배열을 작성해줌으로써, 불필요한 Effect의 re-run을 방지할 수 있다!

Specifying [isPlaying] as the dependency array tells React that *it should skip re-running your Effect if isPlaying is the same as it was during the previous render. *

🙋🏻‍♀️ "dependency values 값이 이전 render의 값과 똑같으면, render가 일어나도 해당 Effect는 re-run 하지 말아주세요!!" 라는 뜻이 된다. 이때 dependencay valudes의 비교는 Object.js comparison을 통해 이뤄진다.

 

아래와 같이 dependency array에 isPlaying 을 추가해주면, isPlaying prop의 값이 바뀌었을 때에만 play()/pause()가 호출된다. 다른 prop인 text가 변경되었을 때에는 Effect의 re-run이 발생하지 않는다.

 

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

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

  return 
  <div>
    <video ref={ref} src={src} loop playsInline />
    <p>{text}</p>
  </div>
}

+) dependency 내 입맛대로 작성하면 안돼?

useEffect를 쓰다보면, eslint에서 dependency array에 추가하라는 경고가 뜨는데, 나는 추가하고 싶지 않은 경우가 있다. Effect 코드 내에서 사용되지만 해당 값이 바뀌어도 Effect를 re-run할 필요가 없는 경우가 그렇다. 이러한 경우에 대해 React DOCs는 아래와 같이 명시해 두었다.

Notice that you can’t “choose” your dependencies. If your Effect uses some value but you don’t want to re-run the Effect when it changes, you’ll need to edit the Effect code itself to not “need” that dependency.

업데이트 시 re-run을 원치 않는 값이 Effect에 포함되어 있을 경우, dependency array에서 해당 값을 뺄 게 아니라 해당 값을 포함하지 않도록 Effect code 자체를 수정해라.

그렇다고 합니당. 앞으로는 명심하겠읍니다.

✔️ Step 3:Add cleanup if needed

cleanup function은 Effect가 re-run되기 직전과, unmount될 때 호출된다.

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

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
  }, []);
  return <h1>Welcome to the chat!</h1>;
}

위처럼 코드를 작성하면 페이지가 이동되고, 돌아올 때마다 mount/unmount가 반복되면서 connection이 쌓이게 된다. 이럴 때 cleanup function을 작성해주어야 한다.

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

The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and an effect → cleanup → effect sequence (as you’d see in development).

 

사실 잘 이해는 안가지만 대강 'cleanup function을 잘 사용해서 사용자로 하여금 Effect가 처음 run된건지 re-run된 건지 모르게끔 해라'라는 뜻 같다.'