[React.js] 리액트의 useEffect 활용법 & 성능 최적화 방법

2023. 4. 12. 17:58React/실전 리엑트 프로그래밍

💻 useEffect 활용법

 

❗ 의존성 배열은 잘못 입력 시, 버그로 이어질 수 있어 되도록 사용하지 않는 것을 권장

 

💡 useEffect 내에서 사용되는 값은 의존성배열에 추가해주어야 변경사항이 적용됨.

function Profile({ userId }) {
  const [user, setUser] = useState();
  useEffect(() => {
    fetchUser(userId).then(data => setUser(data));
  }, [userId]);
}
import { useState, useEffect } from "react";

export default function Profile({ userId }) {
  const [user, setUser] = useState();
  // 여기서는 Profile 컴포넌트가 렌더링 될 때마다 노출되기 때문에, 서버의 API를 호출하는 코드가 항상 실행
  useEffect(() => {
    fetchUser(userId).then((data) => setUser(data));
  });

  // 그래서 아래와 같이 빈배열을 입력해서, 마운트 된 후에 한번만 호출되도록 할 수 있음
  // 근데 이는 userId가 변경되어도 새로운 사용자 정보를 가져오지 못하기 때문에
  useEffect(() => {
    fetchUser(userId).then((data) => setUser(data));
  }, []);

  // 2.js로!!!!!
  // ...
}

💡 마운트 시점에만 실행되기 원한 다면 별도의 훅으로 사용하면 더 직관적이라는 장점이 있습니다.

function Profile({ userId }) {
  const [user, setUser] = useState();
  useOnMounted(() => fetchUser(userId).then(data => setUser(data)))
}
import { useState, useEffect } from "react";
import useOnMounted from './useOnMounted';

export default function Profile({ userId }) {
  const [user, setUser] = useState();

  // 아래와 같이 [userId]를 입력해서, userId가 변경되었을때 실행될 수 있도록
  // 이것이 의존성 배열
  useEffect(() => {
    fetchUser(userId).then((data) => setUser(data));
  }, [userId]);
  // ...
}

export default function Profile2({ userId }) {
  const [user, setUser] = useState();
  const [needDetail, setNeedDetail] = useState(false);
  // 매개변수값이 추가되고 이를 상태값으로 관리한다면
  // 상태값이기 때문에 의존성 배열에 추가를 해줘야하는데 
  // 기존에 있던 코드를 수정할 때에는 의존성 배열에 입력하는것을 깜빡하는 경우가 많다
  // 그래서 eslint에서 사용할 수 있는 룰을 만들어 제공 >> 보통 CRA에는 포함되어있음 >> 터미널에 뜨게 되어있음
  useEffect(() => {
    fetchUser(userId, needDetail).then((data) => setUser(data));
  }, [userId, needDetail]);
  // ...
}

// 근데 userId와 같은것이 변경되지않는다고 확신하게 된다면, useEffect를 사용하기 보다 hook을 추천


// 그래서 위의 것을 대신하여
function Profile({userId}){
  // useEffect를 만들어서 빈배열을 넣는것 보다 아래와 같이 훅을 만들어 하면 훨배 나음
  useOnMounted(()=>(fetchUser(userId).then((data) => setUser(data))));
  return null;
}
// 의존성 배열에 필요한 변수를 입력하지 않았을때, 어떤 문제가 발생할까
// 아래에서는 value2값을 입력하지않았다
// value2가 변경이 되어도 부수효과 함수는 새로 생성이 되겠지만, 의존성 배열에는 v2가 없기 때문에 리액트는 방금 생성된 부수효과 함수를 무시하고,
// 이전에 생성된 부수효과 함수를 계속 사용
// 함수가 생성될 때는 그 함수가 생성될 당시의 지역변수를 기억하고 있음
// 이를 실행 컨텍스트라고 함!!!!!!!!!!!!
import { useState, useEffect } from "react";

export default function MyComponent() {
  const [value1, setValue1] = useState(0);
  const [value2, setValue2] = useState(0);
  useEffect(() => {
    const id = setInterval(() => console.log(value1, value2), 1000);
    return () => clearInterval(id);
  }, [value1]); // 여기에 안넣음
  return (
    <div>
      <button onClick={() => setValue1(value1 + 1)}>value1 증가</button>
      <button onClick={() => setValue2(value2 + 1)}>value2 증가</button>
    </div>
  );
}

💡 useEffect 내에서 async/await 함수 사용

useEffect는 항상 함수를 반환해야하기 때문에 별도의 함수를 정의하여 사용합니다.

function Profile({ userId }) {
  const [user, setUser] = useState();
  async function fetchAndSetUser () {
    const data = await fetchUser(userId);
    setUser(data);
  }

  useEffect(() => {
    fetchAndSetUser();
  }, [fetchAndSetUser]);
  
  return (...);
}
// 부수효과 함수를 async await를 아래와 같이 하면 문제!
// 부수효과 함수의 반환값은 항상 함수 타입이어야 하기 때문
// async await 함수는 Promise 객체를 반환하기 때문에! >> 부수효과 함수가 될 수 없음

// 부수효과 함수는 함수만 반환할 수 있음
// 반환된 함수는 부수효과 함수가 호출되기 직전과 컴포넌트가 사라지기 직전에 호출
useEffect(async () => {
  const data = await fetchUser(userId);
  setUser(data);
}, [userId]);

// 위의 것은 잘못된것!

// async await를 사용하고 싶다면
// 함수를 하나 만들어서 호출해주는 방식을 사용해야 함
useEffect(() => {
  async function fetchAndSetUser() {
    const data = await fetchUser(userId);
    setUser(data);
  }
  fetchAndSetUser();
}, [userId]);

💡 의존성 배열 사용을 줄이기 위한 방법

  • if문으로 호출 조절
import { useState, useEffect } from "react";

export default function Profile({ userId }) {
  const [user, setUser] = useState();
  async function fetchAndSetUser(needDetail) {
    const data = await fetchUser(userId, needDetail);
    setUser(data);
  }
  // 의존성 배열을 되도록 사용하지 않도록 하되,
  // 함수의 실행시점을 의존성 배열로 관리하지 않고 부수효과 함수 내에서 처리를 하면
  // if (!user || user.id !== userId)
  // 부수효과 함수 안에서 사용하는 모든 변수는 최신화 된 값을 참조함으로 안심할 수 있다
  // 그래서 useCallback같은 것을 사용하지않아도 되는 것이지!
  useEffect(() => {
    if (!user || user.id !== userId) {
      fetchAndSetUser(false);
    }
  });
  // ...
}
  • 상태값 변경함수의 매개변수로 함수를 사용

setCount(count + 1);
=>
setCount((prev) => prev + 1);

function MyComponent() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    function onClick() {
      setCount(prev => prev + 1);
    }
    window.addEventListener('click', onClick);
    return () => window.removeEventListener('click', onClick);
  });
  // ...
}
  • useReducer 훅 사용
// dispatch는 변하지 않는 값 (의존성 배열로 빈 배열을 입력했다)
// 현재는 의존성 배열이 전혀 입력되지않은 상태이다
// 값을 변경하는 로직은 reducer에서 작성

// useReducer를 사용하면, 다양한 액션과 상태값을 관리하기 용이하고, 상태값 변경 로직을 여러곳에서 재사용하기에도 좋음
function Timer({ initialTotalSeconds }) {
  const [state, dispatch] = useReducer(reducer, {
    hour: initialTotalSeconds / 3600
    minute: (initialTotalSeconds % 3600) / 60
    second: initialTotalSeconds % 60
  });
  const { hour, minute, second } = state;
  useEffect(() => {
    const id = setInterval(dispatch, 1000);
    return () => clearInterval(id);
  });
  //...
}

function reducer(state) {
  const { hour, minute, second } = state;
  if(second) {
    return { ..state, second: second - 1 };
  } else if(second) {
    return { ..state, minute: minute - 1, second: 59 };
  } else if(second) {
    return { ..state, hour: hour - 1, minute: 59, second: 59 };
  } else {
    return state;
  }
}
  • userRef 훅 사용
// 만약 이를 해결하는 마땅한 방법이 떠오르지않는다면,useRef가 손쉬운 해결책이 될 수 있음
// (아래쪽 의존성 배열로 빈 배열을 입력하면된다)
// 이렇게 렌더링 될때마다, ref객체에 onClick함수를 넣어주면 됨
// 그리고 원래 사용하려고 한곳에서 ref객체를 이용해 함수를 호출하는 방식       onClickRef.current();

// 부수효과 내에서 사용된 ref객체는 의존성 배열에 추가할 필요가 없다
// 그런데, 의존성 배열에 이 함수를 입력하지않으려고 애써보이는데
// 불필요한 코드와 연산이 추가되기도 했음
// 하지만 이 모든 것은 의존성 배열을 관리하는 규칙을 지키기 위한 것
// 이것이 리액트 훅의 단점
import { useRef, useEffect } from "react";

export default function MyComponent({ onClick }) {
  const onClickRef = useRef();
  useEffect(() => {
    onClickRef.current = onClick;
  });
  useEffect(() => {
    window.addEventListener("click", () => {
      onClickRef.current();
      // ...
    });
    // ...
  });
  // ...
}

// ref 객체는 이렇게 컴포넌트 함수에서직접 변경해도 된다고 생각할 수 있지만, 문제가 있음
// 부수효과 함수에서 ref를 수정하는 이유는     onClickRef.current = onClick;
// 나중에 도입될 리액트의 concurrent mode 때문
// concurrent mode : 컴포넌트가 시행되었다 하더라도, 중간에 렌더링이 취소될 수 있음
// 렌더링은 취소되었는데 ref 객체에는 잘못된 값이 저장될 수 있음으로
// ref 객체는 이렇게 컴포넌트 함수에서 직접 수정하면 안됨  8.js   onClickRef.current = onClick;
// 단 concurrent mode로 동작하지 않는 리액트 버전에서는 문제가 되지는 않음
// Concurrent / Concurrency    : 동시적으로 / 동시성
// Parallel / Parallelism            : 병렬적으로 / 병렬
// 나중을 위해 7.js 처럼 작성하는 것이 좋다
// useEffect(() => {
//   onClickRef.current = onClick;
// });

 

 

💻 렌더링 속도를 올리기 위한 성능 최적화 방법

가장 많은 CPU 자원을 사용하는 것은 렌더링이다.

리액트 렌더링 과정

리액트는 데이터+컴포넌트 함수로 화면을 그립니다.

여기서 대부분의 연산은 "컴포넌트 함수 실행과 가상돔"에서 발생합니다.

데이터 : 컴포넌트의 속성 값과 상태 값 >> 변경되면 화면을 다시 그림

 

+가상돔

이전 돔과 비교하여 변경된 부분만 렌더링하기 위한 가상의 돔

 

💡 React.memo

 

리액트 memo함수는 속성값이 변경될 경우에만 렌더링 하도록 도와주는 함수입니다.

 

memo함수는 첫번째인자로 컴포넌트, 두번째 인자로 비교함수를 받습니다.

 

두번째 인자가 없을시, 얕은 비교 수행 함수가 실행됩니다.

👉 얕은 비교라도 속성값이 변경되지 않았다면 실제 돔에도 변경되지 않아 큰문제는 없습니다.

function MyComponent(props) {
  // ...
}
function isEqual(prevProps, nextProps) {
  // true 또는 false를 반환
  // 참을 반환하면 이전 렌더링 결과를 재사용
  // 거짓이라면 컴포넌트 함수를 이용해 가상돔을 업데이트하고 변경된 부분만 실제 돔에 반영
  // 만약 이렇게 속성 값 비교함수를 입력하지 않으면, 얕은 비교를 수행하는 기본함수가 사용이 됨
  //
  // 컴포넌트를 memo 함수로 감싸지 않았다면, 항상 거짓을 반환하는 속성값 비교함수가 사용된다고 생각할 수 있다
  // 이때는 속성값이 변경되지않아도 부모컴포넌트가 렌더링 될때마다 자기자신도 렌더링될것이다
  // 속성값이 항상 거짓을 반환하더라도, 속성값이 변경되지 않았다면, 실제돔도 변경되지않을 것이기때문에 대체로 문제가 되지는 않음
  //
  // 하지만 렌더링 성능이 중요한 상황에서는 memo함수를 사용해서 컴포넌트 함수의 실행과 가상돔의 계산을 생략할 수 있기 때문에 렌더링 성능상의 이점은 있음
}
React.memo(MyComponent, isEqual);

 

이전 속성값과 상태값에 대해 포스팅 했을때,

불변값이 속성값과 가변값 상태값을 말했었습니다.

여기서 상태값 또한 불변값으로 관리하는 것이 좋다고 말했던 이유가 나오는데요.

 

값을 이전과 비교할떄 ===연산자로 비교하기 위해 객체를 불변객체로 관리하는것이 좋기 때문입니다. 

const state = {};
const newState = {};

state === {...state, ...{name: 'horong'}}; // false
const prevProps = {
  todos: [
    { title: "fix bug", priority: "high" },
    { title: "meeting with jone", priority: "low" },
    // ...
  ],
  // ...
};
const nextProps = {
  todos: [
    { title: "fix bug", priority: "high" },
    { title: "meeting with jone", priority: "high" },
    // ...
  ],
  // ...
};

prevProps.todos === nextProps.todos;
prevProps.todos[0] === nextProps.todos[0];
// memo함수를 사용하면, 속성값 비교함수를 통해 컴포넌트 렌더링 과정을 생략할 수 있는데
// 속성값 비교함수를 입력하지않으면, 리액트가 기본으로 갖고있는 함수가 사용됨
// 이전/이후 속성 값을 가지고 있을 때,  속성값 변경여부는 어떻게 알게될까?
// priority == low>> high로 변경됨
// prevProps.todos === nextProps.todos     >>로 비교가능
// 객체를 불변객체로 관리
// 데이터를 불변 데이터로 관리하면, 이전이후 값 단순 비교로 컴포넌트의 속성값이 변경되었는지 알수있음
// 상태 값을 불변객체로 관리하면, 렌더링 성능에 큰도움

// 새로운 객체를 만든다고 생각하면됨
const prevTodos = [1, 2, 3];
const nextTodos = [...todos, 4];
prevTodos === nextTodos;

// ----------------------------------------------------------------------------------------------------------

const prevProps2 = {
  todos: [
    { title: "fix bug", priority: "high" },
    { title: "meeting with jone", priority: "low" },
    // ...
  ],
  friends: [],
  // ...
};
const nextProps2 = {
  todos: [
    { title: "fix bug", priority: "high" },
    { title: "meeting with jone", priority: "high" },
    // ...
  ],
  friends: [],
  // ...
};

// 얕은 비교
const isEqual =
  prevProps.todos === nextProps.todos &&
  prevProps.friends === nextProps.friends;

 

💡 함수 속성값

 

속성값으로 함수를 자식에게 넘겨줄 시, React.memo를 사용하였다 할 지라도

함수는 부모가 렌더링 될 떄마다 새로 생성되기 때문에 자식 또한 다시 렌더링됩니다.

// 속성값으로 함수를 자식에게 넘겨줄 시, React.memo를 사용하였다 할 지라도
// 함수는 부모가 렌더링 될 떄마다 새로 생성되기 때문에 자식 또한 다시 렌더링

// fruit가 변경되었을때 계속 다시 렌더링
import { useState } from "react";

function Parent() {
  const [selectedFruit, setSelectedFruit] = useState("apple");
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{`count: ${count}`}</p>
      <button onClick={() => setCount(count + 1)}>increase count</button>
      <SelectFruit
        selected={selectedFruit}
        onChange={(fruit) => setSelectedFruit(fruit)}
      />
    </div>
  );
}
import { useState } from "react";

function Parent() {
  // 상태값 변경함수는 한번생성되고 다시 생성되지않기 때문에
  const [selectedFruit, setSelectedFruit] = useState("apple");
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{`count: ${count}`}</p>
      <button onClick={() => setCount(count + 1)}>increase count</button>
      {/* setSelectedFruit 와 같이 상태값 변경함수를 그대로 입력해주면 간단히 해결 */}
      <SelectFruit selected={selectedFruit} onChange={setSelectedFruit} />;
    </div>
  );
}
import { useState } from "react";

function Parent() {
  const [selectedFruit, setSelectedFruit] = useState("apple");
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{`count: ${count}`}</p>
      <button onClick={() => setCount(count + 1)}>increase count</button>
      <SelectFruit
        selected={selectedFruit}
        // 하지만 아래와 같이 다른방법이 있다면 이 방식은 힘들겠다
        onChange={(v) => {
          //...
          setSelectedFruit(v);
        }}
      />
      ;
    </div>
  );
}

👉 useCallback 훅 사용으로 해결

useCallback은 함수이니께!!!!!

이 경우 한번 생성된 값으로 고정이 된다!

function Parent() {
  const [v1, setV1] = useState();
  
  const onChangeCallback = useCallback(v => {
    // ...
    setV1(v);
  }, [])
  
  return <Child onChange={onChangeCallback}>
}

 

💡 객체 속성값

 

객체 또한 함수와 동일하게 매번 새로운 객체로 인식합니다. 

 

👉 불변객체라면 분리하여 작성

function Parent() {
  return <Child data={DATA}>
}

const DATA = [1, 2, 3];

👉 가변객체라면 useMemo 훅 사용 ( :필요할 때만 변경)

filter일 경우 계속 새로 불러와야하니까!!!

function Parent() {
  const [maxValue, setMaxValue] = useState(0);
  const data = useMemo(() => DATA.filter(e => e<=maxValue), [
    maxValue
  ]);
  return <Child data={data}>
}

const DATA = [1, 2, 3];
import React, { useState } from "react";

export default function App() {
  return <SelectFruit />;
}

function SelectFruit({ selectedFruit, onChange }) {
  const [fruits, setFruits] = useState(["apple", "banana", "orange"]);
  const [newFruit, setNewFruit] = useState("");
  function addNewFruit() {
    // 아래와 같이 하면안됨 >> 추가가 안됨
    // fruits.push(newFruit);
    // setFruits(fruits);

    // 이렇게 해야함!
    // 선택값은 불변 변수로 해야함
    setFruits([...fruits, newFruit]);
    setNewFruit("");
  }
  // ...
  return (
    <div>
      <Select options={fruits} />
      <input
        type="text"
        value={newFruit}
        onChange={(e) => setNewFruit(e.target.value)}
      />
      <button onClick={addNewFruit}>추가하기</button>
      {/* ... */}
    </div>
  );
}

const Select = React.memo(({ options }) => (
  <div>
    {options.map((item, i) => (
      <p key={i}>{item}</p>
    ))}
  </div>
));

useMemo, useCallback, React.memo >> 너무 쓰려 애쓰지말어. 성능 이슈있을때만 최적화하길 추천

 

출처 : https://keeper.tistory.com/37?category=940178 

 

[React.js] 리액트의 useEffect 활용법 & 성능 최적화 방법

💻 useEffect 활용법 ❗ 의존성 배열은 잘못 입력 시, 버그로 이어질 수 있어 되도록 사용하지 않는 것을 권장 💡 useEffect 내에서 사용되는 값은 의존성배열에 추가해주어야 변경사항이 적용됨. fun

keeper.tistory.com