"리액트에서 key를 사용하는 이유는 무엇인가요?" 라는 질문에 대해 정확히 답변하실 수 있나요?
저는 못했습니다. 이번엔 key를 쓰는 이유를 리액트의 구조와 관련하여 상세히 서술해보겠습니다.
이 글에서는...
- 리액트에서 key를 사용하는 이유를 설명합니다.
더불어, 배열에서 index로 key를 설정하는 것을 지양하는 이유를 설명합니다.- key와 렌더링이 어떤 관련이 있는지를 설명합니다.
- 이와 관련된 리액트 파이버의 작동 원리에 대해 설명합니다.
리액트를 사용하시다보면, 배열로 Element를 렌더링할 때 key를 주어야 한다는
warning이나 코드 리뷰를 받으신 적이 있을 겁니다.
도대체 그럼, 왜 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만이 존재합니다.
하지만 우리는 리액트에서 여러가지 요소들을 표현합니다.
리액트 파이버는 이를 어떻게 표현할까요?
<ul>
<li>뿅뿅</li>
<li>빠방방</li>
</ul>
파이버의 자식은 항상 첫 번째 자식의 참조로 구성됩니다.
리액트 컴포넌트의 root 요소가 무조건 하나여야 하는 이유도 이 때문입니다.
위 코드를 보면, 첫 번째 자식인 ul을 child로 지정합니다.
그리고 나머지 두 개의 <li/>
파이버는 형제, 즉 sibling으로 구성됩니다.
그러고 <li/>
파이버는 <ul/>
파이버를 return으로 갖게 됩니다.
설명이 길었네요, 마지막으로 sibling에 초점을 맞추어봅시다.
리액트에서는 배열로 요소를 렌더링할 때 위에서 말한 형제 요소인
sibling에 초점을 맞춥니다.
만약 배열에 key가 없으면 단순히 파이버 내부의 sibling index만으로
요소의 변경사항을 판단합니다.
그렇기 때문에 작동하는 선에서 밑의 코드와 차이가 없는 것입니다.
<ul>
{arr.map((_, i) => (
<Child key={index} />
))}
</ul>
그렇다면 Math.random()을 사용하여 key를 주는 건 어떤가요?
Math.random()과 같이 매 렌더링마다 변하는 임의의 값을 넣는다 가정하면,
리렌더링이 일어날 때마다 sibling 컴포넌트를 명확히 구분할 수 없어 리렌더링이 발생합니다.
클릭했을 때마다 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를 잘 사용하는 개발자가 됩시다..!!
key를 잘못사용하면 불필요한 랜더링이 생길수 있으니 주의해야겠네요! 글 잘 읽고갑니다~