[developic] react list key error, uuid를 써도 될까?

sue·2021년 5월 20일
9

developic project

목록 보기
17/28
post-custom-banner

블로그 페이지를 최종 점검하는데 픽스토리 페이지 접근 시 콘솔창에서 다음과 같은 경고를 만났다.

Warning: Each child in a list should have a unique "key" prop
리스트에 각각의 child는 고유한 'key'를 가지고 있어야 한다는 것이다.

리스트 Key의 필요성

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

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li key={number.toString()}>
    {number}
  </li>
);

Key를 선택하는 가장 좋은 방법은 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것이다. 대부분의 경우 데이터의 ID를 key로 사용한다.

const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);

렌더링 한 항목에 대한 안정적인 ID가 없다면 최후의 수단으로 항목의 인덱스를 key로 사용할 수 있다.

const todoItems = todos.map((todo, index) =>
  // Only do this if items have no stable IDs
  <li key={index}>
    {todo.text}
  </li>
);

하지만 항목의 순서가 바뀔 수 있는 경우, key에 인덱스를 사용하는 것은 권장하지 않는다. 이로 인해 성능이 저하되거나 컴포넌트의 state와 관련된 문제가 발생할 수 있기 때문이다. 만약 리스트 항목에 명시적으로 key를 지정하지 않으면 React는 기본적으로 인덱스를 key로 사용한다.

자식에 대한 재귀적 처리

DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다.

예를 들어, 자식의 끝에 엘리먼트를 추가하면, 두 트리 사이의 변경은 잘 작동할 것이다.

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React는 두 트리에서 <li>first</li>가 일치하는 것을 확인하고, <li>second</li>가 일치하는 것을 확인한다. 그리고 마지막으로 <li>third</li>를 트리에 추가한다.

하지만 위와 같이 단순하게 구현하면, 리스트의 맨 앞에 엘리먼트를 추가하는 경우 성능이 좋지 않다. 예를 들어, 아래의 두 트리 변환은 형편없이 작동한다.

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React는 <li>Duke</li><li>Villanova</li> 종속 트리를 그대로 유지하는 대신 모든 자식을 변경한다. 왜냐하면 모든 요소가 자신의 기존 자리에 있지 않다고 판단하기 때문이다. 이러한 비효율을 해결하기 위해 React는 key 속성을 지원한다.

Keys🔎

자식들이 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>

이제 React는 '2014' key를 가진 엘리먼트가 새로 추가되었고, '2015'와 '2016' key를 가진 엘리먼트는 그저 이동만 하면 되는 것을 알 수 있다.

실제로, key로 사용할 값을 정하는 것은 어렵지 않다. 그리려고 하는 엘리먼트는 일반적으로 식별자를 가지고 있을 것이고, 그대로 해당 데이터를 key로 사용할 수 있다.

만약 이러한 상황에 해당하지 않는다면, 데이터 구조에 ID라는 속성을 추가해주거나 데이터 일부에 해시를 적용해서 key를 생성할 수 있다. 해당 key는 오로지 형제 사이에서만 유일하면 되고, 전역에서 유일할 필요는 없다.

key로 사용할 값이 없다면❓

원래는 key값이 필요할 경우, 대부분 요소의 id값을 key값으로 사용해왔다. 그런데 픽스토리의 썸네일을 보여줄 때, 해당 픽스토리에 등록된 포스트들의 썸네일을 맵으로 돌려 6개까지 보여주게 했는데 이 썸네일은 이미지의 주소만 저장된 데이터라 고유한 id값을 뽑아올 수가 없는 상황이었다.

최후의 수단으로 배열의 인덱스를 key로 지정할 수 있지만, index를 key로 사용할 때 배열의 재배열이 일어나게 되면 컴포넌트의 state와 관련된 문제가 발생할 수 있다.

컴포넌트 인스턴스는 key를 기반으로 갱신되고 재사용된다. 인덱스를 key로 사용하면, 항목의 순서가 바뀌었을 때 key 또한 바뀌게 된다. 그 결과로, 컴포넌트의 state가 엉망이 되거나 의도하지 않은 방식으로 바뀔 수도 있다. 그렇기 때문에 index를 사용할 순 없었고, 다른 방식으로 고유한 아이디를 생성해야 했다.

uuid

UUID(Universally Unique IDentifier)는 범용 고유 식별자로 소프트웨어 구축에 쓰이는 식별자 표준이다.

네트워크 상에서 서로 모르는 개체들을 식별하고 구별하기 위해서는 각각의 고유한 이름이 필요하다. 이 이름은 고유성(유일성)이 매우 중요하다. 같은 이름을 갖는 개체가 존재한다면 구별이 불가능해 지기 때문이다. 이를 위하여 탄생한 것이 범용고유식별자(UUID)이며 국제기구에서 표준으로 정하고 있다.

UUID는 32개의 16진수로 표현되며 총 36개 문자(32개 문자와 4개의 하이픈)로 된 8-4-4-4-12라는 5개의 그룹을 하이픈으로 구분한다.

uuid 설치

yarn add uuid
yarn add @types/uuid

uuid 사용

key값에 간단하게 uuidv4()로 사용해주면 된다.

          <ul className="picstory__recent-img">
            {posts &&
              posts.slice(0, 6).map((picstoryImgItem: { thumbnail: string }) => (
                <li className="img__box" key={uuidv4()}>
                  <img src={picstoryImgItem.thumbnail} alt="picstory-thumbnail" />
                </li>
              ))}
          </ul>	

이렇게 uuid로 고유한 아이디를 생성해 key값으로 넣어주고 나니 key가 필요하다는 콘솔창의 에러는 더이상 나타나지 않았다.

그렇지만 찝찝하게 남은 리액트 key의 포인트가 있었다.

💥 이렇게 쓰면 안된다

이미 uuid를 사용해 코드를 수정한 뒤 이 글을 작성중이었다.
uuid를 도입한 당시엔,

key값이 없는 데이터에 key값으로 사용할 고유한 아이디가 필요 =>
아이디가 중복없이 랜덤하게 생성되는 uuid를 이용해보자

의 흐름이었다.

하지만 블로그를 작성하면서 react에서 key가 필요한 이유를 자세히 공부하다보니...
uuid 쓰면 안되겠는데? 라는 결론이 나왔다🤔

왜냐하면 유니크한 키를 부여해달라는 오류는 해결 됐지만, 배열이 변경된 경우 동일한 콘텐츠를 가진 요소의 UUID가 다시 생성된다.

리액트에선 키를 한 번 할당하면 목록의 모든 항목이 매번 동일한 키를 받는 것이 이상적이다. 왜냐하면 매번 키가 새로 생성되면 React가 가상 DOM을 조정하고 결정할 때 데이터 변경을 최적화 할 수 없기 때문이다. 이게 바로 리액트에서 키가 필요한 이유임에도 불구하고!

https://stackoverflow.com/questions/39549424/how-to-create-unique-keys-for-react-elements 해당 문제에 관한 열띤 토론의 현장
5 Common Mistakes with Keys in React 2번 문제에 해당

uuid와 비슷한 라이브러리인 nanoId에서는 공식 문서에서 아예 리액트에서 사용하지 않을 것을 권장했다.

key 렌더 간에 일관성이 있어야 하기 때문에 현재 React prop에 nanoid를 사용하는 올바른 방법은 없습니다.

function  Todos ( { todos } )  { 
  return  ( 
    < ul > 
      { todos . map ( todo  =>  ( 
        < li  key = { nanoid ( ) } > / * DO N'T DO IT * /
           { todo . text } 
        < / li > 
      ) ) } 
    < / ul > 
  ) 
}

이를 해결하기 위해 uuid와 index를 쓰지 않고 각 자식에 고유한 아이디를 줄 수 있는 모든 방법을 검색해봤는데...

생각보다 답은 가까운 곳에 있었다. 너무 간단해서 뭐야 왜 이걸 먼저 생각 못했지? 허무했을 정도😶😶

<ul className="picstory__recent-img">
  {posts &&
   posts.slice(0, 6).map(picstoryImgItem => (
     <li className="img__box" key={picstoryImgItem.id}>
                         <img src={picstoryImgItem.thumbnail} alt="picstory-thumbnail" />
     </li>
   ))}
  </ul>

없다던 아이디가 어떻게 생겼냐면, 데이터 구조 상 포스트 썸네일은 아이디를 가지고 있지 않지만, 1포스트 1썸네일이기 때문에 사실상 포스트의 아이디와 일치했다. 그래서 포스트의 아이디를 썸네일 아이디로 사용해주었다.

해결 방법이 아주 허무했지만 리액트 리스트에서 키의 중요성을 다시한번 짚고 넘어갈 수 있었던 계기가 되었다.

key는 반드시 변하지 않고, 예상 가능하며, 유일해야 한다. 변하는 key(Math.random()으로 생성된 값 등)를 사용하면 많은 컴포넌트 인스턴스와 DOM 노드를 불필요하게 재생성하여 성능이 나빠지거나 자식 컴포넌트의 state가 유실될 수 있다.

👉 공식문서를 읽을 때 이 부분이 조금 찜찜했는데 반신반의하며 uuid를 써보고 나서 깨달은 나🤦‍♀️.... 앞으론 쓰기 전에 먼저 공부하자.


📌 결론

  • 리액트에서 리스트의 각각의 자식 요소는 고유한 'key'를 가지고 있어야 한다. 리액트는 자식을 처리할 때 기본적으로 동시에 두 요소를 순회하고 차이점이 있을 때 변경시키는데, 키가 있다면 해당 키를 가진 요소의 변화를 캐치해 빠르게 변경시킬 수 있기 때문이다. 또한 변화가 없는 요소의 경우 리렌더링할 필요가 없어진다.

  • key로 사용할 id가 없을 때 최후의 수단으로 index를 사용할 수 있지만 리스트 배열에 변화가 일어날 경우 문제가 발생하기 때문에 권장하지 않는다.

  • 리스트의 키의 핵심
    : '변하지 않고, 예상 가능해야 하고, 유일해야 한다.'
    따라서 uuid, shortid, Math.random()과 같은 방식으로 키를 생성하는 것은 X

References

post-custom-banner

2개의 댓글

comment-user-thumbnail
2022년 8월 10일

uuid로 사용하되 map으로 렌더링 되기 이전에 마운트되는 시점 한번에만 고유한 키를 부여해주면 되지 않을까요~?

답글 달기
comment-user-thumbnail
2024년 5월 12일

만약 1포스트 1섬네일 같이 1대1 대응이 되지 않는 경우에는 어떤 값을 key로 사용하실 건지 궁금합니다!!!

답글 달기