React : Re-rendering 과 최적화

김대은·2022년 7월 31일
0


리액트 마크 겁나 이쁨...

렌더링

리액트에서 렌더링은 컴포넌트의 정보를 이용해 화면을 보여주는 것을 말한다.
렌더링이 많아지면 속도가 저하되고 메모리 사용량이 높아진다.
하지만 많아지더라도 꼭 필요한 렌더링 과정이라면 어쩔 수 없다.

하지만 전혀 필요하지 않은 렌더링이 발생하게된다면???
효율성이 급격히 떨어지게 된다.
이러한 요소들이 많아질수록 같은 결과를 출력하지만 속도 차이가 많이 나게 될 것이다.
심해지면 UX가...

같은 결과 화면이라도 불필요한 렌더링을 줄이는 것은 중요하다.

리렌더링 조건

  1. State 변경
    State가 업데이트 되면 리렌더링이 발생한다.
    리액트에서 State값이 변경되면 관련 컴포넌트들을 전부 리렌더링 한다.
    리액트는 변화를 바로바로 감지하여 화면에 변경사항을 보여주기 때문이다.

    2.Props 변경
    Props가 업데이트 되면 리렌더링을 한다.
    Props가 변경되는 건 부모 컴포넌트의 State도 변경이 일어난다는 의미이다.

    부모 컴포넌트의 State가 변경되면 Props도 업데이트 되고,
    모든 하위 컴포넌트에 대해 리렌더링이 발생한다.

    3.부모 컴포넌트가 리렌더링 될때

성능 향상을 위한 Memoization

Memoization의 정의는 아래와 같다.

  • 결과를 캐싱하고, 다음 작업에서 캐싱한 것을 재사용 하는 비싼 작업의 속도를 높이는 자바스크립트 기술
  • 이전 값을 메모리에 저장해 동일한 계산의 반복을 제거해 빠른 처리를 가능하게 하는 기술
  • 캐시에 초기 작업 결과를 저장하여 사용함으로 써 최적화 할 수 있다. 만약 작업을 다시 수행해야 한다면, 어딘가에 저장되어진 동일한 결과를 단순히 반환 해준다.

Re-rendering 최적화 방법

1. useMemo

이 함수는 React hook 중 하나로서 리액트에서 CPU소모가 심한 함수들을
캐싱 하기 위해 사용된다.
만약 컴포넌트 내의 어떤 함수가 값을 리턴하는데,
하나의 변화에도 값을 리턴하는데 많은 시간을 소요한다면 이 컴포넌트가 리렌더링 될때마다
함수가 호출되면서 많은 시간을 소요하게 될것이다.
또 그 함수가 return되는 값이 자식 컴포넌트에도 사용이 된다면,
그 자식 컴포넌트도 함수가 호출 될 때마다 새로운 값을 받아 리렌더링 된다.

Memo 는 "memoized" 를 의미하는데
이전에 계산 한 값을 재사용한다는 의미를 가지고 있다.

useMemo(( ) => Function,[deps])

Function 은 캐싱 하고싶은 함수이고,
deps 는 useMemo가 캐싱할 함수에 대한 입력의 배열로서,
해당 값들이 변경되면 함수가 호출된다.
useMemo는 deps의 상태가 변하지 않으면 함수를 굳이 다시 호출하지 않고 이전에 반환한 참조값을 재사용 한다.
즉, 함수 호출 시간도 세이브할 수 있고 같은 값을 props로 받는 하위 컴포넌트의 리렌더링도 방지할 수 있다

2. React.memo

export default memo(Component)

React.memo는 위와 같이 사용되며 직접 컴포넌트를 감싸서 사용한다.
memo 는 컴포넌트의 props가 바뀌지 않앗다면, 리렌더링을 방지하여 컴포넌트의 렌더링 성능을
최적화 해줄 수 있다.
이 함수를 사용한다면, 컴포넌트에서 리렌더링이 필요한 상황에서만 리렌더링을 하도록 설정해준다.

React.memo는 props를 비교할 때 얕은 비교를 진행하는데,
원시 값의 경우는 같은 값을 갖는지 확인하고 객체나 배열과 같은 참조 값은 같은 주소 값을 갖고 있는지 확인한다.

React.memo는 매번 사용해야 할까?

공식 문서에는 무분별한 사용을 지양 할 것을 권장한다.
그 이유는 이를 사용하는 코드와 메모제이션용 메모리가 추가로 필요하게되고,
최적화를 위한 연산이 불필요한 경우엔 비용만 발생시키기 때문이다.
쉽게 말해서 그냥 이거 많이쓰면 메모리 많이 먹는다.

언제 React.memo를 사용할까?

  • 함수형 컴포넌트에 같은 props에 같은 렌더링 결과를 제공할 경우
  • UI element의 양이 많은 컴포넌트의 경우
  • Pure Functional Component 경우

주의사항

부모가 전달하는 callback 함수의 경우 매 렌더링 마다 새로운 함수가 props로 전달 되어
자식 컴포넌트는 리 랜더링이 일어난다.

 <Button handleOnClick={()=>handleOnClick(btn)} />

해결 방안으로는 인라인 함수를 넘겨주는게 아니고 함수 자체를 넘겨 주는것이다.

 <Button handleOnClick={handleOnClick} />

3. useCallback

const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

useMemo가 리턴되는 값을 memoize 시켜주었는데,
useMemo와 비슷한 useCallback은 함수 선언을 memoize 하는데 사용된다.

import React.{memo} from "react";

function Button({ onClick }) {
    console.log("Button component render");

  return (
    <button type="button" onClick={onClick}>
      버튼
    </button>
  );
}

export default memo(Button);

여기서 Button 컴포넌트는 불필요한 렌더링을 막기 위해 memo를 이용하여 memoize 되어 있다.
React.memo는 현재와 다음 props를 비교하여 이전 props와 같다면 컴포넌트를 리렌더링 하지 않는다.

Button 컴포넌트는 onClick props를 함수로 받고 있는데
언제든 부모 컴포넌트가 리렌더링 될 때 Button에게 전달되는 onClick props가 동일한지 체크한 후 동일하다면 리렌더링 되지 않아야 한다.

하지만 이 경우에 Button 컴포넌트도 같이 리렌더링 되는 문제가 발생되는데, 이 상황에선 Button 컴포넌트에 memo로 감싸도 소용이 없다.

그 이유는 함수는 객체이고, 새로 생성된 함수는 다른 참조 값을 가지기 때문에
Button 입장에서는 새로 생성된 함수를 받을 때 props가 변한 것으로 인지하기 때문이다.

그래서 이럴때 useCallback을 써야한다.

상위 컴포넌트에서 하위컴포넌트로 함수를 props로 넘겨줄 때, 상위 컴포넌트가 리렌더링 될 때마다 상위 컴포넌트 안에 선언된 함수를 새로 생성하기 때문에 그때마다 새 참조 함수를 하위 컴포넌트로 넘겨주게 된다.
이에 따라 하위 컴포넌트도 props가 달라졌으므로 또다시 리렌더링 하게 된다.

그러나 useCallback으로 함수를 선언해주면, 종속 변수들이 변하지 않는 이상 굳이 함수를 재생성하지 않고 이전에 있던 참조 변수를 그대로 하위 컴포넌트에 props로 전달하여, 하위 컴포넌트도 props가 변경되지 않았다고 인지하게 되어 하위 컴포넌트의 리렌더링을 방지할 수 있다.

useCallback(fn, deps)은 useMemo(() => fn, deps)와 같다.

4. 자식컴포넌트의 props로 객체를 넘겨줄 경우 변형하지말고 넘겨주기

흔히 작업을 하다보면 props의 값으로 객체를 넘겨주는 경우가 많은데, 이때 props로 전달하는 형태에 주의 하여야 한다.

// 생성자 함수
<Component prop={new Obj("x")} />
// 객체 리터럴
<Component prop={{property: "x"}} />

이런 경우 새로 생성된 객체가 props로 들어가므로 컴포넌트가 리렌더링 될 때마다 새로운 객체가
생성되어 자식 컴포넌트로 전달된다.
props로 전달한 객체가 동일한 값이어도 새로 생성된 객체는 이전 객체와 다른 참조 주소를 가진 객체이기 때문에
자식 컴포넌트는 메모이제이션 되지 않는다.

차라리, 생성자 함수나 객체 리터럴로 객체를 생성 해서 하위 컴포넌트로 넘겨주는 방식이 아닌,
state를 그대로 하위 컴포넌트에 넘겨주어 필요한 데이터를 하위 컴포넌트에서 변경 해주는것이 좋다.

5. 컴포넌트를 매핑할 때에는 key값으로 index를 사용하지 않는다.

공식문서가 하지 말래요 !!
공식문서가 하지 말래요 !!

사람들이 많이 하는 실수 주에 하나가 바로 컴포넌트를 매핑할 때 key값에 index를 넣어준다.
리액트에서 매핑할 때 고유한 key값을 넣어달라고 말하는데, 이걸 index로 넣으면 좋지 않다.

배열에 중간에 어떤 요소가 삽입될때 그 중간 이후에 위치한 요소들은 전부 index가 변경된다.
이로 인해 key값이 변경되어 리액트는 key가 동일할 경우, 동일한 DOM Element를 보여주기 때문에
예상치 못한 문제가 발생 할 수 있다.
또한 데이터가 key와 매치가 안되어서 서로 꼬이는 부작용도 발생 할 수 있다.

그러면 index 요소는 반드시 사용하면 안되는 걸까?

배열의 요소가 필터링, 정렬 삭제, 추가 등의 기능이 들어간다면 문제가 발생할수 있으나 다음과 같은 경우에서는 index로 사용해도 무방다.
다만, 가급적이면 코드의 일관성을 위해 최대한 index 를 사용 안하는 것을 권장한다.

  • 배열과 각 요소가 수정, 삭제, 추가 등의 기능이 없는 단순 렌더링만 담당하는 경우
  • id로 쓸만한 unique 값이 없을 경우
  • 정렬 혹은 필터 요소가 없어야 함

6.useState의 함수형 업데이트

기존의 useState를 사용하며, 대부분 setState시에 새로운 상태를 파라미터로 넣어주었다.
setState를 사용할 때 새로운 상태를 파라미터로 넣는 대신,
상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣을 수도 있는데,
이렇게 하면 useCallback을 사용할 때 두 번째 파라미터로 넣는 배열에 값을 넣어주지 않아도 된다.

// 예시) 삭제 함수 
const onRemove = useCallback(
  id => {
    setTodos(todos.filter(todo => todo.id !== id));
  },
  [todos],
);

// 예시) 함수형 업데이트 후
const onRemove = useCallback(id => {
  setTodos(todos => todos.filter(todo => todo.id !== id));
}, []);

7.제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled Component)

흔히 input 에다가 onChange 넣어서 리랜더링을 무지막지하게 뽑아내는 경우가 있다.

React에서는 Form(<input>, <textarea>, <select>)을 다루는 2가지 방법이 있는데, 바로 제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled Component)이다

Controlled Component

공식문서 참조

Uncontrolled Component

비제어 컴포넌트(Uncontrolled Component)는 ref를 통해서 form에 접근하는데 기존의 Vanilla JavaScript와 비슷하다.
Vanilla JavaScript에서 input값을 입력하고 button을 클릭하여 form을 제출할 때 값을 얻어왔다.
이와 유사한 방식으로 비제어 컴포넌트(Uncontrolled Component)도 form을 제출할 때 실행되는 함수 내에서 ref를 통해 값을 얻어올 수 있다.

제어 컴포넌트와 비제어 컴포넌트 , 어떨때 사용할까 ?

Controlled Component는 입력한 데이터 상태와 저장한 데이터의 상태가 항상 일치한다.
즉, 사용자가 입력할때마다 상태가 변경이되고 리렌더링 된다.

그래서 실시간으로 상태를 보여주어야할 때는 Controlled Component를 사용하고,
불필요한 렌더링을 줄이고 submit 시에만 데이터값이 필요할때는 Uncontrolled Component를 사용하자

반드시 참고할 내용

리액트의 useCallback useMemo, 정확하게 사용하고 있을까

profile
매일 1% 이상 씩 성장하기

0개의 댓글