리액트 재조정

그니·2023년 7월 31일

react

목록 보기
1/1
post-thumbnail
  1. React는 선언적 API를 제공하기 때문에 갱신이될 때마다 매번 무엇이 바뀌었는지 걱정하지 않아도 된다.

React의 비교 알고리즘을 만들 때 어떤 선택을 했는지

  1. render() 함수는 "React의 엘리먼트 트리 를 만드는 것이다." 라고 생각이 드는 순간이 있다.
  2. state, props가 갱신되면 render() 함수는 새로운 React 엘리먼트 트리를 반환할 것이다.
  3. 이때 React는 방금 만들어진 React 엘리먼트 트리에 맞게 가장 효과적으로 ui를 갱신하는 방법을 알아낼 필요가 있다.
  4. 이러한 트리를 다른 트리로 변환하기 위한 최소한의 연산 수를 구하는 알고리즘을 풀기 위한 일반적인 해결책이 있긴 하지만,
  5. n개의 엘리먼트가 있는 트리에 대해 O(n3승) 복잡도를 가진다.
  6. React에 해당 알고리즘을 적용 시, 1000개의 엘리먼트를 그리기 위해 10억 번의 비교 연산을 수행해야 한다.

그래서 React는 2가지 가정을 기반하여 O(n) 복잡도의 휴리스틱 알고리즘을 통해 이를 개선함.

휴리스틱 알고리즘

  • 컴퓨터로 어떤 문제를 해결하기 위한 명확한 절차가 없거나 또는 있다할지라도 그것이 방대한 시간과 비용을 필요로 하는 경우에, 일반적으로 경험적 지식을 도입하여 순차적이며 체계적으로 그 해결방법을 산출해 가는 방법.

2가지 가정
1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.

  1. 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해줄 수 있다.

비교 알고리즘

  1. 두 개의 엘리먼트 트리를 비교할 때, React는 두 엘리먼트의 root 엘리먼트로 부터 비교한다.
  2. 이후의 동작은 루트 엘리먼트의 타입에 따라 달라진다.

엘리먼트의 타입이 다른 경우

  • 두 루트 엘리먼트의 타입이 다르면, React는 이전 트리를 버리고 완전히 새로운 트리를 구축한다.
<a> -> <img>
<Article> -> <Comment>
<Button> -> <div>
  1. 트리를 버릴 때 이전 DOM 노드들은 모두 파괴된다.
    -> 컴포넌트 인스턴스의 경우 componentWillUnmount() 가 실행된다.
  2. 새로운 트리가 만들어질 때, 새로운 DOM 노드들이 DOM에 삽입된다.
    -> 컴포넌트 인스턴스의 경우 UNSAFE_componentWillMount() 가 실행되고
    -> 이어 componentDidMount()가 실행된다.
  3. 이전 트리와 연관된 모든 state는 사라진다.

루트 엘리먼트 아래의 모든 컴포넌트도 Unmount되고, state또한 사라진다.

// 두 루트 엘리먼트의 타입이 다름.
<div>
  <Counter />
</div>

<span>
  <Counter />
</span>
  • 위의 경우 보여지는 모습이 같더라도 React 구조는 서로 다르다.
<div>
 {isToggle ? <Counter /> : <Counter />
</div>

<div>
 {isToggle ? <div><Counter /></div> : <span><Counter /></span>
</div>
  • A는 Counter의 state는 유지된다. -> 루트 엘리먼트 타입이 같기에.
  • B는 Counter의 state가 유지되지 않는다. -> 루트 엘리먼트 타입이 다름.

DOM 엘리먼트의 타입이 같은 경우

  • 같은 타입의 두 React DOM 엘리먼트를 비교할 때, React는 두 엘리먼트의 속성을 확인하여 동일한 내역은 유지하고 변경된 속성들만 갱신한다.
<div className="before" title="stuff" />

<div className="after" title="stuff" />
  • 위 두 엘리먼트를 비교하면, React는 현재 DOM노드 상에 className만 수정한다.

DOM 노드의 처리가 끝나면, React는 이어서 해당 노드의 자식들을 재귀적으로 처리한다.

  • Root 노드의 변화에 따라 자식 노드도 변화함.

같은 타입의 컴포넌트 엘리먼트

  • 컴포넌트가 갱신되면 인스턴스는 동일하게 유지되어 렌더링 간 state가 유지된다.
  • React는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트의 인스턴스의 props를 갱신한다.
  • 해당 인스턴스의 componentWillReceiveProps, WillUpdate, DidUpdate를 호출한다.
  • 다음으로 render() 메서드가 호출되고 비교 알고리즘이 이전 결과와 새로운 결과를 재귀적으로 처리한다.

자식에 대한 재귀적 처리

  1. React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다.
<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>
  • 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다.
  • 위와 같이 단순하게 구현 시, 리스트의 맨 앞에 엘리먼트를 추가하는 경우 성능이 좋지 않다. 변경을 한번 더 순회 후 생성 -> 비효율.
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>
  • 굳이 변경하지 않아도 되는 Duke, Villanova 종속 트리를 그대로 유지하는 대신, 모든 자식을 변경한다.

keys

  1. 이러한 문제를 해결하기 위해, React는 key 속성을 지원한다.
  2. 자식들이 key를 가진다면, React는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인한다.
<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>
  1. 2014 key 를 가진 엘리먼트가 새로 추가
  2. 2015, 2016 key를 가진 엘리먼트는 그저 이동만 하면 되는 것을 React가 알 수 있다.
<li key={item.id}>{item.name}</li>
  1. 해당 key 는 오로지 형제 사이에서만 유일하면 된다. 전역에서 유일할 필요는 없음.
  2. 배열의 인덱스를 사용할 경우 배열이 재배열될 경우 key가 형제 사이에서 얽힌다.
    -> key를 사용하는 이유가 퇴색됨. -> 재조정 과정에서 형제 사이에 key를 통해 변경이 필요한지 안한지 React에게 알려줄 수 있는데, 배열이 재조정되버리면 다시 모든 엘리먼트를 비교해야함.

재조정 알고리즘은 구현상의 세부사항이라는 것을 명심.

  • React는 항상 전체 앱을 재렌더링할 수 있지만, 최종적으로 출력되는 결과는 항상 같을 것이다.

  • 재렌더링은 모든 컴포넌트의 render() 를 호출하는 것이지 React가 Unmount -> Mount 하는 것이 아니다.

  • 위 규칙에 따라 렌더링 전후에 변경된 부분만을 적용할 것이다.
    즉, 규칙에 따라 렌더링 전후에 변경된 부분만을 효율적으로 적용하는 것이다.

  • 현재 구현체에서는 한 종속 트리가 그 형제 사이에서 이동했다는 사실을 표현 가능하지만

  • 아예 다른 곳으로 이동했다는 사실을 표현할 수 없다.

  • 알고리즘은 전체 종속 트리를 재렌더링할 것이다.

리액트는 휴리스틱에 의존하고 있기 때문에, 휴리스틱 기반이 하고 있는 가정에 부합하지 않는 경우 성능이 나빠질 수 있다.
1. 알고리즘은 다른 컴포넌트의 타입을 갖는 종속 트리들의 일치 여부를 확인하지 않는다.

  • 매우 비슷한 결과물을 출력하는 두 컴포넌트를 교체할 경우
  • 그 둘을 같은 타입으로 만드는 것이 더 나을 수 있다.
  1. key는 반드시 변하지 않고, 예상이 가능하며, 유일해야 한다.
  • 많은 컴포넌트의 인스턴스와 DOM 노드를 불필요하게 재생성하여 성능이 나빠지거나 자식 컴포넌트의 state가 유실될 수 있다.

재조정 (Reconciliation)

2개의 댓글

comment-user-thumbnail
2023년 7월 31일

글 잘 봤습니다.

1개의 답글