worst practice

Kaia·2023년 2월 19일
post-thumbnail

Prop Drilling

Prop Drilling으로 인해 불필요한 재렌더링이 발생할 수 있습니다.
React component는 prop이 변경될 때 항상 렌더링되므로 단순히 prop을 자식 컴포넌트로 전달하는 중간 컴포넌트는 prop을 전달할 때 강제로 렌더링됩니다.
따라서 이 과정에서 메모리와 성능 문제가 발생할 수 있습니다.

원인

  • 중첩된 하위 컴포넌트가 많은 프로젝트에서 특정 컴포넌트에서 상태 값을 사용하고 싶을 경우, 원하는 지점까지 props를 내려줌으로써 상태 값을 사용하게 됩니다.
  • 이 과정에서 메모리와 성능 문제가 발생할 수 있습니다.

해결

  • context api(useContext)를 사용하거나 상태관리(redux, recoil)를 사용할 수 있습니다.
  • context를 이용하면, 트리 단계마다 명시적으로 props를 넘겨주지 않아도 많은 컴포넌트가 값을 공유합니다. 같은 데이터를 트리 안 여러 레벨이 있는 많은 컴포넌트에 주어야 할 때 사용하며 context를 사용하면 컴포넌트를 재사용하기가 어려워지므로 꼭 필요할 때만 쓰세요.
  • compound component를 이용하면, props 넘기는 걸 대체 가능

as-is

<Page user={user} avatarSize={avatarSize} />
	...
    <PageLayout user={user} avatarSize={avatarSize} />
		...
        <NavigationBar user={user} avatarSize={avatarSize} />
        	...
            <Link href={user.permalink}>
              <Avatar user={user} size={avatarSize} />
            </Link>

to-be

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// 이제 이렇게 쓸 수 있습니다.
<Page user={user} avatarSize={avatarSize} />
	...
	<PageLayout userLink={...} />
    	...
		<NavigationBar userLink={...} />
        	...
			{props.userLink}

부적절한 useEffect 사용

클래스형 컴포넌트에서 componentDidMount는 이벤트 리스너를 할당하는 데 사용하는 일반적인 함수였으며 함수형 컴포넌트에서는 해당 역할에 useEffect를 사용합니다.

원인

  • useEffect가 실행될 때 설정한 이벤트리스너를 제거하지 않습니다. 따라서 종종 다시 렌더링될 때 이벤트리스너를 트리거합니다. 이러면 여러 리스너가 생성되어 메모리누수가 생기며 성능에도 문제가 발생할 수 있습니다.

해결

  • 마운트가 해제되는 때에 제거합니다. clean-up함수 즉, return 시에 제거 함수를 실행합니다.

    unmount 될 때 - useEffect(func, [])
    특정값 update 직전 - useEffect(func, [특정값])
    => re-render -> 이전 effect clean-up -> effect

  • useEffect가 한 번만 실행되도록 빈 디펜던시 배열을 두 번째 매개변수로 사용
  • 모든 디펜던시의 변화를 감지해 깨진 클로저가 만들어지는 것을 피하려면 모든 디펜던시를 디펜던시 배열에 추가
function App() {
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);

  // 이렇게 하면 안된다!
  useEffect(() => {
    const timeoutA = setTimeout(() => setVarA(varA + 1), 1000);
    const timeoutB = setTimeout(() => setVarB(varB + 2), 2000);

    return () => {
      clearTimeout(timeoutA);
      clearTimeout(timeoutB);
    };
  }, [varA, varB]);

  return (
    <span>
      Var A: {varA}, Var B: {varB}
    </span>
  );
}
function App() {
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    if (varA > 0) return;

    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]); // 이렇게 해야한다. 모든 디펜더시가 배열에 들어가 있다.

  return <span>Var A: {varA}</span>;
}

삼항연산자 중첩

큰 구성 요소를 래핑할 수 있고 코드를 읽을 때 해독하기 어려울 수 있다는 단점이 있습니다.
이 경우 함수를 사용하여 렌더링 논리를 추상화합니다.


map() index를 key로 사용

map() 사용 시 해당 요소 또는 구성 요소의 적절한 추적을 처리할 수 있도록 Key는 고유해야 합니다. 인덱스를 Key로 사용하는 경우 해당 키는 경우에 따라 중복될 수 있으므로 피해야 합니다.

원인

  • 배열의 인덱스를 Key로 사용하면, 추가, 삭제 등과 같은 작업을 수행하면 엘리먼트의 순서가 변경될 수 있습니다. 이 경우 Key는 이전과 같게 되고 React는 이전과 동일한 요소/구성 요소라고 가정하며 따라서 상태의 정확한 변화를 감지할 수 없습니다.

    Virtual DOM을 생성해서 업데이트된 부분과 기존의 돔을 비교하는 과정을 통해 변경된 ui부분을 업데이트하는데,
    어떠한 부분이 기존의 돔과 비교하여 변경이 되었는지 확인하기 위해 각 엘리먼트에 중복성이 없는 유일한 값을 key값으로 할당해야 합니다.

  • 또한 재사용 가능한 컴포넌트가 계속 사용되다 보면 하나의 UI컴포넌트안에 중복이 일어날 수 있습니다.

해결

  • 복잡한 코드에선 대부분 fetch나 api를 통한 연산 이후 데이터를 렌더링하게 되는데, 대부분 비동기로 이루어지기 때문에 이로 인해 인덱스의 고유값이 깨지게 되고 이는 예기치못한 결과로 이어질 수도 있다고 한다.

  • 고유성이 보장되는 Database의 id나, global 변수를 이용해 id를 생성하고 이를 key로 사용해주는것이 좋다.

const App = ({numbers}) => {
  return (
    <ul className="App">
      {numbers.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};

이벤트 전달 시 익명 함수 사용

원인

  • 버튼이 렌더링될 때마다 다른 콜백이 생성됩니다.
  • 즉 익명 함수를 전달하고, 렌더링되면 또다른 새로운 익명함수를 받기 때문에 항상 재렌더링 됩니다.

해결

  • 이벤트 핸들러로 전달하거나 생성자 바인딩을 사용합니다.

as-is

const App = () => {
  const handleClick = () => {
    console.log("You Clicked???????");
  };
  return <button onClick={() => handleClick()}>Click me</button>;
};

to-be

const App = () => {
  const handleClick = () => {
    console.log("You Clicked???????");
  };
  return <button onClick={handleClick}>Click me</button>;
};

필요 이상의 importing

  • Parcel 및 webpack과 같은 대부분의 최신 App Bundler는 프로덕션을 위해 코드를 최소화하고 압축하기 위해 많은 작업을 수행하지만, 문제를 해결하기 위해 App Bundler에만 의존하지 않고 클라이언트에 어떤 코드가 전달되는지 알고 있는 것이 좋습니다

각 렌더링마다 중복되는 작업

  • useMemo 및 useCallback를 사용할 수 있습니다. 렌더링할 때마다 반복할 필요가 없는 특정 작업을 메모하여 성능을 향상시킵니다.
  • 일반적으로 좋은 일이지만 모든 작은 일에 대해 수행해서는 안 됩니다.

지나치게 많은 작업에 상태값 사용

  • 상태 값에 가능한 한 적은 데이터를 저장하기 때문에(필요한 데이터만) 상태에 데이터를 배치하기 전에 다른 저장된 변수 또는 상태에서 필요한 데이터를 가져올 수 있는지 고려해야합니다.

Boolean 연산자의 잘못된 사용

  • 경우에 따라 조건부로 렌더링된 컴포넌트를 자체 컴포넌트로 분리하고 조건이 충족되지 않으면 null을 반환하는 것이 합리적입니다.
  • 일반적으로 number/array/string 등으로 확인하는 조건에 대해 boolean으로 변경하는 것이 더 실용적입니다.

as-is

to-be

컴포넌트 내부에 컴포넌트 선언

  • 내부 컴포넌트를 export 할 수 없으며 내부 컴포넌트는 상위 컴포넌트의 범위에 종속되어 재사용성이 떨어지고 결합도가 높습니다.
  • 각 부모 컴포넌트의 렌더링에서 내부 컴포넌트에 대한 선언 함수가 다시 생성되므로 성능이 저하됩니다.
const Main = () => {
  const [name, setName] = useState("Ateev");
  
  const Sub = () => {
    return <h1 className="p-5">Hello {name}, I am the child of Mr & Mrs Khana</h1>;
  };
  return (
    <>
      <Sub />
    </>
  );
};

0개의 댓글