리액트 최적화 하실거예요? 하시겠습니까? 해야되나요? 왜죠? 왜그래야하죠?😂

const job = '프론트엔드';·2023년 8월 12일
0

난 지금도 아주 좋은데, 최적화 해야되나요?

웹 사이트 들어갔는데, 화면이 늦게뜨면 "이 정도면 내가 이 사이트를 만드는게 빠르겠네" 이런적 한 번 쯤 있잖아요..

나만 그래요? 나만 못됐나요?😱

여기서부터는(?) 아까부터 지극히 저의 개인적인 의견이 들어있습니다.

지금부터 최면을 걸겠다. '나는 우연히 엘지전자 홈페이지에 방문했다 🖥 '(여기는 엄청 빠름).

그 사이트는 ✅ 헤더, 네비바

✅ 캐로셀, 이벤트배너


✅ 상품소개

엘지전자(lge)페이지를 예시로 사용한 이유

https://lge-clone.vercel.app/

  • 메인페이지만 리액트로 만들어본 경험있음
  • 그때 와....메인에 많은걸 넣어놨구나 라고 느낀 경험이 있어서 예시페이지로 가져와 봄 !

메인화면인데, 정보가 굉장히 많다 = "로딩이 느리지 않을까?🤔" 합리적인 추측....

그런데, 이 한가지 정보의 상태가 변할때마다 화면이 매번 새로고침 된다면?

그래서 등장했다 !👏

리액트 최적화👏

컴포넌트 내부의 state 변경 시에만 리렌더링이 진행되게 하자 !

컴포넌트가 렌더되는 조건: 상태가 변하면 리렌더(re-render)

function Component() {
 const value = calculate(); 
 return <div> {value} </div>
}

function calculate() {
 return 10
}

기본원리 (Component 함수 호출시 모든 내부 변수 초기화)

  • Component 함수가 렌더링 되는 과정에서,value 변수는 초기화
  • 이때, calculate 함수의 값은 변하지도(update) 않았는데 호출

그러니깐,

const App = () => {
 
  const [listText, setListText] = useState([]);

  return (
    <>
    
      <Form     
        listText={listText}
      />
    </>
  );
};
const Form = ({ listText}) => {
 
  return (
   
      <Content listText={listText} />

  );
};
  • App컴포넌트가 최상위 부모 - Form 자식 컴포넌트 - Content 손자컴포넌트
  • App컴포넌트 내에서 listText의 상태가 변하면?
  • App컴포넌트 렌더됨 - Form컴포넌트 렌더됨 - Context컴포넌트 렌더됨
  • 매우 비효율적 !

메모이제이션(memoization): 리렌더링 될 필요가 없다면? 리렌더링 하지말자 !

원래 상태를 기억해 놓고 불필요한 렌더를 하지 않겠다

📌 충격 ! '손자 컴포넌트 Content의 독립 선언'(React.memo)

  • React.memo를 이용하면 컴포넌트의 props가 변경되지 않았을때 리렌더링되는 것을 방지하여 성능 최적화를 도모할 수 있음
  • 변경사항이 없음에도 리렌더링 되는 것은 불필요한 자원을 낭비하는 일이므로 효율성이 높음
  • 이러한 기능이 메모이제이션(memoization)
const Content = React.memo(({ listText }) => {
  console.log("Content is rendered");
  return (
    <ul>
      {listText.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
});

  • 컴포넌트 함수를 React.memo로 감싸주면
  • 다른 상위컴포넌트(부모, 최상위 부모)의 상태가 변하더라도 해당 컴포넌트는 리렌더링이 발생하지 않음
  • 해당 컴포넌트는 자신 컴포넌트의 상태가 바뀔때에만 리렌더 됨
  • Content컴포넌트 상태변화: list가 업데이트 되면 렌더됨

또 다른 가출방법 useMemo

  • React.memo와 결과는 똑같음
  • 형태는 useEffect와 비슷
const Contents = ({ listText }) => {
  return useMemo(() => {
    console.log("Content is rendered");
    return (
      <ul>
        {listText.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    );
  }, [listText]);
};

컴포넌트를 분리하는 경우

  • props요소를 구조분해해서 담아옴
  • 생성하고자 하는 컴포넌트 반환문에 작성해야 함
useMemo(콜백함수, [의존성배열])

useMemo(() => {
	return UI 반환
}, [의존성 배열])
  • 의존성배열의 상태가 바뀔때에만 콜백함수가 실행

다만 리소스를 많이 사용하는 계산이거나 연산이 빈번하게 재실행되지 않아도 되는 경우 useMemo가 적합

  • useMemo는 value와 복잡한 연산의 결과를 메모이제이션하는 데 사용
  • 따라서, 의존성 배열이 변경되지 않았을 때 중복계산을 피하기 위해 사용됨

🤚 코드를 작성하고, 기능을 보다가 갑자기 드는 의문..useEffect를 사용하면 안될까🤔?


아, 안되는구나? 사용 목적이 아예 다름 !

  • useEffect는 사이드 이펙트를 관리하는데 사용됨(데이터 가져오기, DOM조작, 외부 이벤트 구독 등)
  • 그러니깐 렌더링 목적으로 사용하는 것은 적합하지 않으며
  • UI 컴포넌트의 렌더링에서 사용되어서는 절대 안 됨
useEffect(() => {

},[])
  • 다시 확인해보니깐, useEffect에는 return 반환이 없음 !

컴포넌트를 분리하지 않는 경우

const Form = ({ listText, currentText, setListText, setCurrentText }) => {
  console.log("Form is rendered");
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        setListText([...listText, currentText]);
      }}
    >
      <input
        name="text"
        onChange={(e) => {
          setCurrentText(e.target.value);
        }}
      />
      <input type="submit" />
   

      {useMemo(() => {
        console.log("Content is rendered");
        return (
          <ul>
            {listText.map((item, index) => (
              <li key={index}>{item}</li>
            ))}
          </ul>
        );
      }, [listText])}
      
    </form>
  );
};
  • 이미 Form 컴포넌트의 return문 내에 마크업되고자 하는 부분에 작성해줌
  • {} 로 감싸준 이유는 자바스크립트문이기 때문

호적에서 파버릴꺼야 ! useCallback

  • useCallback 함수는 useMemo와 유사한 역할을 수행
  • 차이점: 함수를 반환, 부모 컴포넌트에서 해결 가능
  • 따라서 컴포넌트가 리렌더링 될 때마다 내부 함수가 재정의 되는 것을 방지할 수 있음
useCallbak(() => 내부함수(),[의존성배열]) 
  • 필요한 이유: 함수가 매번 재정의 될 경우, 이는 자식 컴포넌트의 리렌더링으로 이어질 수 있기 때문
const App = () => {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);

  const toggleMaster = useCallback(() => setMasterOn(!masterOn), [masterOn]);
  const toggleKitchen = useCallback(
    () => setKitchenOn(!kitchenOn),
    [kitchenOn]
  );
  const toggleBath = useCallback(() => setBathOn(!bathOn), [bathOn]);

  return (
    <>
      <Light room="침실" on={masterOn} toggle={toggleMaster} />
      <Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
      <Light room="욕실" on={bathOn} toggle={toggleBath} />
    </>
  );
};

export default App;
  • 사실 이 hook은 부모컴포넌트에서 자식컴포넌트를 떼어 내려는게 아니라, 불편할까봐 미리 조치를 취해주는 것
  • App(부모) - Light(자식)
  • 그러니깐, 다른 자식들의 상태가 바뀌는데 나머지 자식들까지 렌더가 되는 현상을 막기위함

useCallback을 사용하지 않고, 인라인으로 토글함수에 set함수를 전달해봄

import React, { useState } from "react";

const Light = React.memo(({ room, on, toggle }) => {
  console.log({ room, on });
  return (
    <button onClick={toggle}>
      {room} {on ? "ON" : "OFF"}
    </button>
  );
});

const App = () => {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);


  return (
    <>
      <Light room="침실" on={masterOn} toggle={() => setMasterOn(!masterOn)} />
      <Light
        room="주방"
        on={kitchenOn}
        toggle={() => setKitchenOn(!kitchenOn)}
      />
      <Light room="욕실" on={bathOn} toggle={() => setBathOn(!bathOn)} />
    </>
  );
};

export default App;
  • useCallback을 굳이 쓰지 않고, 인라인 방식으로 작성해도 사용에는 전혀 문제가 없지만
  • 이러한 방식은 렌더링마다 새로운 함수가 생성되므로 성능에 약간의 영향을 미칠 수 있음

🤚 여기서 의문! 이번엔 return도 없는데, useEffect는 안될까? ⭐가능⭐ 하지만 당연히 useCallback을 만들어낸 이유가 있겠지?

  • useEffect는 사이드 이펙트를 관리하기 위해서였고, 이 중에는 DOM을 조작하곤 했음
  • 그래서 이 과정을 useEffect를 사용한다면? 컴포넌트 구조가 복잡해지고 관리가 복잡해짐
import React, { useState, useEffect } from "react";

const Light = React.memo(({ room, on, toggle }) => {
  console.log({ room, on });
  return (
    <button onClick={toggle}>
      {room} {on ? "ON" : "OFF"}
    </button>
  );
});

const App = () => {
  const [masterOn, setMasterOn] = useState(false);
  const [kitchenOn, setKitchenOn] = useState(false);
  const [bathOn, setBathOn] = useState(false);

  useEffect(() => {
    // 현재 상태 값을 이용하여 토글 함수 업데이트
    const toggleMaster = () => setMasterOn(!masterOn);
    const toggleKitchen = () => setKitchenOn(!kitchenOn);
    const toggleBath = () => setBathOn(!bathOn);

    // 버튼에 업데이트된 토글 함수 연결
    document.querySelector("#masterButton").addEventListener("click", toggleMaster);
    document.querySelector("#kitchenButton").addEventListener("click", toggleKitchen);
    document.querySelector("#bathButton").addEventListener("click", toggleBath);

    // 컴포넌트가 언마운트될 때 이벤트 리스너 정리
    return () => {
      document.querySelector("#masterButton").removeEventListener("click", toggleMaster);
      document.querySelector("#kitchenButton").removeEventListener("click", toggleKitchen);
      document.querySelector("#bathButton").removeEventListener("click", toggleBath);
    };
  }, [masterOn, kitchenOn, bathOn]); // 상태 값이 변경될 때마다 효과 업데이트

  return (
    <>
      <Light room="bedroom" on={masterOn} />
      <Light room="kitchen" on={kitchenOn} />
      <Light room="Bathroom" on={bathOn} />
    </>
  );
};

export default App;
  • 이 경우 useEffect를 사용해서 버튼에 이벤트 리스너를 연결
  • 이벤트 리스너 버튼이 클릭되면, 해당 상태 토글을 트리거하도록 설정
  • 또 useEffect에 마무리 로직을 포함해서 컴포넌트가 언마운트될 때 이벤트 리스너를 제거하게 설정

결론

어느 것이 더 나은 방법이라고 할 수 없음. 적재적소에 알맞은 것을 쓰도록 하세오.

profile
`나는 ${job} 개발자`

0개의 댓글