본문 바로가기

Frontend Study - 1/React

React : useEffect에 대하여 !

목차

1. useEffect란?

2. useEffect의 사용

3. useEffect 문제해결

 

 

1. useEffect란?

useEffect(setup, dependencies?)

 

유즈이펙트는 외부 시스템과 컴포넌트를 동기화하는 리액트 훅이다. 리액트의 주된 목적인 UI구현을 제외한 나머지 기능들을 side effect라고 하는데 (구독, 타이머, 로깅 fetching 등)  이 때 useEffect를 사용한다. useEffect에 첫번째 인자로 전달된 setup 함수는 렌더링이 화면에 반영된 이후에 실행되게 된다. 두번째 인자로 전달된 dependency([])는 특정값이 변경될 때만 함수가 실행될 수 있게 해준다. 

 

setup 함수는 경우에 따라 cleanup 함수를 리턴할 수 있다. 컴포넌트가 처음 DOM에 추가되면 리액트는 setup 함수를 실행한다. dependency변화에 따른 리렌더링 이후, cleanup 함수가 있다면 리액트는 처음으로 그 cleanup 함수를 이전 값과 함께 실행한다. 그 이후 새로운 값과 함께 setup 함수를 실행한다. 만약 컴포넌트가 DOM에서 사라지는 경우라면 리액트는 cleanup 함수를 마지막으로 실행한다.

 

dependency는 setup코드에 있는 모든 (reactive value)반응적인 값들이다. 반응형 값들은 props, state, 컴포넌트 body에서 선언된 모든 변수들과 함수들을 포함한다. 디펜던시는 상수로 표현될 수 있는 개수를 가진 아이템들을 가져야 한다. (ex) [dep1, dep2, dep3]). 리액트는 모든 디펜던시들을 Object.is를 통해 이전 값과 비교할 것이다. 만약 특정 디펜던시를 지정하지 않으면 모든 리렌더링 마다 useEffect가 실행된다. 빈 array를 지정하면 컴포넌트가 마운트될 때 한번만 실행된다.

 

+ Strict 모드일 때 리액트는 개발자 모드에서 살재 setup전 추가적인 setup-cleanup 사이클을 진행한다. 이것은 cleanup로직이 setup로직을 제대로 반영하였는지, setup로직이 하던 일을 제대로 중지시키는지 확인 하기 위한 스트레스 테스트이다. 추가 사이클로 인해 문제가 발생한다면, 제대로 된 cleanup 함수를 구현해야 한다. 

 

 

2. useEffect의 사용

 

1) 주의사항

 

useEffect는 React 패러다임에서 벗어나는 방법이다. 이를 통해 네트워크 또는 브라우저 DOM과 같은 외부 시스템과 컴포넌트를 동기화 할 수 있게 되는 것. 외부 시스템과 동기화를 위한 것이 아니라면 굳이 useEffect를 쓸 필요없다. (props, state가 변경될 때 컴포넌트의 상태를 업데이트 하려고 할 때, 렌더링을 위한 데이터를 변환) 불필요한 effects의 제거를 통해 오류를 방지하고, 코드의 가독성 및 실행 속도빨라지게 할 수 있다.

 

- object dependency

만약 디펜던시에 오브젝트함수가 들어있다면 그것은 원하는 것 이상의 리렌더링을 야기할 위험이 있다. 오브젝트나 함수는 실제 내부 값이 변하지 않았더라도 새롭게 HEAP에 저장되고 기존의 값과 다른 값으로 인식하기 때문. 이것을 고치기 위해서는 우선 불필요한 오브젝트와 함수 디펜던시를 제거하고, 상태 업데이트, 비 반응 로직을 useEffect 외부로 내보내야 한다. 

 

- useLayoutEffect

effect가 유저상호작용(클릭)에 의해 발생하지 않았으면, 리액트는 effect가 실행 되기전에 브라우저가 업데이트된 스크린을 paint하게 한다. 만약 Effect가 visual과 관련된 일(ex) 툴팁 위치고정)을 하고 있거나 딜레이가 눈에 띄는 경우 (ex)화면 깜빡임) useLayoutEffect를 사용해야 한다. 

 

effect가 유저상호작용(클릭)에 의해 발생 하였어도, 브라우저는 effect 내부의 상태 업데이트를 처리하기 전에 화면을 다시 그릴 수 있다. (일반적으로 이게 바람직) 그러나 브라우저가 화면을 다시 그리는 것을 차단해야 하는 경우,  useLayoutEffect를 사용해야 한다. 

 

useLayoutEffect는 useEffect의 한 종류이며, 브라우저가 스크린을 repaint하기 전에 동작한다는 특징이 있다. 퍼포먼스에 좋지 않을 수 있음으로 필요한 경우가 아닌 이상 useEffect를 사용해야 한다.

 

- Client side

useEffect는 client 사이드에서만 실행된다. 서버 렌더링 중에 실행되지 않는다.

 

 

2) 사용예제

 

- 외부 시스템 연결

때로 컴포넌트는 페이지가 보여지는 동안 네트워크 혹은 브라우저 API, third-party 라이브러리 등과 연결된 상태를 유지해야 한다. 예를 들어 타이머 setInterval(), clearInterval() / 이벤트 구독 window.addEventListenr() / window.removeEventListener() / animation.start(), animation.reset() 등. 이 때 useEffect를 쓸 수 있다. 

 

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]);
  // ...
}

 

setup코드를 통해 시스템에 연결하고 있고 컴포넌트가 언마운트될 때 연결을 끊기 위해서 cleanup 함수를 리턴하고 있다. 디펜던시 리스트에는 컴포넌트에서 사용 중인 모든 값들이 들어가 있다. 위의 ChatRoom 컴포넌트가 페이지에 추가되면, 초기 serverUrl과 roomId와 함께 채팅방에 연결된다. 만약 serverUrl이나 roomId가 렌더링과 함께 변경되면 useEffect는 이전 방에서 연결을 끊고(cleanup function) 다음 방으로 연결한다. ChatRoom 컴포넌트가 페이지에서 최종적으로 제거될 때는, useEffect는 마지막으로 한 번 연결을 끊는다.

 

 

- fetching Data with Effects

useEffect는 데이터 fetching에 사용할 수 있다.

 

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);

  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    };
  }, [person]);

useEffect에서 데이터를 페칭하는 일반적인 코드의 모습이다. 특별한 할 점은, ignore이라는 변수가 false로 선언된 뒤에, cleanup 함수를 통해 true로 다시 설정되었다는 것. 이 로직은 race condition에서 벗어나게 해준다. 

 

*race condition : 시스템의 동작이 제어할 수 없는 이벤트의 순서나 타이밍에 의해 의존하는 상태. 예를 들어 클릭 이벤트를 통해 useEffect가 실행되고 각각 다른 데이터를 응답 받는다고 가정할 때, 클릭을 빠르게 여러번 했을 경우 race condition으로 인해 마지막 클릭에 대한 값이 최종 값으로 들어오지 않을 수 있다. 

 

useEffect를 통해 데이터 fetching을 하게 될 경우의 장점이라고 하면 'network waterfalls'를 만드는 것이 쉽다는 것이다. 부모 컴포넌트를 렌더링 하고 일부 데이터를 가져오고, 자식 컴포넌트를 렌더링 하면 자식 컴포넌트가 데이터를 가져오기 시작하게 되는 것. 하지만 네트워크가 느리다면 모든 데이터를 병렬로 가져오는 것보다 느리게 된다.

 

data fetching시 useEffect를 사용했을 때의 단점들이 있다. useEffect는 서버에서 실행되지 않는다. 초기에 서버에서 렌더링 된 HTML에 데이터가 없이 로딩상태만 포함하게 된다. 클라이언트는 모든 Javscript를 다운로드 하고 앱을 렌더링 한 뒤에 데이터를 가져온다.

 

그리고 데이터를 프리로드하거나 캐시하지 않는다. 예를 들어 컴포넌트가 언마운트 되고 다시 마운트되었다면 데이터를 다시 가져와야 한다. 추가로 race condition과 같은 버그를 해결하기 위한 별도의 boilerplate code들이 필요하다는 것.

 

그러므로 여타 다른 프레임워크들의 데이터 fetching 매커니즘을 사용하는 것이 useEffect를 사용하는 것보다 더 효율적일 것인 경우가 많다.

 

 

- 이전 state 값에 기반한 state 값 업데이트

 

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1); // You want to increment the counter every second...
    }, 1000)
    return () => clearInterval(intervalId);
  }, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
  // ...
}

 위 코드는 dependency에 count가 들어있음으로, count state의 변경에 따라 effect를 실행하게 된다. 하지만 cleanup function이 포함되어 있기 때문에 count가 변경될 때마다 clearInterval 함수가 실행되고 setup이 function이 다시 실행되게 된다. count라는 state값의 변경은 그 자체로 리렌더링을 동작시킨다. 즉 이상적이지 않은 코드인 것.

 

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ Pass a state updater
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ Now count is not a dependency

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

위와 같이 수정할 수 있다. setCount함수에 이전 상태값을 인자로 받는 updater function을 지정해주면 된다.

 

 

 

3. useEffect 문제해결

 

무한렌더링 문제

무한렌더링 문제는 아래 두가지 사항으로 인해 일어난다. 

 

- effect를 통해 state를 업데이트하고 있다.

- state의 변경이 re-render를 발생시키고 동시에 useEffect dependency의 변경을 야기 한다. 

 

해결에 앞서 useEffect가 외부 시스템과의 연결 혹은 data 흐름을 관리하는 것이 아니라면 로직을 단순화 함으로써 useEffect사용을 제거하는 것을 고려해야 한다. 

 

그리고 렌더링 되지 말아야 할 데이터를 추적 하고 있는 경우 'ref'를 이용하는 방법에 대해서 고려해 봐야한다. 

마지막 해결책은 useMemo, useCallback을 통해 감싸는 것. 

 

나 같은 경우 javascript에서 오브젝트를 생성할 때 같은 내용이 들어있음에도 새로운 오브젝트로 취급하는 구조로 인해 무한렌더링 문제를 많이 겪었었다. 디펜던시 값을 오브젝트 자체로 하지 않고 오브젝트 내부의 값을 지정해 줌으로써 해결할 수 있었다.

 

 

참고사이트

https://beta.reactjs.org/reference/react/useEffect

 

'Frontend Study - 1 > React' 카테고리의 다른 글

React : memo  (0) 2023.03.27
React : state, useState에 대하여, snapshot, batches  (0) 2023.03.01