React의 Reconciliation과 최적화 - (2)

shorecrab·2022년 6월 12일
0

지난 글에서 Diffing 알고리즘과 React.memo()를 사용한 최적화 방법에 대해서 알아봤다. 이번에는 Diffing의 Key에 대해서 조금 더 자세히 알아보려고 한다.

Key

왜 key를 사용할까?

지난 글에서 엘리먼트가 update 되어야 할 때, 같은 타입의 엘리먼트에 대해서 어떻게 처리하는지를 알아봤다. 그런데 같은 타입의 엘리먼트가 여러개 존재하고, 순서만 변경되는 경우에는 어떻게 될까?

아래 예제를 보자. 코드가 길어보이지만 하는 일은 간단하다. 리스트에 새로운 아이템을 추가하고, 이를 Item 컴포넌트를 통해 렌더링하는 것이다. 추가적으로 오름차순, 내림차순 정렬을 할 수 있도록 버튼을 달아줬다.

//App.js
import Item from "./list-item";
import { useState } from "react";

export default function App() {
  const [list, setList] = useState([]);
  const handleAddClick = (e) =>
    setList((prevList) => {
      return [...prevList, prevList.length + 1];
    });

  const handleAscendingSortClick = (e) =>
    setList((prevList) => [...prevList].sort());

  const handleDescendingSortClick = (e) =>
    setList((prevList) =>
      [...prevList].sort((a, b) => {
        return b - a;
      })
    );

  console.log("App rendered");

  return (
    <>
      {list.map((v, i) => {
        return <Item itemId={v} />;
      })}
      <div>
        <button onClick={handleAddClick}>Add Item</button>
        <button onClick={handleAscendingSortClick}>Ascending Sort</button>
        <button onClick={handleDescendingSortClick}>Descending Sort</button>
      </div>
    </>
  );
}

Item 컴포넌트는 itemId를 props으로 받아서 보여주고, 컴포넌트가 update된 시간을 보여주고 있다. React.memo()를 사용했는데, 부모 컴포넌트에서 상태가 변경됐을 때 자식이 update되는 것을 방지하기 위해서 사용했다.

//list-item.js
import React from "react";

function ListItem(props) {
  console.log(`Item ${props.itemId} rendered`);

  return (
    <div>
      {props.itemId} / {new Date().toTimeString()}
    </div>
  );
}

const MemoizedListItem = React.memo(ListItem);
export default MemoizedListItem;

이제 코드를 실행해서 리스트에 아이템을 추가하고 정렬을 해보자.

React.memo()를 사용했음에도 모든 컴포넌트가 update되는 것처럼 보인다(시간 부분을 잘 보자). 그리고 리스트에 item이 5개 있을 때 내림차순 정렬하는 모습을 유심히 보면, 3번 item은 다시 렌더링되지 않는데 나머지는 다시 update되는 이상한 모습을 볼 수 있다.
정렬을 할 때 React에서는 새로운 컴포넌트가 추가된 것인지, 아니면 기존의 컴포넌트가 위치만 변경된 것인지를 알 수 없기 때문에 이러한 일이 발생하는 것이다. 그래서 기존에 컴포넌트가 존재했다는 것을 알려주기 위해서 key라는 것이 사용된다.

key 사용

리액트에서는 key를 사용해서 동일한 엘리먼트 간 비교를 수행한다. key가 주어지지 않으면 암묵적으로 인덱스를 key로써 사용한다. 이전의 예제에서도 암묵적으로 index가 key로 사용되었다고 볼 수 있다. 그래서 item이 5개 있을 때, 3번이 update되지 않은 것이다. 1-2-3-4-5 순서로 있던 리스트에 대해서 내림차순 정렬을 하게 되면 5-4-3-2-1이 되고, 3번 컴포넌트는 변경되지 않았다고 판단하여 update가 일어나지 않는다.

그렇다면 명시적으로 한 번 key를 사용해보자.

//App.jsx의 return 부분
return (
  <>
    {list.map((v, i) => {
      return <Item itemId={v} key={v} />;
    })}
    <div>
      <button onClick={handleAddClick}>Add Item</button>
      <button onClick={handleAscendingSortClick}>Ascending Sort</button>
      <button onClick={handleDescendingSortClick}>Descending Sort</button>
    </div>
    </>
);

App.jsx에서 key를 명시적으로 넘겨주도록 return 부분을 변경했다. 이제 한 번 실행해보자.

순서가 변경될 때 update가 일어나지 않는 것을 볼 수 있다. 명시적으로 key를 넘겼기 때문에 React에서 이전에 존재했던 컴포넌트라는 것을 알 수 있었기 때문이다.

주의점

첫번째로, 인덱스를 key로 전달하는 것은 key를 전달하지 않는 것과 다를바가 없다.

//App.jsx의 return 부분
return (
  <>
    {list.map((v, i) => {
      return <Item itemId={v} key={i} />;
    })}
    <div>
      <button onClick={handleAddClick}>Add Item</button>
      <button onClick={handleAscendingSortClick}>Ascending Sort</button>
      <button onClick={handleDescendingSortClick}>Descending Sort</button>
    </div>
    </>
);

App.jsx의 return 부분에서 key={i}로 수정한 코드이다. 실행해보면 처음에 실행했던 화면과 다른 점이 없이 이상하게 작동한다. 이미 암묵적으로 key가 index로 넘어가는 것을 명시적으로 넘겨준 차이 밖에 없기 때문이다.

두번째로, key를 계속 변경되는 값으로 두면 안된다.

//App.jsx의 return 부분
return (
  <>
    {list.map((v, i) => {
      return <Item itemId={v} key={Math.random()} />;
    })}
    <div>
      <button onClick={handleAddClick}>Add Item</button>
      <button onClick={handleAscendingSortClick}>Ascending Sort</button>
      <button onClick={handleDescendingSortClick}>Descending Sort</button>
    </div>
    </>
);

App.jsx의 return 부분에서 key={Math.random()}로 수정한 코드이다. 실행해보면 아래와 같은 결과가 나온다.

Item을 추가만 하는 경우에도 모든 컴포넌트에 대해서 매번 update가 수행된다. 부모 컴포넌트의 상태가 변경되면서 Math.random() 함수가 다시 실행되어 key가 매번 변경되기 때문에, React에서는 새로운 컴포넌트로 인식하고 이전 V-dom tree를 파괴하고 새로운 tree를 형성하려고 하기 때문이다.

마무리

결론적으로, 리스트를 활용해서 컴포넌트를 렌더링하게 된다면 key를 명시적으로 넘겨주되 index가 아니라 리스트 item에 고유한 값을 주어야하고, 계속해서 변경되는 값을 주어서는 안된다.
오늘도 React의 재조정과 최적화에 대해서 글을 작성했는데, 다음을 마지막으로 마무리 지으려고 한다. 다음에는 React.memo()를 사용할 때 추가적인 주의점에 대해서 말해보려 한다.


참고자료

https://ko.reactjs.org/docs/reconciliation.html#the-diffing-algorithm

profile
주니어 프론트엔드 개발자!

0개의 댓글