NOTION 정리/REACT

UseEffect(Hook)

dev_sr 2023. 11. 6. 23:19

UseEffect 

Effect HookUseEffect는 함수 컴포넌트 내에서 side effects를 수행할 수 있게 해줌 

class 의 componentDidMount  componentDidUpdate, componentWillUnmount와 같은 목적으로 제공되지만, 하나의 API로 통합된 것

  • Hook: 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 “연동(hook into)“할 수 있게 해주는 함수
  • side effects(짧게 effects): 컴포넌트 안에서 데이터를 가져오거나 구독하고 DOM을 직접 조작하는 작업  
  • componentDidMount: 컴포넌트를 만들고, 첫 렌더링을 마친 이후 실행
  • componentDidUpdate: 리렌더링을 완료한 후 실행(render가 업데이트될 때마다 실행)
  • componentWillUnmount: 컴포넌트를 DOM에서 제거할 때 실행

 

사용법

1. 리렌더링될 때마다 실행

useEffect(() => {
  console.log();
});

 

 

2. 컴포넌트가 mount 됐을 때 (처음에만 실행)

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

 

 

3.컴포넌트가 업데이트될 때 (props, state가 변경될 때)

useEffect(() => {
  console.log(count);
},[count]);

 

처음 마운트 될 때도 실행되므로 업데이트될 때만 실행시키고 싶으면 다음 방법 사용 가능

const ref = useRef(false);
useEffect(() => {
  if(!ref.current){
    ref.current = true;
  } else {
    //todo
    console.log(count);
  }
},[count]);

 

 

4. 컴포넌트가 언마운트될 때, 업데이트 되기 직전에 실행)

useEffect(() => {
  console.log();
  
  //clean up, 렌더링이 다시 일어날 때 실행
  return () => console.log();
},[]);

 

컴포넌트가 언마운트될 때만 실행하는 경우 deps 에 빈배열을 넣고

특정 값이 업데이트 되기 직전 clean up 함수를 실행하고 싶다면 deps에 넣고 싶은 값을 넣어줌

 

clean up 함수를 사용하게 되면 작동 순서는 다음과 같다.

 re-render -> 이전 effect clean up -> effect 

 

api 가 두번씩 호출될 때

react18 + strict mode에서 api 호출(data fetching) 시 두번씩 호출되는 경우가 있는데

이것은 react 18은 페이지 이동 후 다시 돌아왔을 때 앱이 망가지는 부분이 없는지 확인하기 위해

개발모드에서 한 컴포넌트를 두번 렌더링해서 useEffect 가 두번 호출되기 때문이라고 한다.

 

clean up 함수를 작성하게 되면 useEffect가 두번 호출되어도

useEffect가 두번 호출되는 것을 느낄 수 없다고 함

 

 

❗불필요한 렌더링을 일으키는 useEffect 피하기(공식문서 일부 내용)

상태를 업데이트할 때 React는 먼저 화면에 무엇을 표시해야 하는지 계산하기 위해 컴포넌트 함수를 호출한다.

그런 다음 React는 이러한 변경 사항을 DOM에 "커밋"하여 화면을 업데이트하고 그런 다음 React는 Effects를 실행한다.

Effect가 즉시 상태를 업데이트하는 경우, 이러한 과정이 처음부터 다시 시작된다.

 

불필요한 렌더링 단계를 피하려면 컴포넌트의 최상위 수준에서 모든 데이터를 변환해야 한다.

해당 코드는 props나 state 가 변경될 때 다시 실행하게 된다.

 

 

props나 state에 기반한 업데이트

아래 코드에서 fisrtName과 lastName이 변경될 때마다 fullName이 업데이트되길 원하는 경우

아래와 같이 사용하는 것은 비효율적이다.

fullName에 대한 오래된?(stale) 값을 이용해서 전체 렌더링을 수행한 다음

즉시 업데이트된 값을 이용하여 다시 렌더링한다.

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 🔴 Avoid: redundant state and unnecessary Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

 

 

기존 props나 state 로부터 계산할 수 있는 것은 상태에 넣지 말아야 한다.

대신 렌더링 중에 계산이 가능하다.

이러면 코드가 더 빨라지고 간단해지고(추가적인 연쇄(cascading) 업데이트를 피할 수 있음)

서로 다른 변수가 동기화되지 않아서 발생하는 버그를 피할 수 있다.

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
  // ...
}

 

 

비용이 많이 드는 계산 캐싱하기

아래 코드에서 todos와 filter가 변경될 때마다 visibleTodos가 변경되고 있다.

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // 🔴 Avoid: redundant state and unnecessary Effect
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}

 

 

getFilteredTodos가 느리지 않은 함수일 경우, 아래 코드로 바꾸는 것이 좋다.

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ This is fine if getFilteredTodos() is not slow.
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}

 

 

getFilteredTodos 가 하는 일이 많거나 느릴 경우, useMemo를 사용할 수 있다.

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  const visibleTodos = useMemo(() => {
    // ✅ Does not re-run unless todos or filter change
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);
  // ...
}

//한줄로 쓰기
import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ Does not re-run getFilteredTodos() unless todos or filter change
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}

 

 

 

props가 변경될 때 모든 state 리셋하기

아래 코드 예시인 ProfilePage에서는 userId를 props로 받고 userId 별로 comment를 작성할 수 있는데, 

어느 날 한 userId에서 다른 userId로 이동 시 comment가 reset 되지 않은 것을 발견하여

아래와 같이 코드를 작성하게 됨

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // 🔴 Avoid: Resetting state on prop change in an Effect
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

이 방법은 먼저 오래된 값으로 렌더링된 다음 다시 렌더링되기 때문에 비효율적이고, 

댓글 UI가 중첩되어 있는 경우 ProfilePage 내부의 모든 상태를 지우려면 각 컴포넌트에서 이 작업을 수행해야 한다.

 

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  const [comment, setComment] = useState('');
  // ...
}

리액트는 동일한 컴포넌트가 동일한 위치에 렌더링될 때 상태를 보존한다.

각 사용자 프로필을 컴포넌트로 분리하고 userId를 Profile 컴포넌트에 key로 전달함으로써 

두 개의 userId를 가진 두 개의 Porfile 컴포넌트를 서로 다른 컴포넌트로 취급하고 어떤 상태도 공유하지 않게 한다.

key가 변경될 때마다 리액트는 DOM을 다시 생성하고 Profile 컴포넌트와 해당 모든 자식의 상태를 재설정한다.

(프로필 간 이동 시 comment를 따로따로 리셋해줄 필요 없음)

 

 

props가 변경될 때 state 일부 조정하기

이번엔 items가 변경될 때마다 selection을 null로 리셋하려고 한다

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // 🔴 Avoid: Adjusting state on prop change in an Effect
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

 

items가 변경될 때마다 Lists와 하위 컴포넌트는 오래된 selection 값으로 먼저 렌더링되고,

리액트는 DOM 업데이트 및 Effect를 실행한다.

마지막으로 setSelection(null)은 List와 하위 컴포넌트를 다시 리렌더링하고, 전체 프로세스를 다시 진행시킨다.

 

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // Better: Adjust the state while rendering
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

위 코드는 렌더링 중에 직접 setSelection을 실행시킨다.

또한, items 값을 prevItems에 담고 현재 Items값과 비교함으로써 리렌더링을 줄일 수 있다.

 

 

event handler 간 로직 공유

유저가 장바구니에 product을 담을 때 알림(모달)을 보여주는 로직이 있다.

function ProductPage({ product, addToCart }) {
  // 🔴 Avoid: Event-specific logic inside an Effect
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name} to the shopping cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }

  function handleCheckoutClick() {
    addToCart(product);
    navigateTo('/checkout');
  }
  // ...
}

 

evnet(click) handler 를 통해 showNotification을 호출하는 것은 반복적이기 때문에 useEffect 에 넣을려고 할 수 있으나,

만약 페이지 전환 시  장바구니(product.isInCart)의 이전 상태 값이 기억된다면(true로 유지된다면) 

예상치 못한 버그가 발생할 수 있다.

 

따라서 이벤트 핸들러 내부에서 해당 로직을 실행하는 게 좋다.

function ProductPage({ product, addToCart }) {
  // ✅ Good: Event-specific logic is called from event handlers
  function buyProduct() {
    addToCart(product);
    showNotification(`Added ${product.name} to the shopping cart!`);
  }

  function handleBuyClick() {
    buyProduct();
  }

  function handleCheckoutClick() {
    buyProduct();
    navigateTo('/checkout');
  }
  // ...
}

 

 

어플리케이션 초기화 

function App() {
  // 🔴 Avoid: Effects with logic that should only ever run once
  useEffect(() => {
    loadDataFromLocalStorage();
    checkAuthToken();
  }, []);
  // ...
}

 

이렇게 하면 개발환경에서 두 번 실행되는 현상을 경험할 수 있다.

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      // ✅ Only runs once per app load
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

앱 로드마다 한 번만 실행되어야한다면 이미 실행되었는지 여부를 추적하는 최상위 변수를 추가한다. 

 

 

data fetching

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);

  useEffect(() => {
    // 🔴 Avoid: Fetching without cleanup logic
    fetchResults(query, page).then(json => {
      setResults(json);
    });
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

만약에 query 값으로 "hello" 를 빠르게 입력할 경우

query는 "h", "he", "hel", "hell", "hello" 로 변경되고 각각 요청이 시작되지만 

응답이 도착하는 순서에 대한 보장은 없음

("hell" 응답이 "hello"보다 늦을 수 있음)

이처럼 서로 다른 요청이 경쟁하여 예상과 다른 순서를 가져오는 것을 경쟁상태(race condition) 이라고 함

race condition 해결을 위해서는 오래된 응답을 무시하는 clean up 이 필요하다

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);
  useEffect(() => {
    let ignore = false;
    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });
    return () => {
      ignore = true;
    };
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

 

"hello"로 렌더링 => 이전 effect clean up => "hello"로 effect 수행

 

 

 

참조

https://react.dev/learn/you-might-not-need-an-effect

 

You Might Not Need an Effect – React

The library for web and native user interfaces

react.dev

https://ko.legacy.reactjs.org/docs/hooks-effect.html

 

Using the Effect Hook – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

https://despiteallthat.tistory.com/182

 

[React] useEffect란?

오늘은 useEffect()에 대해 알아보겠습니다. [ useEffect ] useEffect() 함수는 React component가 렌더링 될 때마다 특정 작업(Sied effect)을 실행할 수 있도록 하는 리액트 Hook입니다.여기서 Side effect는 component가

despiteallthat.tistory.com

https://velog.io/@jay/you-might-need-useEffect-diet

 

useEffect 잘못 쓰고 계신겁니다.

고혈압을 10분만에 예방하세요.

velog.io

https://velog.io/@pius712/useEffect-race-condition-%EB%8B%A4%EB%A3%A8%EA%B8%B0

 

useEffect, race-condition 다루기

useEffect를 내에서 데이터 요청을 하는 로직은 race condition이 발생할 수 있다. 예를들어, id에 따라 데이터를 다르게 비춰주는 컴포넌트가 있다고 할 때,아래의 useEffect 훅에서 { id: 10 } { id: 20 } 이 연

velog.io

https://kiwitrip.tistory.com/entry/react-clean-up-function%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4

 

[react] clean-up function에 대한 이해

리액트의 hooks 인 useEffect 에는 clean-up Function 이라는 기능이 있습니다. 이에 관해 햇갈리는 분들이 많은 것 같아 한 번 정리해 보았습니다. 1) useEffect 는 기존 class형 컴포넌트의 componentDidMount 와 유

kiwitrip.tistory.com

 

'NOTION 정리 > REACT' 카테고리의 다른 글

중첩 객체 state 변경  (0) 2023.11.03
input 입력 시 따라오는 단위 만들기  (0) 2023.11.03