[Wanted]_Week2-1_React 렌더링 최적화 & Advanced Hook

hanseungjune·2023년 7월 4일
0

Wanted

목록 보기
8/21
post-thumbnail

1. 렌더링

1-1) 렌더링이란?

  • 렌더링은 UI 요소를 화면에 그려내는 과정을 의미합니다.
  • React와 같은 UI 라이브러리 또는 프레임워크를 사용하는 이유는 렌더링 과정을 효율적으로 처리하기 위해서입니다.
  • 브라우저에서의 렌더링은 DOM 요소를 계산하고 화면에 그려내는 과정을 말합니다.
  • DOMCSSOM이 결합되어 최종적으로 브라우저에 그려집니다.
  • ReactUI를 선언적으로 작성하면 실제 렌더링은 React에서 처리하며, 개발자는 UI 설계에 집중할 수 있습니다.
  • React는 선언적인 개발 방식을 강조하는데, 이는 애플리케이션의 규모가 커져도 관리하기 쉽게 만들어줍니다.
  • React를 포함한 다른 라이브러리 및 프레임워크들은 애플리케이션에서 보여주고 싶은 핵심 UI를 "선언"하기만 하면 실제 DOM 조작은 라이브러리가 대신 처리합니다.
  • React 내부의 렌더링 과정을 최적화하기 위해 각 단계에서 렌더링이 언제 발생하고 어떤 과정을 거치는지 이해하는 것이 중요합니다.
  • 이를 통해 각 과정에서 렌더링을 최적화할 수 있습니다.

1-2) 리액트에서 리렌더링이 되는 시점

  • 리액트에서의 리렌더링은 state가 변경되었을 때 발생합니다.
  • 리액트의 주요 개념 중 하나는 UI와 상태(state)를 연동시키는 것입니다.
  • UI는 데이터를 가지고 있고, state는 변할 수 있는 데이터를 나타냅니다.
  • state를 사용하여 UI를 동적으로 업데이트할 수 있습니다.
  • setState 함수를 사용하여 state를 변경하고, 변경할 때마다 리렌더링이 수행됩니다.
  • 특정 컴포넌트의 state가 변경되면 해당 컴포넌트와 하위 컴포넌트들이 모두 리렌더링됩니다.
  • state의 변경을 통해 UI를 업데이트하므로, UI와 데이터를 연동시키는 핵심 원칙입니다.
  • 이를 이해하고 적절하게 활용하여 리액트 애플리케이션을 설계하고 최적화할 수 있습니다.

1-3) 리액트의 렌더링 과정

리액트에서의 렌더링 과정은 다음과 같은 단계로 이루어집니다:

  1. 기존 컴포넌트의 UI를 재사용할 지 확인한다.

    • 컴포넌트의 UI가 이전과 동일하다면 새로운 렌더링이 필요하지 않으므로 이전 결과를 재사용한다.
  2. 컴포넌트 함수를 호출하거나 클래스 컴포넌트의 render 메소드를 호출하여 새로운 Virtual DOM을 생성한다.

    • 컴포넌트 함수 또는 render 메소드가 실행되어 JSX 코드를 해석하여 Virtual DOM 객체를 생성한다.
  3. 이전의 Virtual DOM새로운 Virtual DOM을 비교하여 변경된 부분을 식별한다.

    • Virtual DOM 비교 알고리즘을 통해 이전과 새로운 Virtual DOM을 비교하고 변경된 요소를 찾아낸다.
  4. 변경된 부분만 실제 DOM에 적용하여 화면을 업데이트한다.

    • 변경된 요소들에 대해 DOM 조작을 수행하여 화면에 변경 사항을 반영한다.

이러한 과정을 통해 리액트는 Virtual DOM을 활용하여 필요한 최소한의 DOM 조작만 수행하여 브라우저의 성능을 최적화한다. 리액트는 Virtual DOM을 통해 변경 사항을 비교하고 일부분만 업데이트함으로써 전체 화면을 다시 그리는 비용을 최소화하여 효율적인 렌더링을 가능하게 한다.

브라우저는 근본적으로 화면을 보여주기 위해서 HTML, CSS, JavaScript를 다운로드 받고 그를 처리해서 화면에 픽셀 형태로 그려냅니다. 그리고 이 과정을 CRP(Critical Rendering Path)라고 부릅니다.

Critical Rendering Path는 기본적으로 아래의 과정을 수행합니다.

  1. HTML을 파싱해서 DOM을 만든다.
  2. CSS를 파싱해서 CSSOM을 만든다.
  3. DOM과 CSSOM을 결합해서 Render Tree를 만든다.
  4. Render Tree와 Viewport의 width를 통해서 각 요소들의 위치와 크기를 계산한다.(Layout)
  5. 지금까지 계산된 정보를 이용해 Render Tree상의 요소들을 실제 Pixel로 그려낸다. (Paint)

리액트에서는 UI의 변화를 처리하기 위해 DOM 조작이 필요합니다. 하지만 개별적인 DOM 조작은 브라우저에 많은 연산을 요구하고 성능 저하를 야기할 수 있습니다. 따라서 리액트는 이런 문제를 해결하기 위해 Virtual DOM이라는 개념을 도입합니다.

리액트에서 UI의 변화가 발생하면 변경 사항을 바로 실제 DOM에 적용하는 대신, 리액트가 관리하는 Virtual DOM 객체 형태로 만들어냅니다. 이전의 Virtual DOM새로운 Virtual DOM을 비교하여 변경된 부분을 식별한 후, 실제로 변경이 필요한 DOM 요소들만 찾아내고 한 번에 조작합니다.

이러한 처리를 통해 리액트는 브라우저에서 수행되는 Critical Rendering Path (CRP)의 빈도를 줄이는 최적화를 수행합니다. 즉, 이전의 Virtual DOM새로운 Virtual DOM을 비교하여 변경된 부분만 실제 DOM에 적용함으로써 최소한의 DOM 조작만 수행하고 성능을 향상시킵니다. 이 최적화는 리액트 내부에서 처리되므로 개발자는 별도의 최적화를 수행할 필요가 없습니다.

따라서 개발자가 할 수 있는 최적화는 기존 컴포넌트의 UI를 재사용하는지 확인하는 것과 새로운 Virtual DOM을 생성할 때 최적한 형태로 만드는 것입니다. 이를 통해 리액트는 효율적인 렌더링을 위해 Virtual DOM을 활용하고, 불필요한 DOM 조작을 최소화하여 성능을 개선합니다.

리액트를 사용하는 개발자가 할 수 있는 최적화 방법은 다음과 같습니다:

  1. 기존 컴포넌트의 UI를 재사용할 지 확인하기:

    • 리렌더링 될 컴포넌트의 UI가 이전의 UI와 동일한지 확인합니다.
    • 동일한 경우, 새로운 컴포넌트 함수를 호출하는 대신 이전의 결과값을 그대로 사용하여 리렌더링을 방지합니다.
    • 이를 통해 불필요한 컴포넌트 함수 호출과 Virtual DOM 생성을 줄여 최적화할 수 있습니다.
  2. 새로운 Virtual DOM을 생성하기:

    • 컴포넌트 함수가 호출될 때 새로운 Virtual DOM을 생성하는 과정에서 최적화를 수행합니다.
    • 새로운 Virtual DOM의 형태를 이전과 비교하여 차이가 적은 형태로 만들어질 수 있도록 설계합니다.
    • 예를 들어, UI를 변경하기 위해 태그의 종류를 변경하는 것보다는 클래스 이름 등의 속성만 변경하여 Virtual DOM의 차이를 최소화합니다.
<div> -> <span> (x)
<div className="block" /> -> <div className="inline"> (o)

위 두 가지 최적화 방법을 적절히 활용하여 리액트 컴포넌트의 불필요한 리렌더링과 Virtual DOM 생성을 줄일 수 있습니다. 이를 통해 성능 향상과 효율적인 렌더링을 달성할 수 있습니다.

2. React.memo

리액트에서 컴포넌트의 UI를 재사용하기 위한 최적화 방법:

  • 리액트에서는 state가 변할 경우 해당 컴포넌트와 하위 컴포넌트를 모두 리렌더링한다.
  • 하지만 하위 컴포넌트의 경우 props가 변하지 않았다면 UI가 변화하지 않았을 수도 있다.
  • UI가 변화되었는지 여부를 매번 모든 컴포넌트 트리를 순회하여 확인하는 것은 비효율적이다.
  • 리액트에서는 React.memo 함수를 제공하여 개발자가 컴포넌트의 리렌더링 여부를 결정할 수 있다.
  • React.memo는 컴포넌트를 래핑하여 이전에 렌더링된 결과를 기억하고, props가 변경되었을 때에만 리렌더링을 수행한다.
  • 개발자는 React.memo를 사용하여 컴포넌트의 UI를 재사용할 지 판단할 수 있다.
  • 이전 결과와 현재 props를 비교하여 UI가 동일하다고 판단되면 컴포넌트 함수 호출을 피하고 이전 결과를 재사용한다.
  • 이를 통해 효율적인 UI 업데이트를 달성할 수 있다.

따라서, React.memo를 활용하여 컴포넌트의 리렌더링 여부를 결정하고 이전 결과를 재사용함으로써 효율적인 UI 업데이트를 수행할 수 있다.

2-1) React.memo

리액트에서 컴포넌트의 리렌더링을 제어하기 위해 React.memo를 사용하는 방법:

  1. React.memoHOC(Higher Order Component)로, 컴포넌트를 인자로 받아 컴포넌트를 리턴하는 형태의 함수이다.
  2. React.memo로 감싸진 컴포넌트는 상위 컴포넌트가 리렌더링될 때 모든 경우에 리렌더링되는 것이 아니라, 이전 props다음 렌더링 때 사용될 props를 비교하여 차이가 있을 경우에만 리렌더링을 수행한다.
  3. 리렌더링이 필요하지 않을 때에는 이전의 렌더링 결과를 재사용한다.
  4. React.memo의 두 번째 인자로는 변화를 판단하는 함수를 전달할 수 있다.
  5. 이 함수는 이전 props와 새로운 props를 순서대로 인자로 받아와 비교하며, true를 반환할 경우 이전 결과를 재사용하고 false를 반환할 경우 리렌더링을 수행한다.
  6. areEqual(prevProps, nextProps)와 같이 변화를 판단하는 함수를 작성하여 React.memo의 두 번째 인자로 전달할 수 있다.
  7. 변화를 판단하는 함수에서 true를 반환하면 이전 결과를 재사용하고, false를 반환하면 리렌더링을 수행한다.

아래는 React.memo와 변화를 판단하는 함수를 사용한 코드 예시:

function MyComponent(props) {
  /* render using props */
}

function areEqual(prevProps, nextProps) {
  /* 
  true를 반환할 경우 이전 결과를 재사용
  false를 반환할 경우 리렌더링을 수행
  */
}

export default React.memo(MyComponent, areEqual);

이 예시에서는 MyComponentReact.memo로 감싸고, 변화를 판단하는 함수 areEqual을 두 번째 인자로 전달하였다. areEqual 함수는 이전 props새로운 props를 비교하여 true나 false를 반환하며, 반환 값에 따라 리렌더링 여부가 결정된다.

2-2) 자바스크립트 데이터 타입

자바스크립트의 데이터 타입:

  • 자바스크립트의 데이터 타입에는 기본형 타입참조형 타입이 있다.
  • 기본형 타입(원시형 타입)은 원시적이고 독립적인 형태로 존재하는 데이터로서, string, number, boolean, null, undefined, bigint, symbol 등이 있다.
  • 참조형 타입(객체형 타입)은 여러 데이터를 모아서 만든 객체로서, 객체, 배열, 함수 등이 있다.

React.memo와 데이터 타입:

  • React.memo는 기본적으로 props 객체 간의 비교를 통해 동작한다.
  • React.memo를 사용할 때, props의 데이터 타입에 대한 이해가 필요하다.
  • 기본형 타입(props의 값이 변경되면 항상 다른 객체로 취급됨)과 참조형 타입(props의 참조가 변경되지 않으면 같은 객체로 취급됨)을 구분하여 처리해야 한다.
  • 기본형 타입의 경우, 값이 변경될 때마다 리렌더링이 발생한다.
  • 참조형 타입의 경우, 객체의 참조가 변경되지 않으면 이전 결과를 재사용하고 리렌더링을 방지한다.

2-3) 불변성

객체의 가변성과 불변성에 관한 요약:

  • 원시형 타입(기본형 타입)은 불변성(immutable)을 가집니다. 변수에 할당된 값은 변경할 수 없으며, 변경이 필요한 경우에는 새로운 값을 만들어 변수에 재할당합니다.
  • 참조형 타입(객체형 타입)은 가변성(mutable)을 가집니다. 객체는 여러 타입의 데이터를 모아놓은 형태이며, 내부의 값들을 언제든지 변경할 수 있습니다.

가변성과 비교:

  • 가변성은 메모리를 절약하고 객체를 유연하게 사용할 수 있는 장점이 있지만, 객체의 내용이 예상하기 어려워질 수 있고 객체 간의 비교가 어려워집니다.
  • 자바스크립트에서는 객체를 비교할 때 객체의 메모리 주소를 기준으로 일치 여부를 판단합니다. 내용이 동일한 객체라도 메모리 주소가 다르기 때문에 일치하지 않는 결과가 나올 수 있습니다.

불변성의 활용:

  • 객체를 불변하게 유지하는 것은 객체를 변경하지 않고 새로운 객체를 생성하여 교체하는 방식을 의미합니다.

  • 불변성을 유지하는 경우, 객체를 비교할 때 내부의 값이 변경되었는지를 일일이 확인하지 않고도 객체의 동등성을 판단할 수 있습니다.

  • 불변성을 유지하면 객체의 변경을 추적하기 쉬워지고, 객체를 사용하는 다양한 컴포넌트나 함수에서 예상 가능한 동작을 보장할 수 있습니다.

예시와 요약:

예시 1:

let dog = "tori";
dog = "mozzi";
  • dog 변수에는 새로운 문자열 "mozzi"가 할당되며, 기존의 문자열 "tori"는 변경되지 않고 새로운 객체로 교체됩니다.

예시 2:

const yeonuk = { name: "yeonuk", gender: "male" };
yeonuk.name = "charlie";
  • yeonuk 객체의 name 속성을 "charlie"로 변경할 수 있습니다. 객체 자체의 참조는 변경되지 않고 내부의 값을 변경하는 것입니다.

요약:

  • 자바스크립트의 원시형 타입은 불변하며, 변경이 필요한 경우 새로운 값을 할당합니다.
  • 객체형 타입은 가변하며, 객체의 내용을 언제든지 변경할 수 있습니다.
  • 객체의 가변성은 객체 간 비교를 어렵게 만들 수 있습니다.
  • 객체를 불변하게 유지하면 예상 가능한 동작과 객체 비교의 용이성을 얻을 수 있습니다.

2-4) memo의 잘못된 활용

React.memo에서의 props 비교shallow compare에 관한 요약:

  • React.memo는 기본적으로 props의 변화를 판단하기 위해 shallow compare를 수행합니다.
  • props는 객체 형태로 표현되며, 매 렌더링마다 새로운 props 객체가 생성됩니다. 따라서 props 객체 자체를 비교하는 것은 의미가 없습니다.
  • React.memo는 props 객체 안의 각 property를 개별적으로 비교합니다.
  • 비교는 Object.is(===) 연산자를 사용하여 수행됩니다. 이 연산자는 두 값이 정확히 같은지 비교하며, 하나라도 false가 나오면 props가 변경되었다고 판단하여 리렌더링을 수행합니다.

예시와 요약:

<Component name="foo" hello="world" object={{first:1, second:2}} array={[1,2,3]}  />

<Component name="bar" hello="world" object={{first:1, second:2}} array={[1,2,3]}  />

const areEqual = (prevProps, nextProps) => {
  if (prevProps.name !== nextProps.name) return false;
  if (prevProps.hello !== nextProps.hello) return false;
  if (prevProps.object !== nextProps.object) return false;
  if (prevProps.array !== nextProps.array) return false;

  return true;
}
  • 위의 예시에서는 두 개의 <Component> 컴포넌트가 있습니다.
  • React.memo를 사용하여 컴포넌트를 래핑할 때 areEqual 함수를 통해 props의 비교를 수행합니다.
  • areEqual 함수prevPropsnextProps를 받아 각 property들을 개별적으로 비교합니다.
  • props 객체의 각 property들이 변경되었는지를 체크하여 변경이 있을 경우 false를 반환하고, 모든 property들이 같을 경우 true를 반환합니다.
  • 이를 통해 React.memoprops의 변경 여부를 판단하여 리렌더링을 수행할지 결정합니다.

3. Memoization

Memoization은 특정 값을 저장해두고 재사용하는 테크닉을 의미합니다. 함수 컴포넌트는 매 렌더링마다 함수가 호출되기 때문에 이전 호출과 새로운 호출 간에 값을 공유할 수 없습니다. 하지만 리액트는 함수 컴포넌트에서 값을 memoization할 수 있는 API를 제공합니다.

요약:

  • Memoization은 값을 저장해두고 재사용하는 테크닉입니다.
  • 함수 컴포넌트는 매 렌더링마다 호출되기 때문에 이전 호출과 새로운 호출 간에 값을 공유할 수 없습니다.
  • 리액트는 함수 컴포넌트에서 값을 memoization할 수 있는 API를 제공합니다.

리액트에서 제공하는 memoization 관련 API:

  • React.memo: 컴포넌트의 출력 결과를 memoization하여 이전 결과와 동일한 경우 리렌더링을 방지합니다.
  • useMemo: 특정 계산 값을 memoization하여 의존성이 변경되지 않는 한 이전 값이 재사용됩니다.
  • useCallback: 특정 함수를 memoization하여 의존성이 변경되지 않는 한 이전 함수가 재사용됩니다.

이러한 API를 활용하여 함수 컴포넌트 내에서 값을 memoization할 수 있으며, 이는 성능 향상과 불필요한 계산을 방지하는 데 도움을 줍니다.

1. useMemo

useMemo는 리액트에서 값을 memoization할 수 있도록 해주는 훅입니다.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo는 두 개의 인자를 받습니다. 첫 번째 인자는 콜백 함수이며, 이 함수의 반환값이 메모됩니다. 두 번째 인자는 의존성 배열입니다. 의존성 배열에 포함된 값들 중 하나라도 이전 렌더링과 비교했을 때 변경되었다면, 메모된 값을 활용하는 대신에 새로운 값을 다시 계산합니다.

위의 예시에서 a와 b는 메모이제이션을 위해 사용되는 두 개의 변수입니다. 만약 a나 b가 변경되면, 이전에 메모된 값을 사용하는 대신에 함수 내부의 코드가 다시 실행되어 새로운 값을 계산합니다. 이를 통해 의도한 결과를 유지할 수 있습니다.

2. useCallback

const memorizedFunction = useMemo(() => () => console.log("Hello World"), []);

const memorizedFunction = useCallback(() => console.log("Hello World"), []); => 이게 더 좋음
  • useCallback은 useMemo를 함수 콜백에 적용하기 편리하게 만든 훅입니다.
  • useMemo를 사용하여 함수를 메모이제이션할 때는 콜백 함수를 반환하는 형태가 되는데, 이를 useCallback을 사용하면 간소화할 수 있습니다.
  • useCallback을 사용하면 콜백 함수를 직접 반환할 필요 없이 간단하게 함수를 메모이제이션할 수 있습니다.
  • 의존성 배열에는 함수 내에서 참조하는 외부 변수를 포함해야 합니다.

3. 언제 memoization을 해야 할까?

  • 메모이제이션은 특정 상황에서 효율적입니다.
  • 명확한 목적 없이 메모이제이션을 사용하는 것은 비효율적일 수 있습니다.
  • 메모이제이션을 사용하기 전에 값을 새로 생성하는 비용과 메모이제이션 함수를 호출하고 의존성을 비교하여 가져오는 비용을 고려해야 합니다.
  • 리액트에서 메모이제이션은 복잡한 연산을 수행하는 경우나 값의 동일성을 보장하고 싶을 때 사용할 수 있습니다.
  • 메모이제이션된 객체를 사용하면 객체의 동일성이 보장되어 리렌더링을 최적화할 수 있습니다.
// 1번 상황 - 복잡한 연산
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

// 2번 상황 - 값의 동일성 보장
const memoizedObject = useMemo(() => ({ a, b }), [a, b]);

// useCallback과 React.memo와 함께 사용하는 경우
const memoizedCallback = useCallback(() => {
  // 콜백 함수 로직
}, [dependency]);

const MemoizedComponent = React.memo(Component);
  1. 새로운 값을 만드는 연산이 복잡한 경우:

    • useMemo를 사용하여 복잡한 연산 결과를 메모이제이션
    • 의존성 배열에 변경 감지할 변수들을 포함
  2. 함수 컴포넌트의 이전 호출과 다음 호출 사이에 값의 동일성을 보장하고 싶은 경우:

    • useMemo를 사용하여 객체나 값을 메모이제이션
    • 의존성 배열에 변경 감지할 변수들을 포함
  3. useCallback과 React.memo와 함께 사용하는 경우:

    • useCallback을 사용하여 콜백 함수를 메모이제이션
    • React.memo를 사용하여 컴포넌트를 메모이제이션

Aha-Moment) 언제 최적화를 해야할까?

최적화는 매력적인 주제이지만, 비용이 들고 추가적인 노력과 시간이 필요합니다. 따라서 항상 최적화를 할 때는 가치 창출과 관련하여 신중하게 판단해야 합니다.

주요 사항:

  • 최적화는 공짜가 아닙니다. 최적화를 위한 코드 추가는 프로젝트의 복잡도를 증가시킬 수 있습니다.
  • 최적화를 위해 개발자의 시간과 노력이 필요하며, 비용이 발생할 수 있습니다.
    현업에서의 개발자는 가치를 창출해내야 합니다. 개발자의 자기만족을 위한 개발보다는 실제 가치 창출이 중요합니다.
  • 최적화는 성능 이슈가 발생했거나 발생 가능성이 있는 상황에서 필요성이 있는 경우에 수행해야 합니다.
  • 최적화를 수행하기 전에 현재 상황을 분석하고, 최적화의 이유를 정리하여 관련된 사람들에게 알리고 공감대를 형성해야 합니다.

코드 예시 및 리스트 형태로 요약:

  1. 최적화는 가치 창출과 관련하여 신중하게 판단해야 합니다.
  2. 최적화를 위해 코드를 추가하면 복잡도가 증가할 수 있습니다.
  3. 최적화에는 개발자의 시간과 노력이 필요하며 비용이 발생할 수 있습니다.
  4. 현업에서는 가치를 창출해야 하므로 최적화는 필요성이 있는 상황에서 수행해야 합니다.
  5. 최적화를 수행하기 전에 현재 상황을 분석하고 관련된 사람들에게 알려 공감대를 형성해야 합니다.

이러한 점들을 고려하여 최적화를 수행하면 효과적으로 성능 개선을 할 수 있습니다.

profile
필요하다면 공부하는 개발자, 한승준

0개의 댓글