티스토리 뷰

리액트 컴포넌트의 렌더링 조건

  • 컴포넌트는 자신의 state가 변경되거나 부모 컴포넌트가 렌더링 되었을 때 함께 렌더링이 됩니다.

 

const School = (props) => {
    return (
        <Student
            name={'금동이'}
            age={10}
            gender={'male'}
        />
    );
}
  • Student 컴포넌트는 이름, 나이, 성별을 props로 전달받아서 Student의 정보를 보여주는 간단한 컴포넌트입니다.

 

const Student = ({ name, age, gender}) => {
    return (
        <div>
            <h1>{name}</h1>
            <span>{age}</span>
            <span>{gender}</span>
        </div>
    );
}
  • School 컴포넌트는 Student의 부모 컴포넌트입니다.
  • 리액트는 기본적으로 부모 컴포넌트가 렌더링되면 모든 자식 컴포넌트도 같이 렌더링이 됩니다.
  • Student 컴포넌트는 항상 같은 props를 전달받도록 되어있죠?! 그래서 Student는 항상 같은 결과를 화면에 보여줄 것입니다.
  • 그런데 School 컴포넌트가 렌더링이 되면 Student 컴포넌트는 변화가 없음에도 함께 렌더링이 됩니다.
  • 만약 Student 컴포넌트가 복잡한 로직이나 데이터베이스의 연산을 거쳐서 데이터를 가져온다고 하면 항상 같은 값을 리턴하는데 반복적으로 연산을 해야하니 성능적으로 매우 좋지 않을 것 같습니다.
  • 그럼 Student 컴포넌트에 무언가 변화가 있어서(props가 변경이 될 때) 렌더링이 되어야만 할 때 렌더링이 되도록 할 수는 없을까요?!

 

React.memo

  • React.memo는 리액트에서 제공하는 고차 컴포넌트중 하나입니다. 고차 컴포넌트(HOC)는 어떤 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환해주는 컴포넌트입니다.

 

  • 렌더링이 되어야 할 상황에 놓일 때마다 Prop check를 통해 props에 변화가 있다면 렌더링을 하고 변화가 없다면 기존의 렌더링 된 내용을 재사용하게 됩니다.

 

const Student = ({ name, age, gender}) => {
    return (
        <div>
            <h1>{name}</h1>
            <span>{age}</span>
            <span>{gender}</span>
        </div>
    );
}

export default React.memo(Student);
  • React.memo를 사용하려면 위와 같이 컴포넌트를 memo로 감싸주기만 하면 됩니다!

 

React.memo 를 사용하기 좋은 상황

  • 컴포넌트가 같은 props로 자주 렌더링이 될 때
  • 컴포넌트가 렌더링이 될 때마다 복잡한 로직을 처리해야 할 때

memo = memoization으로 기존의 렌더링된 데이터를 재사용하기 위해 메모리 어딘가에 저장해두는 것이므로 무분별한 사용은 오히려 성능 저하를 불러올 수 있습니다!

 

deep comparison

const School = (props) => {
    const name = {
        lastName: '엄',
        firstName: '금동'
    }

    return (
        <Student
            name={name}
            age={10}
            gender={'male'}
        />
    );
}
  • 예시를 조금 바꾸어보았습니다.
  • 기존에 string으로 넣었던 name prop을 객체로 전달하는데 이 때에도 string이 객체로 변한 것 외에는 Student는 항상 같은 화면을 보여주게 될 것입니다.
  • 그런데 위와 같은 상황에서 React.memo를 사용하고 부모 컴포넌트를 렌더링 해보면 예상과는 다르게 자식 컴포넌트까지 함께 렌더링이 됩니다.
  • 그 이유는 React.memo는 기본적으로 얕은 비교(swallow comparison)로 props의 변화를 감지하는데 참조 변수는 주솟값이 같은지를 확인하겠죠!
  • 하지만 부모 컴포넌트는 렌더링이 되면서 모든 변수와 함수를 초기화하므로 주솟값이 변하게 되고 React.memo는 props가 변했다고 판단해서 자식 컴포넌트를 렌더링시키는 것입니다.
const Student = ({ name, age, gender}) => {
    const equalComparison = (prev, cur) => {
        prev.name === cur.name;
    }

    return (
        <div>
            <h1>{name}</h1>
            <span>{age}</span>
            <span>{gender}</span>
        </div>
    );
}

export default React.memo(Student, equalComparison);
  • deep comparison을 수행하도록 하려면 memo의 두 번째 인자로 비교 함수를 전달하면 됩니다.
  • 리렌더링이 되기 전에 비교 함수를 거쳐서 비교 함수가 true를 반환하면 렌더링을 하지 않고 false를 반환할 때만 렌더링이 될 것입니다!

 

useMemo

  • useMemo는 메모이제이션 된 값을 반환합니다.
  • React.memo 또한 메모이제이션을 하지만 props를 비교하는 것과 차이가 있습니다.
  • 우선 바로 위 React.memo가 객체prop를 얕은 비교를 해서 렌더링 했던 경우에 useMemo를 사용하면 값 자체를 비교하기 때문에 비교 함수를 두 번째 인자에 넣어준 것과 동일하게 동작할 수 있고
  • 또한 하나의 컴포넌트에서 두 개 이상의 state가 있을 때도 활용할 수가 있습니다.
const School = (props) => {
    const name = useMemo(() => {
        return {
            lastName: '엄',
            firstName: '금동'
        };
    }, []);

    return (
        <Student
            name={name}
            age={10}
            gender={'male'}
        />
    );
}

 

useCallback

  • useCallback은 메모이제이션된 함수를 반환합니다.
  • 앞서 name 객체를 props로 넘겼을 때와 마찬가지로 함수 또한 참조 변수이기 때문에 부모 컴포넌트가 렌더링이 되면 새로운 주솟값을 갖게 되고 자식 컴포넌트에서는 props가 바뀌었다고 판단해서 함께 렌더링이 됩니다.
const School = (props) => {
    const meow = useCallback(() => {
        console.log("야옹");
    }, []);

    return (
        <Student
            name={'금동이'}
            age={10}
            gender={'male'}
            meow={meow}
        />
    );
}
  • useMemo를 사용했을 때와 마찬가지로 useCallback으로 사용하고자 하는 함수를 감싸주어서 사용할 수 있습니다.
  • 이렇게 되면 함수의 종속 변수가 변하지 않는 이상 굳이 함수를 새로 생성하지 않고 이전에 있던 참조 변수를 그대로 사용하여 리렌더링을 방지할 수 있습니다.

 

useCallback 예시

import React, { useState, useEffect } from 'react';

function Profile({ id }) {
  const [data, setData] = useState("");

  const fetchData = () =>
    fetch(`url/${id}`)
      .then(response => response.json())
      .then(({ data }) => data)

  useEffect(() => {
    fetchData().then(data => setData(data))
  }, [fetchData])
}
  • 위에서 fetchData는 함수이기 때문에 컴포넌트가 렌더링이 될 때마다 새로운 참조값으로 변경이 됩니다. 함수가 변경되었기 때문에 매번 useEffect가 실행되어 다시 렌더링 되어 무한루프게 빠지게 됩니다.

 

import React, { useState, useEffect } from 'react';

function Profile({ id }) {
  const [data, setData] = useState("");

  const fetchData = useCallback(
    () =>
      fetch(`url/${id}`)
        .then(response => response.json())
        .then(({ data }) => data),
    [id],
  )

  useEffect(() => {
    fetchData().then(data => setData(data))
  }, [fetchData])

  // ...
}
  • 이런 경우에 useCallback을 이용해서 props로 받는 id가 변하지 않는 한 함수의 참조값이 변하지 않도록 유지시킬 수 있습니다.

 

렌더링 최적화 결론

  • 함수 또는 컴포넌트가 동일한 입력으로 여러 번 호출되는 경우 메모이제이션은 유용할 수 있습니다.
  • 그러나 연산에 대한 결과가 자주 바뀌는 경우에는 메모이제이션은 쓸데없는 비교 연산과 메모리 낭비를 불러오겠죠?!
  • 따라서 연산이나 함수 자체가 복잡해서 다시 계산하는 데 비용이 많이 드는 경우처럼 적절한 곳에서만 사용해야겠습니다!

관련 자료

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함