Frontend/React

react - key props(feat. 공식문서)

Creative_Lee 2023. 6. 14. 00:58

이번 포스팅에서는 react의 key props에 대해 탐구하는 시간을 가져보겠습니다.


1. Key란 ?

React 구 공식문서에는 이렇게 나와 있습니다.

Key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕습니다.
key는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 합니다.

 

React 신 공식문서에는 이렇게 나와있구요.

key는 각 컴포넌트가 어떤 배열 항목에 해당하는지 React에 알려주어 나중에 매칭할 수 있도록 합니다.
이는 배열 항목이 (정렬 등으로 인해) 이동하거나, 삽입되거나, 삭제될 수 있는 경우 중요해집니다.
잘 만들어진 key는 React가 정확히 무슨 일이 일어났는지 추론하고 DOM 트리를 올바르게 업데이트하는 데 도움이 됩니다.

 

아직 잘 모르겠네요...!

key가 등장한 이유를 먼저 알아봅시다...!


2. key가 등장한 이유(feat. diffing Algorithm)

key의 등장은 react의 diffing Algorithm과 밀접하게 연관되어 있습니다.

설명에 앞서 위 내용에 대한 구 공식문서 링크를 첨부합니다.

가볍게 정리하면 다음과 같습니다.

 

  • react는 보다 효과적으로 UI를 갱신하기 위해 diffing Algorithm을 사용합니다.
    이를 통해 기존과 비교해 어떤 변화가 있었는지 판단하고, 변경된 사항만을 실제 DOM에 반영합니다.
    (기존 react element tree와 새로운 react element tree를 비교합니다.)

  • 하지만 트리 변환을 위한 최첨단의 알고리즘도 O(n3)의 복잡도로 매우 비싼 연산비용이 발생했고,
    react는 2가지 가정을 기반으로 이를 O(n) 복잡도까지 최적화 했습니다.

  • 이 2가지 가정 중 하나가 바로 key props입니다.
    개발자가 list element의 자식 element에 key를 명시하여 어떤 자식 element가 변경되는지 알려줄 수 있습니다.

3. key를 명시하지 않았을 때 발생하는 비효율

그렇다면, 하라는대로 하지 않았을 때 어떤 비효율이 발생할까요?

key가 없을 때 볼 수 있는 경고

특정 위치에 있는 요소에 이동, 삽입, 삭제 등의 작업이 일어날 경우 react는 각 자식 element를 고유하게 식별할 수 없기에

이미 존재하는 자식 element 임에도 다시 랜더링 할 수 밖에 없습니다. 상당히 비효율적이죠. 

이는 배열의 규모가 크고 복잡할수록 더욱더 비효율적이게 됩니다.


4. 배열의 index를 key로 사용하는 것을 지양해야 하는 이유(버그 가능성)

한편, react는 key가 명시되지 않은 경우 기본적으로 배열의 index를 key로 사용합니다.

하지만 이는 공식 문서에도 나와 있듯 성능 문제뿐만 아니라,  버그를 야기하기도 합니다.

예시를 통해 직접 보며 알아봅시다.


4-1. 예시 코드 &  설명

예시 코드는 다음과 같습니다.

// 부모 컴포넌트
const Parent = () => {
  const [names, setNames] = useState(["도밥", "세인", "참새"]); // 부모의 배열 상태

  // 배열 요소 삭제 함수
  const deleteName = (targetName) => {
    const updatedNames = names.filter((name) => name !== targetName);
    setNames(updatedNames);
  };

  return (
    <div className="App">
      {names.map((name, idx) => (
        <Child key={idx} name={name} deleteName={deleteName} /> // index를 key로 사용
      ))}
    </div>
  );
}

부모 컴포넌트는 크루들의 이름으로 이루어진 배열을 순회하며 자식 컴포넌트를 랜더합니다.

이때 index를 key로 사용했습니다.


// 자식 컴포넌트
const Child = ({ name, deleteName }) => {
  const [text, setText] = useState(""); // 각 자식의 고유 상태

  return (
    <>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <div>{name}</div>
      <button onClick={() => deleteName(name)}>
        {`이름 배열에서 ${name} 지우기`}
      </button>
    </>
  );
};

각 자식 컴포넌트는 고유한 text 상태를 가지고 있습니다.


독립적으로 입력 가능

때문에 각 텍스트 상자에 독립적으로 입력이 가능합니다.


4-2. 삭제 실행 결과... 버그 발생!

이때 이름 배열의 맨앞에 있는 '도밥'을 삭제하면 어떻게 될까요?

의도대로라면 다음과 같이 동작해야 합니다.

  1. '세인', '참새' 만을 포함한 배열로 리렌더링
  2. 자식 element는 각 text 상태를 그대로 유지함

 

한번 삭제해 봅시다.

????

삭제는 되었으나, text 상태가 이상합니다!

이미 삭제된 '도밥'의 text 상태를 '세인'이 가지고 있네요.

'참새'의 text 상태는 사라지고 엄한 '세인'의 text 상태를 가지고 있습니다!

버그가 발생했습니다.


4-3. 버그의 이유

버그의 이유는 구 공식 문서에 다음과 같이 나와 있습니다.

컴포넌트 인스턴스는 key를 기반으로 갱신되고 재사용됩니다.
배열의 index를 key로 사용하면 항목의 순서가 바뀌었을 때 key 또한 바뀔 것입니다.
그 결과로 컴포넌트의 state가 엉망이 되거나 의도하지 않은 방식으로 바뀔 수도 있습니다.

 

구 공식문서에서 추천하는 index를 key로 사용했을 때의 부정적인 영향 상세 설명 블로그를 보면 다음과 같이 말합니다.

If the key is same as before React assumes that the DOM element represents the same component as before.
키가 이전과 동일한 경우 React는 DOM 요소가 이전과 동일한 구성 요소를 나타낸다고 가정합니다.

 

위 내용을 토대로 예제를 풀어서 설명하면 다음과 같습니다.

  1. 도밥(0), 세인(1), 참새(2) 의 key를 가지고 첫 렌더
  2. 도밥 삭제
  3. 세인(0), 참새(1) 의 key를 가지고 리렌더
    • 이때 react는 0, 1이라는 key를 확인하고 기존 0, 1 key에 해당하는 컴포넌트 재사용
    • 기존 0, 1 key에 해당하는 컴포넌트는 각각 '나는 도밥', '나는 세인' text 상태를 가지고 있음.
  4. 의도하지 않은 버그 발생

4-4. Bug fix !

이제 버그를 고쳐봅시다.

 return (
    <div className="App">
      {names.map((name) => (
        <Child key={name} name={name} deleteName={deleteName} /> // 고유한 key를 사용
      ))}
    </div>
  );

부모 컴포넌트에서 고유한 key를 사용하도록 변경했습니다.


편안

이제 의도한 대로 동작하는 것을 확인할 수 있습니다.


번외.  key 유무에 따른 렌더링 성능 테스트

포스팅 당시 마침 크루 요술토끼key의 유무에 따른 랜더링 성능 테스트를 하고 있었어요.

직관하다가 부탁해서 성능 측정 자료를 제공받았습니다!😀(고마워요)


1. 테스트 설명

총 5만개의 div, span

홀수 번째에는 div를, 짝수 번째에는 span을 넣어 총 5만개의 요소가 들어있는 list를 랜더링한 모습입니다.

 

테스트 하고자 하는 내용은 다음과 같아요.

  • n번째 요소와 n+1 번째 요소의 위치를 서로 바꾸도록 하고 그 과정에서의 성능을 profiler로 측정한다.
    • 대조군 1에는 고유한 key를 제공한다.
    • 대조군 2에는 key를 제공하지 않는다

 

의도한 결과는 다음과 같습니다.

  • 대조군 1은 자식요소의 위치만 변경되었다는 사실을 알 수 있고 자식 컴포넌트는 재사용된다.
  • 대조군 2는 모든 자식 요소에게 부여된 새로운 key로 인해 5만개의 자식컴포넌트가 모두 새롭게 그려진다.

2. 테스트 결과

다음은 profiler로 확인한 결과입니다.

Key: 3.7s / no Key: 18.5s

Key : 3.7s 

no Key : 18.5s

 

다른 조건들은 고려되지 않아 정확하지는 않겠지만,

랜더링 성능 측면에서 key가 주는 영향은 생각보다 엄청나다는 사실을 확인할 수 있었습니다.

 

 

끄읏...!

 

 

 

공식문서는 언제나 옳다.