리액트에서 리스트를 이용해 컴포넌트를 렌더링하도록 하면 key prop을 붙이는 것을 권장하고 있습니다. 만약 key를 사용하지 않으면 다음과 같은 에러를 콘솔에 출력하고 내부적으로는 인덱스를 key로 사용하게 됩니다.

리액트에서 Key는 어떤 역할을 하길래 리스트의 렌더링에 필수적으로 사용하여야 할까요?

리스트 항목을 순서대로 유지

공식 문서에 따르면 리스트에서 key prop의 역할은 리스트 항목을 순서대로 유지하기 위해서입니다. key를 사용한다면 리스트의 각 요소를 식별하여 올바른 순서를 유지할 수 있도록 만듭니다. 그런데 리스트의 항목을 순서대로 유지한다는 것은 무슨 말일까요? 리스트라는 자료 구조 자체에서 이미 순서를 유지하기 때문에 key를 사용하지 않아도 순서는 유지되는 데 말입니다.

key의 작동 실험해보기

다음은 리액트에서 key의 역할을 보다 자세히 알아보기 위해 작성한 코드입니다.

export const array = [
  {
    id: 1,
    content: "컴포넌트" + 1,
  },
  {
    id: 2,
    content: "컴포넌트" + 2,
  },
  {
    id: 3,
    content: "컴포넌트" + 3,
  },
  {
    id: 4,
    content: "컴포넌트" + 4,
  },
  {
    id: 5,
    content: "컴포넌트" + 5,
  },
];

export type Element = (typeof array)[number];
import { useState } from "react";
import { array as _array } from "./array";
import Key from "./Key";

export default function KeyArray() {
  const [array, setArray] = useState(_array);

  function popMiddle() {
    const newArray = [...array];
    newArray.splice(Math.floor(newArray.length / 2), 1);
    setArray(newArray);
  }

  return (
    <>
      {array.length > 0 && array.map((e) => <Key {...e}></Key>)}
      <button onClick={popMiddle}>중간 요소 삭제하기</button>
    </>
  );
}
import { useEffect, useRef } from "react";
import { Element } from "./array";

export default function Key({ content }: Element) {
  const ref = useRef(content);
  console.log(`${content} 렌더링됨`);

  useEffect(() => {
    console.log(`${ref.current}이/가 새롭게 생성되었습니다. 상태:${ref.current}`);
    const currentRef = ref.current;
    return () => {
      console.log(`${currentRef}이/가 완전히 삭제되었습니다. 상태:${currentRef}`);
    };
  }, []);

  return (
    <div>
      {content} {ref.current} 의 상태
    </div>
  );
}

코드가 실행된 모습, 각 컴포넌트와 상태를 표시합니다.

이제 요소를 추가, 삭제할 때마다 컴포넌트의 렌더링, 생성, 삭제를 개발자 도구의 콘솔 도구에서 확인할 수 있습니다. key prop을 사용할 경우 우리가 예상할 수 있듯이 삭제되는 컴포넌트가 정확하게 삭제됩니다. 컴포넌트들이 전부 다시 렌더링되는 것은 부모의 상태가 변했기 때문입니다. 이를 방지하려면 React.memo를 사용할 수 있습니다.

 

삭제된 컴포넌트를 정확하게 인식합니다.

만약 위의 코드에서 key prop을 제거한다면 중간의 요소를 삭제할 때, 삭제한 인덱스 이후의 요소를 이전과 같은 컴포넌트로 인식하지 못해 모두 언마운트한 뒤 새롭게 마운트하게 됩니다. 이 때문에 이후의 컴포넌트가 상태를 가지고 있다면 자신의 상태를 잃게 되는데요. 대신 이전 번호의 컴포넌트가 가진 상태를 이어받아 상태가 서로 뒤틀리게 됩니다. 이는 key prop을 설정하지 않으면 리액트에서 자동적으로 인덱스를 key로 사용하기 때문이기도 합니다. 즉 key가 1씩 작아지기 때문에 상태가 잘못된 컴포넌트를 찾아가는 것입니다.

 

리스트에 컴포넌트3을 삭제하였는데 이상하게 컴포넌트 5가 삭제되었다고 표시됩니다.
하나씩 앞에 있던 컴포넌트의 상태를 이어받고 있습니다.

그리고 memo를 사용하면 작동을 더 정확하게 볼 수 있는데요. 실제로 props이 바뀌지 않았지만, key를 기준으로 컴포넌트를 구분하기 때문에 prop이 바뀐 것으로 인식되어 리렌더링이 발생합니다.

key를 인덱스로 설정할 경우 다음과 같이 바뀌므로 2와 3을 key로 가진 컴포넌트의  props이 바뀐 것으로 인식합니다. 
---삭제 전---
<Key key=0 content="컴포넌트1"></Key>  
<Key key=1 content="컴포넌트2"></Key>  
<Key key=2 content="컴포넌트3"></Key>  
<Key key=3 content="컴포넌트4"></Key>  
<Key key=4 content="컴포넌트5"></Key>  
---삭제 후---
<Key key=0 content="컴포넌트1"></Key>  
<Key key=1 content="컴포넌트2"></Key>  
<Key key=2 content="컴포넌트4"></Key>  
<Key key=3 content="컴포넌트5"></Key>  

 

그래서 다음과 같은 결과를 볼 수 있습니다.

삭제된 요소 이후의 컴포넌트는 props가 바뀐 것으로 인식합니다.

 

즉 key prop은 형제 컴포넌트 각각을 고유하게 식별하여 각 컴포넌트가 자신의 상태를 올바르게 찾아가도록 도와주는 역할을 합니다.

key를 이용해 컴포넌트 초기화하기

만약 특정 컴포넌트를 조건에 따라 초기화하고 싶을 때 key를 이용할 수도 있습니다.

import { useState } from "react";

export default function Counter() {
  const [state, setState] = useState(0);

  function change() {
    setState((e) => ++e);
  }

  return (
    <>
      <div>{state}</div>
      <button onClick={change}>더하기</button>
    </>
  );
}

더하기 버튼을 누르면 숫자가 1씩 증가하는 간단한 컴포넌트입니다. 물론 이 컴포넌트의 초기화는 간단하지만 더 복잡한 컴포넌트를 초기화하려고 한다면 코드가 길어질 수 있습니다. 이때 단순히 key를 바꿔주는 함수만 추가하면 해당 컴포넌트를 지우고 새로 렌더링할 수도 있습니다. 이를 활용하면 현재 선택한 요소에 따라 세부 정보가 나타나는 폼을 만들 때 현재 선택한 값을 key로 설정하면 이전 값 때문에 올바르지 않은 세부정보가 표시되는 것을 쉽게 막을 수 있는 등 생각보다 다양한 활용이 가능합니다.

 

import { useState } from "react";
import Counter from "./Counter";

export default function Initializer() {
  const [state, setState] = useState("a");

  function changeKey() {
    setState((e) => (e === "a" ? "b" : "a"));
  }
  return (
    <>
      <Counter key={state}></Counter>
      <button onClick={changeKey}>초기화</button>
    </>
  );
}

 

정리

  • key를 이용하면 같은 리액트 컴포넌트의 인스턴스가 자신의 상태를 올바르게 찾아 갈 수 있도록 할 수 있습니다.
    • 이 경우, key는 고유해야 하며 key를 변경해서는 안됩니다. 즉 렌더링 과정에서 key를 만드는 것은 올바른 사용법이 아닙니다!
  • key는 리액트가 각 컴포넌트로 생성된 인스턴스를 구분하는 역할을 하므로 컴포넌트 초기화에도 활용할 수 있습니다.

추가적으로 key는 {...props}처럼 구조 분해 할당으로 전달되어서는 안되며 무조건 명시적으로 전달되어야 합니다. 그리고 key는 실제 props가 아닌 pseudo-prop이라 불리는 prop으로 자식 컴포넌트에서 props.key로 접근이 불가능합니다. 해당 값에 접근하면 항상 undefined가 반환됩니다.

+ Recent posts