왜 리액트에서 key를 사용할까?

우빈·2024년 4월 1일
7
post-thumbnail

"리액트에서 key를 사용하는 이유는 무엇인가요?" 라는 질문에 대해 정확히 답변하실 수 있나요?
저는 못했습니다. 이번엔 key를 쓰는 이유를 리액트의 구조와 관련하여 상세히 서술해보겠습니다.

이 글에서는...

  • 리액트에서 key를 사용하는 이유를 설명합니다.
    더불어, 배열에서 index로 key를 설정하는 것을 지양하는 이유를 설명합니다.
  • key와 렌더링이 어떤 관련이 있는지를 설명합니다.
  • 이와 관련된 리액트 파이버의 작동 원리에 대해 설명합니다.

리액트에서는 key를 왜 사용할까?

리액트를 사용하시다보면, 배열로 Element를 렌더링할 때 key를 주어야 한다는
warning이나 코드 리뷰를 받으신 적이 있을 겁니다.

도대체 그럼, 왜 key를 사용할까요?
저는 이를 자세히 모르기 전까지는 다음과 같이 답변했습니다.

각 요소를 구별해주기 위해서입니다!

좋습니다. 그렇다면 구별은 왜 할까요?

key의 "진짜" 역할

리액트에서 렌더링이 발생하는 경우 중 하나는 다음과 같습니다.

컴포넌트의 key props가 변경되는 경우

리액트에서 key는 명시적이지는 않지만 모든 컴포넌트에서 사용할 수 있습니다.

key는 형제 요소들 사이에서 렌더링 동안 동일한 요소를 식별하는 값입니다.
리렌더링이 발생할 때 리액트가 기존 DOM에서 어떤 변경사항이 있는지를 구별해야 하는데,
두 트리에서 같은 컴포넌트인지를 구별하는 것이 key입니다.

조금 더 들어가, 왜 index로 key를 설정하는 것을 지양하는 지에 대해서도 알아보겠습니다.

앞에서부턴 "index로 키를 설정하는 행위"를 key-index로 정의하겠습니다.

리액트 파이버

위에서 말한 동일한 요소를 구별하는 역할을 하는 친구가 "리액트 파이버"입니다.

리액트 파이버는...

  • Virtual DOM과 실제 DOM을 비교하여 변경 사항을 수집합니다.
  • 이 둘을 비교하여 차이가 있을 경우 화면에 렌더링을 요청합니다.

리액트 파이버가 key-index와 어떤 관련이 있을까요?
리액트 파이버에서는 다음과 같은 속성들이 존재합니다.

  • stateNode : 파이버 자체에 대한 reference를 가집니다.
    이를 바탕으로 파이버와 관련된 상태에 접근합니다.
  • child, sibling, return : 파이버 간의 관계를 나타냅니다.

우리가 자세히 보아야 할 것은 child, sibling, return입니다.

리액트에서는 children이 없고, child만이 존재합니다.
하지만 우리는 리액트에서 여러가지 요소들을 표현합니다.
리액트 파이버는 이를 어떻게 표현할까요?

child, sibling, return

<ul>
  <li>뿅뿅</li>
  <li>빠방방</li>
</ul>

파이버의 자식은 항상 첫 번째 자식의 참조로 구성됩니다.
리액트 컴포넌트의 root 요소가 무조건 하나여야 하는 이유도 이 때문입니다.

위 코드를 보면, 첫 번째 자식인 ul을 child로 지정합니다.
그리고 나머지 두 개의 <li/> 파이버는 형제, 즉 sibling으로 구성됩니다.
그러고 <li/> 파이버는 <ul/> 파이버를 return으로 갖게 됩니다.

설명이 길었네요, 마지막으로 sibling에 초점을 맞추어봅시다.

왜 key-index를 지양하는가?

리액트에서는 배열로 요소를 렌더링할 때 위에서 말한 형제 요소인
sibling에 초점을 맞춥니다.

만약 배열에 key가 없으면 단순히 파이버 내부의 sibling index만으로
요소의 변경사항을 판단합니다.

그렇기 때문에 작동하는 선에서 밑의 코드와 차이가 없는 것입니다.

<ul>
  {arr.map((_, i) => (
  	<Child key={index} />
  ))}  
</ul>

그렇다면 Math.random()을 사용하여 key를 주는 건 어떤가요?

Math.random()과 같이 매 렌더링마다 변하는 임의의 값을 넣는다 가정하면,
리렌더링이 일어날 때마다 sibling 컴포넌트를 명확히 구분할 수 없어 리렌더링이 발생합니다.

key를 사용하여 코드를 최적화하는 다른 방법

클릭했을 때마다 input에 focus를 주는 코드를 만들어보겠습니다.

const App = () => {
  const [select, setSelect] = useState(0);

  return (
    <div>
      <button onClick={() => setSelect((prev) => prev + 1)}>select</button>
      <h1>{select}</h1>
      <Focus key={select} select={select} />
    </div>
  );
};

const Focus = ({ select }) => {
  const [init, setInit] = useState("");

  useEffect(() => {
    setInit(true);
  }, [select]);

  return <input autoFocus={init} />;
};

보통은 이런 식으로 useEffect를 활용하여 이를 구현할 것입니다.
작동도 제대로 되지도 않고, 자식 컴포넌트에서 state를 하나 더 만들어서
useEffect를 사용해 렌더링시키는 비효율적인 구조를 가지고 있습니다.

key를 이용하면 이 코드를 매우 깔끔하게 바꿀 수 있습니다.

const App = () => {
  const [select, setSelect] = useState(0);

  return (
    <div>
      <button onClick={() => setSelect((prev) => prev + 1)}>select</button>
      <h1>{select}</h1>
      <Focus key={select} />
    </div>
  );
};

const Focus = () => {
  return <input autoFocus />;
};

이렇게 구현하면 key가 변경될 때마다 강제로 렌더링을 발생시켜 Focus가 업데이트됩니다.
key를 사용하면 props, state, useEffect 총 세 가지를 줄여 원하는 기능을
아주 간단하게 구현할 수 있습니다.

결론

key를 잘 사용한다면 배열 밖에서도 원하는 기능을 클린하게 구현할 수 있습니다.
우리 모두 key를 잘 사용하는 개발자가 됩시다..!!

profile
프론트엔드 공부중

4개의 댓글

comment-user-thumbnail
2024년 4월 1일

key를 잘못사용하면 불필요한 랜더링이 생길수 있으니 주의해야겠네요! 글 잘 읽고갑니다~

1개의 답글
comment-user-thumbnail
2024년 4월 2일

짤 재밌네요 ㅋㅋㅋ

1개의 답글