React Key

R정우·2023년 10월 2일
46

React

목록 보기
1/4


리액트에서 key는 보통 map 함수를 사용하여 이터러블(iterable) 객체를 렌더링할 때 사용된다

const ItemList = ({ items }) => {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

이 글에서는 key가 왜 필요한지에 대해 알아본다

1. 컴포넌트의 고유성

리액트에서는 컴포넌트마다 고유의 메모리를 보유하고 있다

이 메모리를 통해 useState, useRef를 사용해서 컴포넌트가 리렌더링 되더라도 이전의 상태나 값을 기억하도록 할 수 있다

컴포넌트의 메모리가 실제로 컴포넌트 내부에 존재한다면 key는 필요가 없을 것이다

하지만 컴포넌트의 메모리는 리액트의 내부 인스턴스와 상태 저장 매커니즘에 저장된다

그 후 렌더링 과정으로 생성된 UI 트리에 따라 컴포넌트에 순차적으로 연결된다

실제로는 보다 복잡한 메커니즘이지만 이해를 돕기 위해 순차적으로 연결된다고 가정한다

문제는 이터러블한 객체는 리렌더링 할 때에 그 순서가 바뀔 수 있다는 것이다
다음의 예시를 보자

const Component = ({ alphabet }) => {
  const [count, setCount] = useState(0);

  const addCount = () => {
    setCount((prev) => prev + 1);
  };

  return (
    <div>
      <div>{alphabet}</div>
      <div>{count}</div>
      <button onClick={addCount}>+</button>
    </div>
  );
};

const App = () => {
  const [arr, setArr] = useState(['A', 'B', 'C']);

  const swapBC = () => {
    setArr((prev) => [prev[0], prev[2], prev[1]]);
  };

  return (
    <div>
      {arr.map((alphabet) => (
        <Component name={alphabet} />
      ))}
      <button onClick={swapBC}>swapBC</button>
    </div>
  );
};

각각 A, B, C 라는 name 속성을 가진 세 컴포넌트가 존재하고, 이 컴포넌트들은 모두 각자의 count 상태를 가지고 있다
swapBC 버튼을 클릭하는 것으로 BC 컴포넌트의 위치를 바꿀 수 있다

초기 렌더링 시에 다음과 같은 모습일 것이다

이 때 BC의 자리를 바꾸면 어떻게 될까?

분명 B의 카운트를 5까지 올린 후 BC의 자리를 바꿨는데 C의 카운트가 5가되고 B는 0이 되었다

그림과 같이 이유를 확인해보자

카운트를 담은 각 메모리는 UI 트리에 순차적으로 연결이 됐었다

BC의 순서가 바뀌었지만 UI 트리에서 보면 B의 자리에 C가 왔기 때문에 B에 연결되었던 메모리가 C에 연결 된 것이다

이처럼 각 컴포넌트 메모리는 어떤 컴포넌트에 연결되는 것이 아닌 어디에 있는 컴포넌트에 연결되고 이는 기대와 다른 결과를 가져올 수 있다

index값을 key의 값으로 사용하는 것은 안티패턴이라는 말이 존재하는데 위의 예시가 그 이유다
실제로 이터러블한 함수 내부에서 key를 명시적으로 할당하지 않으면 내부적으로 index 값이 할당된다

그렇다면 key를 할당하는 것으로 문제를 해결해보자

const App = () => {
  const [arr, setArr] = useState([
    { id: 0, alphabet: 'A' },
    { id: 1, alphabet: 'B' },
    { id: 2, alphabet: 'C' },
  ]);

  const swapBC = () => {
    setArr((prev) => [prev[0], prev[2], prev[1]]);
  };

  return (
    <div>
      {arr.map(({ id, alphabet }) => (
        <Component key={id} name={alphabet} />
      ))}
      <button onClick={swapBC}>swapBC</button>
    </div>
  );
};

각각의 컴포넌트의 key 값으로 id를 할당해주었다

이것은 React가 컴포넌트에 메모리를 연결할 때 다음과 같은 명령을 내린다

UI 트리의 위치가 아닌, 특정 key를 가진 컴포넌트를 찾아가

이제 메모리는 특정 위치의 컴포넌트에 연결되는 것이 아닌 특정 key를 가진 컴포넌트에 연결되게 된다

따라서 위치를 바꾸더라도 정확하게 기대와 같이 동작하게 된다

이 떄문에 key 값은 고유해야하며, 이는 형제 요소 사이에서만 고유하면 된다

2. 렌더링 최적화

리액트는 렌더링 시에 가상 DOM을 생성하고 Reconciliation(재조정) 과정에서 이전 렌더와 새 렌더 사이의 차이를 비교한 후, 필요한 변경만 DOM에 적용한다

이는 React의 렌더링 최적화 기법으로 불필요한 DOM 조작을 줄이고 성능을 향상시킨다

위에서 이터러블(iterable) 객체를 렌더링할 때 명시적으로 key 값을 할당하지 않으면 index 값이 할당된다고 했다

이는 key의 변경을 발생시킬 수 있다

다음의 예시를 살펴보자

export const App = () => {
  const [arr, setArr] = useState(['A', 'B', 'C']);

  return (
    <ul>
      {arr.map((alphabet, index) => (
        <li key={index}>{alphabet}</li>
      ))}
    </ul>
  );
};

그림으로 보면 다음과 같은 모습일 것이다

이 때, AB사이에 D를 추가해보자

Dkey 값으로 1이 할당되고 B, Ckey 값이 각각 1에서 22에서 3으로 변경되었다

key가 변경되면 뭐가 문제일까?

key 값이 변경되면 React는 해당 컴포넌트를 완전히 새로운 컴포넌트로 간주하고, 이전 인스턴스를 파괴한 후에 새로운 인스턴스를 생성한다

위의 특성을 활용해 key가 필요하지 않은 단일 요소에 의도적으로 key를 할당하거나 변경하는 것으로 해당 컴포넌트의 상태를 초기화할 수 있다

이는 강제적으로 DOM 업데이트를 발생시키기 때문에 성능에 부정적인 영향을 미칠 수 있다

D는 새롭게 추가되었기에 DOM에 새롭게 그려지는 것이 당연하지만, BC는 그 내부가 전혀 바뀌지 않았음에도 DOM에 새롭게 그려진다는 것이다

다시 고유의 key를 할당하는 것으로 문제를 해결해보자

export const App = () => {
  const [arr, setArr] = useState([
    { id: 0, name: 'A' },
    { id: 1, name: 'B' },
    { id: 2, name: 'C' },
  ]);

  return (
    <ul>
      {arr.map(({ id, name }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  );
};

이 상태에서 이전과 같이 AB 사이에 { id: 3, name: 'D' }를 추가해보자

D가 추가되었지만 각각의 컴포넌트의 key 값은 변하지 않았다

때문에 가상 DOM으로부터 실제 DOM에 반영될 때 변경된 부분만 반영하게 되고, BC 컴포넌트에 변경이 없었다고 가정한다면 D만 실제 DOM에 추가되게 되는 것이다

profile
시행착오를 즐기는 프론트엔드 개발자입니다!

4개의 댓글

comment-user-thumbnail
2023년 10월 4일

퀄리티 좋네요..

답글 달기
comment-user-thumbnail
2023년 10월 4일

야무지네요!!

답글 달기
comment-user-thumbnail
2024년 4월 9일

key 값과 관련된 내용을 정리한 글 중에 가장 깔끔하게 정리된 글 인 것 같아요! 잘 읽었습니다

1개의 답글